面试准备(MySQL存粹问题版)

一、MySQL基础篇

面试官:请你介绍一下MySQL的架构,它主要由哪些部分组成?

回答:

好的,MySQL的架构可以分为三层,我从上到下给您介绍一下:

第一层是连接层,也叫客户端连接层。当我们的应用程序连接MySQL时,首先会经过这一层。它主要负责:

  • 处理客户端的连接请求
  • 进行身份认证,验证用户名密码
  • 管理连接池,维护线程

第二层是服务层,这是MySQL的核心层,包含了很多重要组件:

  • 查询缓存:不过在MySQL 8.0已经移除了,因为命中率太低
  • 解析器:对SQL语句进行词法分析和语法分析,生成解析树
  • 优化器:这个很重要,它会对SQL进行优化,选择最优的执行计划,比如选择用哪个索引
  • 执行器:调用存储引擎接口,执行SQL语句

第三层是存储引擎层,MySQL支持插件式的存储引擎,常见的有InnoDB、MyISAM等。存储引擎负责数据的存储和读取。

一条SQL语句的执行流程大概是这样的:客户端发送SQL → 连接器验证 → 解析器解析 → 优化器优化 → 执行器执行 → 存储引擎读写数据。


面试官:你刚才提到了InnoDB和MyISAM,能详细说说它们的区别吗?

回答:

好的,这两个是MySQL最常用的存储引擎,它们的区别我从几个维度来说:

首先是事务支持方面:

  • InnoDB支持事务,支持ACID特性,可以进行COMMIT和ROLLBACK操作
  • MyISAM不支持事务,如果操作出错,没办法回滚

其次是锁的粒度:

  • InnoDB支持行级锁,锁的粒度更细,并发性能更好
  • MyISAM只支持表级锁,一个写操作会锁住整张表,并发性能较差

第三是外键约束:

  • InnoDB支持外键,可以保证数据的引用完整性
  • MyISAM不支持外键

第四是索引结构:

  • InnoDB使用聚簇索引,数据文件和索引文件是绑定在一起的,主键索引的叶子节点直接存储数据行
  • MyISAM使用非聚簇索引,数据文件和索引文件是分开的,索引的叶子节点存储的是数据的地址

第五是COUNT(*)性能:

  • MyISAM会保存表的总行数,执行COUNT(*)时直接返回,非常快
  • InnoDB不保存行数,需要全表扫描来统计

第六是崩溃恢复:

  • InnoDB有redo log和undo log,支持崩溃后的数据恢复
  • MyISAM没有这个机制,崩溃后可能丢失数据

总结一下,现在基本都推荐使用InnoDB,它从MySQL 5.5版本开始就是默认引擎了。除非是一些只读的、不需要事务的场景,否则都应该选InnoDB。


面试官:你提到InnoDB使用聚簇索引,能详细解释一下什么是聚簇索引和非聚簇索引吗?

回答:

好的,这个问题很重要,我来详细说一下。

聚簇索引,英文叫Clustered Index,它的特点是:索引和数据是存储在一起的。在InnoDB中,主键索引就是聚簇索引,它的B+Tree叶子节点直接存储的是完整的数据行。

举个形象的例子,聚簇索引就像一本按拼音顺序排列的字典,字的解释就直接跟在拼音后面,找到拼音就找到了内容。

非聚簇索引,也叫二级索引或辅助索引,它的叶子节点存储的不是数据行,而是主键的值。

比如我们在username字段上建了一个普通索引,当我们通过username查询时:

  1. 首先在username索引的B+Tree中找到对应的主键id
  2. 然后再用这个id去聚簇索引中查找完整的数据行

这个过程就叫做回表

关于回表,我再补充一下: 回表是有性能损耗的,因为需要两次B+Tree查找。所以在实际开发中,我们会尽量避免回表,方法就是使用覆盖索引

什么是覆盖索引呢?就是我们查询的字段刚好都在索引中,不需要回表就能拿到数据。比如:

-- 假设有联合索引 idx_name_age(name, age)SELECT name, age FROMuserWHERE name ='张三';-- 这个查询就用到了覆盖索引,不需要回表

还有一点要补充:InnoDB要求表必须有主键。如果我们没有显式定义主键,InnoDB会这样处理:

  1. 首先找一个非空的唯一索引作为聚簇索引
  2. 如果也没有,就会自动生成一个6字节的隐藏主键ROW_ID

所以建表时一定要显式定义主键,而且推荐使用自增主键,因为顺序插入效率更高,不会造成页分裂。


二、索引篇

面试官:为什么MySQL选择B+Tree作为索引的数据结构,而不是B-Tree或者Hash?

回答:

这是个好问题,我从几个角度来分析:

首先说说为什么不用Hash:

Hash索引的等值查询确实很快,时间复杂度是O(1)。但它有几个致命缺点:

  1. 不支持范围查询:Hash只能做等值比较,像WHERE age > 20这种范围查询就没法用
  2. 不支持排序:Hash是散列存储的,没有顺序
  3. 不支持最左前缀匹配:对于联合索引没办法部分使用
  4. 存在Hash冲突:大量数据时冲突会影响性能

然后说说B+Tree相比B-Tree的优势:

B-Tree的每个节点都存储数据,而B+Tree只在叶子节点存储数据,非叶子节点只存储索引键。这带来几个好处:

  1. 树的高度更低
    • 因为非叶子节点不存数据,每个节点能存储更多的索引键
    • 一个节点通常是一个磁盘页(16KB),能存更多key意味着树更矮
    • 树矮了,磁盘IO次数就少了,查询更快
  2. 范围查询效率高
    • B+Tree的叶子节点之间用双向链表连接
    • 范围查询时,找到起点后沿着链表遍历就行
    • B-Tree做范围查询需要中序遍历,效率低很多
  3. 查询效率稳定
    • B+Tree所有查询都要走到叶子节点,路径长度一致
    • B-Tree可能在中间节点就找到数据,查询效率不稳定

实际举个例子:

假设一个节点能存1000个key,3层的B+Tree能存储1000×1000×1000 = 10亿条数据,而3次磁盘IO就能定位到任意数据。这就是B+Tree的威力。


面试官:什么是最左前缀原则?能举例说明吗?

回答:

好的,最左前缀原则是使用联合索引时必须遵循的规则。

简单来说,对于联合索引,查询条件必须从索引的最左边开始匹配,并且不能跳过中间的列。

我举个具体例子:

假设我们有一个联合索引idx_a_b_c(a, b, c),包含a、b、c三个字段。

-- 能用到索引的情况:WHERE a =1-- 用到索引(a)WHERE a =1AND b =2-- 用到索引(a, b)WHERE a =1AND b =2AND c =3-- 用到索引(a, b, c),完全匹配-- 能部分用到索引的情况:WHERE a =1AND c =3-- 只用到(a),c用不上,因为跳过了b-- 用不到索引的情况:WHERE b =2-- 用不到,没有最左边的aWHERE b =2AND c =3-- 用不到,没有最左边的aWHERE c =3-- 用不到

有一个特殊情况要注意:查询条件的顺序不影响索引使用,因为MySQL优化器会自动调整顺序。

WHERE b =2AND a =1-- 优化器会调整为 a = 1 AND b = 2,能用到索引

还有一个重要的点:范围查询会导致后面的列无法使用索引。

WHERE a =1AND b >5AND c =3-- a走索引,b走索引(范围),但c就用不到索引了

实际工作中的建议:

  1. 把区分度高的字段放在联合索引的前面
  2. 把等值查询的字段放在范围查询字段的前面
  3. 利用覆盖索引避免回表

面试官:哪些情况会导致索引失效?

回答:

索引失效是面试高频题,我总结了几种常见的情况:

1. 违反最左前缀原则

-- 联合索引 idx_a_b_c(a, b, c)SELECT*FROM t WHERE b =1;-- 没有a,索引失效

2. 在索引列上使用函数或运算

-- 索引失效SELECT*FROMuserWHERELEFT(name,3)='张';SELECT*FROMuserWHERE age +1=25;SELECT*FROMuserWHEREYEAR(create_time)=2024;-- 正确写法SELECT*FROMuserWHERE name LIKE'张%';SELECT*FROMuserWHERE age =24;SELECT*FROMuserWHERE create_time >='2024-01-01'AND create_time <'2025-01-01';

3. 隐式类型转换

-- 假设phone是varchar类型SELECT*FROMuserWHERE phone =13800138000;-- 数字,索引失效SELECT*FROMuserWHERE phone ='13800138000';-- 字符串,索引有效

这里的原理是:当类型不匹配时,MySQL会对索引列进行类型转换,相当于加了函数,就失效了。

4. LIKE以%开头

SELECT*FROMuserWHERE name LIKE'%张';-- 索引失效SELECT*FROMuserWHERE name LIKE'%张%';-- 索引失效SELECT*FROMuserWHERE name LIKE'张%';-- 索引有效

5. OR条件中有非索引列

-- 假设name有索引,status没有索引SELECT*FROMuserWHERE name ='张三'ORstatus=1;-- 整体不走索引

要解决这个问题,要么给status也加索引,要么改写成UNION:

SELECT*FROMuserWHERE name ='张三'UNIONSELECT*FROMuserWHEREstatus=1;

6. 使用NOT IN、NOT EXISTS、!=、<>

SELECT*FROMuserWHERE id NOTIN(1,2,3);SELECT*FROMuserWHERE name !='张三';

这些操作可能导致优化器认为全表扫描更快,从而放弃索引。

7. IS NULL / IS NOT NULL(看情况)

如果表中NULL值很多,IS NOT NULL可能走索引;如果NULL值很少,IS NULL可能走索引。这个取决于数据分布和优化器的判断。

总结一句话:索引列要保持"干净",不要对它做任何加工,让它直接参与比较。


三、事务篇

面试官:说说事务的四大特性ACID,以及MySQL是如何实现的?

回答:

ACID是事务的四大特性,我来逐一说明:

A - 原子性(Atomicity)

原子性是指事务是一个不可分割的整体,要么全部成功,要么全部失败。

比如转账操作,A给B转100块:

  1. A的账户减100
  2. B的账户加100

这两步必须同时成功或同时失败,不能出现A扣了钱但B没收到的情况。

MySQL的实现方式:通过undo log(回滚日志) 来实现。在执行事务时,MySQL会把修改前的数据保存到undo log中。如果事务需要回滚,就用undo log中的数据恢复。

C - 一致性(Consistency)

一致性是指事务执行前后,数据库从一个一致状态变到另一个一致状态。比如转账前后,A和B的总金额应该不变。

MySQL的实现方式:一致性是事务的最终目标,是由其他三个特性(原子性、隔离性、持久性)共同保证的。

I - 隔离性(Isolation)

隔离性是指多个事务并发执行时,相互之间不能干扰。一个事务内部的操作对其他并发事务是隔离的。

MySQL的实现方式:通过MVCC(多版本并发控制) 来实现。

  • 写写冲突通过锁来解决
  • 读写冲突通过MVCC来解决

D - 持久性(Durability)

持久性是指事务一旦提交,对数据的修改就是永久的,即使系统崩溃也不会丢失。

MySQL的实现方式:通过redo log(重做日志) 来实现。事务提交时,先把修改写入redo log并刷盘,这样即使系统崩溃,重启后也能通过redo log恢复数据。

补充一下redo log和undo log的区别:

  • redo log:记录的是"物理日志",即数据页的修改,用于崩溃恢复
  • undo log:记录的是"逻辑日志",即相反的操作,用于事务回滚和MVCC

面试官:MySQL的事务隔离级别有哪些?分别能解决什么问题?

回答:

MySQL有四种事务隔离级别,从低到高分别是:

1. 读未提交(READ UNCOMMITTED)

这是最低的隔离级别,一个事务可以读取到另一个事务未提交的数据。

问题:会产生脏读。比如事务A读到了事务B修改但未提交的数据,如果B回滚了,A读到的就是无效的脏数据。

实际开发中基本不用这个级别。

2. 读已提交(READ COMMITTED)

一个事务只能读取到其他事务已提交的数据。

解决了:脏读问题
仍存在:不可重复读。就是在同一个事务中,两次读取同一条数据可能得到不同的结果,因为期间可能有其他事务修改并提交了。

Oracle数据库的默认隔离级别就是这个。

3. 可重复读(REPEATABLE READ)

在同一个事务中多次读取同一数据,结果是一致的。

解决了:脏读、不可重复读
仍存在:幻读。就是在同一事务中,两次查询的结果集行数不同,因为有其他事务插入或删除了数据。

但是,MySQL的InnoDB在这个级别通过Next-Key Lock(临键锁) 在很大程度上解决了幻读问题。

MySQL的默认隔离级别就是REPEATABLE READ。

4. 串行化(SERIALIZABLE)

最高的隔离级别,事务串行执行,完全隔离。

解决了:脏读、不可重复读、幻读
代价:性能最差,并发度最低

我用表格总结一下:

隔离级别脏读不可重复读幻读
READ UNCOMMITTED可能可能可能
READ COMMITTED不会可能可能
REPEATABLE READ不会不会可能(InnoDB基本解决)
SERIALIZABLE不会不会不会

实际工作中的选择:

  • 大多数互联网项目用默认的REPEATABLE READ就够了
  • 对于一些金融类的高一致性要求场景,可能会用到SERIALIZABLE
  • 如果是读多写少、对一致性要求不那么高的场景,可以用READ COMMITTED提高并发性能

面试官:什么是MVCC?它是怎么实现的?

回答:

MVCC,全称Multi-Version Concurrency Control,多版本并发控制。它的核心思想是:通过保存数据的多个版本,让读操作和写操作不冲突,从而提高并发性能。

简单来说,当你读数据的时候,读的是某一个历史版本;当其他事务在写数据的时候,并不影响你的读取。这就是所谓的"快照读"。

MVCC的实现依赖三个核心组件:

1. 隐藏字段

InnoDB会为每行数据添加几个隐藏字段:

  • DB_TRX_ID(6字节):记录最后一次修改该行的事务ID
  • DB_ROLL_PTR(7字节):回滚指针,指向这行数据的上一个版本(在undo log中)
  • DB_ROW_ID(6字节):隐藏的自增主键,如果表没有主键才会有这个

2. Undo Log(版本链)

每次修改数据时,旧版本会被保存到undo log中。多次修改就形成了一个版本链:

当前数据 → undo log版本1 → undo log版本2 → undo log版本3 → ... 

通过DB_ROLL_PTR指针可以找到所有历史版本。

3. Read View(读视图)

当事务执行快照读(普通SELECT)时,会生成一个Read View,它包含:

  • m_ids:当前活跃(未提交)的事务ID列表
  • min_trx_id:m_ids中的最小事务ID
  • max_trx_id:系统应该分配给下一个事务的ID
  • creator_trx_id:创建这个Read View的事务ID

Read View的可见性判断规则:

对于版本链中的某个版本,它的trx_id与Read View比较:

  1. 如果trx_id < min_trx_id,说明这个版本在Read View创建之前就已经提交了,可见
  2. 如果trx_id >= max_trx_id,说明这个版本是在Read View创建之后才生成的,不可见
  3. 如果min_trx_id <= trx_id < max_trx_id:
    • 如果trx_id在m_ids中,说明这个版本的事务还未提交,不可见
    • 如果trx_id不在m_ids中,说明这个版本的事务已经提交,可见

不同隔离级别下Read View的生成时机不同:

  • READ COMMITTED:每次SELECT都会生成新的Read View
  • REPEATABLE READ:只在第一次SELECT时生成Read View,后续复用

这就是为什么RC级别会有不可重复读,而RR级别可以保证可重复读的原因。


四、锁篇

面试官:MySQL中有哪些类型的锁?

回答:

MySQL的锁可以从多个维度来分类,我来详细说一下:

一、按锁的粒度分

1. 全局锁
锁住整个数据库实例,使其处于只读状态。主要用于全库逻辑备份。

FLUSH TABLESWITHREADLOCK;-- 加锁UNLOCKTABLES;-- 解锁

2. 表级锁
锁住整张表,开销小、加锁快,但并发度低。包括:

  • 表锁:LOCK TABLES t READ/WRITE
  • 元数据锁(MDL):访问表时自动加,防止DDL和DML冲突
  • 意向锁:表级别的锁,用于快速判断表中是否有行锁

3. 行级锁
只锁住需要的行,开销大、加锁慢,但并发度高。InnoDB支持行级锁。

二、按锁的模式分

1. 共享锁(S锁/读锁)

SELECT...LOCKINSHAREMODE;-- MySQL 5.xSELECT...FORSHARE;-- MySQL 8.0+

多个事务可以同时持有S锁,用于读取数据。

2. 排他锁(X锁/写锁)

SELECT...FORUPDATE;

只有一个事务能持有X锁,用于修改数据。INSERT/UPDATE/DELETE会自动加X锁。

三、InnoDB的行锁类型(重点)

1. Record Lock(记录锁)
锁住索引中的一条记录。

2. Gap Lock(间隙锁)
锁住索引记录之间的间隙,防止其他事务在间隙中插入数据。这是为了解决幻读问题。

比如表中有id为1、5、10的记录,间隙锁可以锁住(1,5)、(5,10)这些区间。

3. Next-Key Lock(临键锁)
Record Lock + Gap Lock的组合,锁住一条记录以及它前面的间隙。

这是InnoDB在REPEATABLE READ级别下默认的行锁算法。

举个例子:
假设表中有id为1、5、10的记录,执行:

SELECT*FROM t WHERE id =5FORUPDATE;

在RR级别下,会加Next-Key Lock,锁住(1,5]这个范围。


面试官:什么是死锁?怎么避免和解决?

回答:

什么是死锁?

死锁是指两个或多个事务在执行过程中,因互相持有对方需要的锁而造成的一种阻塞现象。如果没有外力介入,这些事务都无法继续执行。

举个经典的例子:

-- 事务ASTARTTRANSACTION;UPDATE account SET balance = balance -100WHERE id =1;-- 获得id=1的X锁-- 此时事务A持有id=1的锁,等待id=2的锁-- 事务BSTARTTRANSACTION;UPDATE account SET balance = balance -100WHERE id =2;-- 获得id=2的X锁UPDATE account SET balance = balance +100WHERE id =1;-- 等待id=1的锁-- 事务A继续UPDATE account SET balance = balance +100WHERE id =2;-- 等待id=2的锁-- 死锁产生!A等B释放id=2,B等A释放id=1

如何检测和处理死锁?

InnoDB有两种策略:

1. 等待超时
参数innodb_lock_wait_timeout,默认50秒。超时后事务会回滚。
缺点:等待时间长,业务响应慢。

2. 死锁检测(推荐)
参数innodb_deadlock_detect=ON(默认开启)。
InnoDB会主动检测死锁,发现后立即回滚其中一个代价较小的事务。

如何避免死锁?

1. 按固定顺序访问资源
所有业务代码都按照相同的顺序获取锁。比如都先锁id小的,再锁id大的。

2. 减小锁的粒度和持有时间

  • 尽量使用行级锁而不是表级锁
  • 事务中的SQL尽量少,尽快提交
  • 避免在事务中进行耗时操作(如调用外部接口)

3. 使用合理的索引
如果查询没有走索引,InnoDB会进行全表扫描并锁住所有行,更容易死锁。

4. 降低隔离级别
如果业务允许,使用READ COMMITTED级别,没有Gap Lock,死锁概率降低。

5. 使用乐观锁代替悲观锁

-- 乐观锁方式UPDATE product SET stock = stock -1, version = version +1WHERE id =1AND version =10;

如何排查死锁?

-- 查看最近一次死锁信息SHOWENGINEINNODBSTATUS;-- 查看当前锁等待SELECT*FROM information_schema.INNODB_LOCKS;SELECT*FROM information_schema.INNODB_LOCK_WAITS;

面试官:说说乐观锁和悲观锁的区别,以及各自的使用场景

回答:

悲观锁

悲观锁的思想是:每次操作数据时都认为会有并发冲突,所以先加锁再操作。

实现方式:数据库的锁机制,如SELECT FOR UPDATE

STARTTRANSACTION;-- 先加排他锁SELECT stock FROM product WHERE id =1FORUPDATE;-- 业务逻辑处理UPDATE product SET stock = stock -1WHERE id =1;COMMIT;

特点:

  • 数据安全性高
  • 性能开销大(锁等待、可能死锁)
  • 适合写多读少的场景

乐观锁

乐观锁的思想是:假设冲突很少发生,不加锁,而是在更新时检查数据是否被修改过。

实现方式:通常用版本号或时间戳

-- 1. 先查询数据和版本号SELECT id, stock, version FROM product WHERE id =1;-- 假设返回 stock=100, version=1-- 2. 更新时检查版本号UPDATE product SET stock = stock -1, version = version +1WHERE id =1AND version =1;-- 3. 检查受影响行数-- 如果是0,说明数据被其他事务修改过,需要重试

特点:

  • 性能好(无锁开销)
  • 可能需要重试机制
  • 适合读多写少的场景

使用场景对比:

场景推荐使用原因
电商秒杀/库存扣减乐观锁读多写少,用户量大
银行转账悲观锁资金安全优先,写操作频繁
文章点赞/阅读数乐观锁允许少量误差,并发量大
订单状态流转悲观锁/分布式锁状态一致性要求高

CAS(Compare And Swap):

乐观锁的本质其实就是CAS思想:比较并交换。在Java中,Atomic类就是用CAS实现的。在数据库中,我们用version字段来模拟CAS操作。


五、SQL优化篇

面试官:如何定位和优化慢SQL?

回答:

这是一个很实际的问题,我按照实际工作中的流程来说:

第一步:开启慢查询日志,定位慢SQL

-- 查看慢查询日志是否开启SHOW VARIABLES LIKE'slow_query_log';-- 开启慢查询日志SETGLOBAL slow_query_log =ON;-- 设置慢查询阈值,比如2秒SETGLOBAL long_query_time =2;-- 查看慢查询日志文件位置SHOW VARIABLES LIKE'slow_query_log_file';

在生产环境中,也可以通过监控系统(如Prometheus+Grafana)来发现慢SQL。

第二步:使用EXPLAIN分析执行计划

EXPLAINSELECT*FROMuserWHERE username ='zhangsan';

重点关注这几个字段:

  • type:访问类型,从好到差:system > const > eq_ref > ref > range > index > ALL
    • const:通过主键或唯一索引查询,最多一行
    • ref:使用非唯一索引
    • range:索引范围扫描
    • index:全索引扫描
    • ALL:全表扫描(最差,要优化)
  • key:实际使用的索引,NULL表示没用索引
  • rows:预估扫描的行数,越少越好
  • Extra:额外信息
    • Using index:覆盖索引,好
    • Using filesort:需要额外排序,需要优化
    • Using temporary:使用临时表,需要优化

第三步:针对性优化

1. 添加合适的索引

-- 为WHERE条件字段添加索引CREATEINDEX idx_username ONuser(username);-- 为排序字段添加索引CREATEINDEX idx_create_time ONuser(create_time);-- 联合索引CREATEINDEX idx_status_create_time ONorder(status, create_time);

2. 优化SQL写法

-- 避免SELECT *SELECT id, username, email FROMuserWHERE id =1;-- 避免函数操作索引列-- 不好SELECT*FROMuserWHEREYEAR(create_time)=2024;-- 好SELECT*FROMuserWHERE create_time >='2024-01-01'AND create_time <'2025-01-01';-- 避免隐式类型转换-- phone是varchar类型-- 不好SELECT*FROMuserWHERE phone =13800138000;-- 好SELECT*FROMuserWHERE phone ='13800138000';

3. 分页优化

-- 深分页问题SELECT*FROMorderLIMIT1000000,10;-- 需要扫描100万+10行-- 优化方案:延迟关联SELECT*FROMorderWHERE id >=(SELECT id FROMorderORDERBY id LIMIT1000000,1)LIMIT10;-- 或者:记录上次查询的最后一个IDSELECT*FROMorderWHERE id >1000000ORDERBY id LIMIT10;

4. 批量操作

-- 不好:循环单条插入INSERTINTOuserVALUES(1,'a');INSERTINTOuserVALUES(2,'b');-- 好:批量插入INSERTINTOuserVALUES(1,'a'),(2,'b'),(3,'c');-- 大批量数据建议分批,每批500-1000条

5. 合理使用JOIN

-- 小表驱动大表-- 如果department表小,用INSELECT*FROM employee WHERE dept_id IN(SELECT id FROM department);-- 如果employee表小,用EXISTSSELECT*FROM employee e WHEREEXISTS(SELECT1FROM department d WHERE d.id = e.dept_id);-- JOIN时确保关联字段有索引-- 控制JOIN的表数量,一般不超过3张

面试官:你在实际项目中做过哪些SQL优化?能举个具体的例子吗?

回答:

好的,我举一个之前处理过的真实案例。

背景:
有一个订单列表查询接口,在数据量达到500万后,查询变得很慢,平均响应时间超过5秒。

原始SQL:

SELECT*FROMorderWHERE user_id =12345ANDstatusIN(1,2,3)AND create_time >='2024-01-01'ORDERBY create_time DESCLIMIT0,20;

排查过程:

  1. 首先用EXPLAIN分析,发现type是ALL,全表扫描,没走索引
  2. 检查发现表上只有主键索引,没有针对查询条件的索引

优化方案:

第一步:添加联合索引

CREATEINDEX idx_user_status_time ONorder(user_id,status, create_time);

添加后,type变成了range,rows从500万降到了几千。

**第二步:优化SELECT ***

-- 改成只查需要的字段SELECT id, order_no,status, amount, create_time FROMorderWHERE...

第三步:考虑覆盖索引

因为查询的字段比较多,创建覆盖索引不太现实。但对于一些只需要少量字段的查询,可以考虑。

第四步:分页优化

原来的分页到后面页数时会很慢:

-- 原来的,第1000页时要扫描20000行SELECT...LIMIT19980,20;-- 优化后,使用游标分页SELECT...WHERE id < 上一页最后一条的id ORDERBY id DESCLIMIT20;

优化效果:

  • 查询时间从5秒+降到50毫秒以内
  • 支撑了日均百万级的查询量

总结几个要点:

  1. 一定要有合适的索引
  2. 联合索引要考虑查询条件的顺序和区分度
  3. 避免SELECT *
  4. 深分页要特别处理
  5. 定期EXPLAIN分析关键SQL

六、其他高频问题

面试官:MySQL主从复制的原理是什么?

回答:

MySQL主从复制是通过binlog来实现的,整个过程可以分为三个步骤:

第一步:Master记录binlog
主库执行写操作时(INSERT/UPDATE/DELETE),会把变更记录到binlog(二进制日志)中。

第二步:从库IO线程读取binlog
从库有一个IO线程,它会连接到主库,读取主库的binlog,然后写入到从库本地的relay log(中继日志)中。

第三步:从库SQL线程执行relay log
从库还有一个SQL线程,它会读取relay log中的事件,在从库上重新执行一遍,从而实现数据同步。

整个流程:

Master写数据 → 写入binlog → 从库IO线程读取 → 写入relay log → 从库SQL线程执行 → 数据同步 

binlog的三种格式:

  1. STATEMENT:记录SQL语句本身
    • 优点:日志量小
    • 缺点:某些函数(如NOW()、UUID())可能导致主从数据不一致
  2. ROW:记录每行数据的变化
    • 优点:不会出现数据不一致
    • 缺点:日志量大
  3. MIXED:混合模式,MySQL自动选择
    • 一般语句用STATEMENT,特殊情况用ROW

推荐使用ROW格式,虽然日志量大,但数据一致性有保证。

主从延迟的原因和解决方案:

原因:

  • 主库并发写入,从库单线程重放
  • 网络延迟
  • 从库机器性能差
  • 大事务

解决方案:

  • 使用并行复制(MySQL 5.6+)
  • 从库配置更好的硬件
  • 避免大事务
  • 读写分离时,对实时性要求高的读走主库

面试官:如何保证MySQL和Redis缓存的数据一致性?

回答:

这是一个经典问题,我来说说常见的几种策略:

策略一:Cache Aside Pattern(旁路缓存模式)

这是最常用的策略:

  • 读:先读缓存,缓存没有再读数据库,然后写入缓存
  • 写:先更新数据库,再删除缓存
// 读取publicUsergetUser(Long id){User user = redis.get("user:"+ id);if(user ==null){ user = db.getUser(id); redis.set("user:"+ id, user);}return user;}// 写入publicvoidupdateUser(User user){ db.update(user); redis.delete("user:"+ user.getId());}

为什么是删除缓存而不是更新缓存?

  • 更新缓存可能有并发问题:A先更新DB,B后更新DB,但B先更新缓存,A后更新缓存,导致缓存是旧数据
  • 删除更简单,让下次读取时重新加载

为什么是先更新DB再删缓存,而不是先删缓存再更新DB?

  • 先删缓存的问题:删除缓存后、更新DB前,另一个请求读到旧数据并写入缓存,导致脏数据

策略一的问题:
即使先更新DB再删缓存,极端情况下仍可能不一致:

  1. 缓存刚好失效
  2. 请求A读DB(旧值)
  3. 请求B更新DB(新值)
  4. 请求B删除缓存
  5. 请求A把旧值写入缓存

不过这种情况概率很低,因为写操作通常比读操作慢。

策略二:延迟双删

为了解决上面的极端情况:

publicvoidupdateUser(User user){ redis.delete("user:"+ user.getId());// 第一次删除 db.update(user);Thread.sleep(500);// 等待一段时间 redis.delete("user:"+ user.getId());// 第二次删除}

延迟时间要大于一次读操作的时间。

策略三:异步更新缓存(基于消息队列或binlog)

写操作 → 更新DB → 发送消息/监听binlog → 异步更新/删除缓存 

使用Canal监听binlog的方式更可靠,缺点是有一定延迟。

总结:

  • 一般场景:Cache Aside Pattern + 设置缓存过期时间
  • 要求较高:延迟双删
  • 强一致性要求:分布式事务或不用缓存

面试官:最后一个问题,你觉得一个合格的索引应该怎么设计?

回答:

这个问题很好,我总结一下索引设计的核心原则:

1. 选择合适的字段建索引

  • 高频查询条件字段:WHERE后面经常出现的字段
  • 排序字段:ORDER BY的字段
  • 连接字段:JOIN ON的字段
  • 高区分度字段:值的种类多,重复少(如用户ID),而不是像性别这种

2. 联合索引的设计原则

  • 最左前缀:把最常用的查询条件放在最左边
  • 区分度高的在前:区分度高的字段放前面,可以更快过滤数据
  • 范围查询放最后:范围查询会使后面的字段无法使用索引
  • 考虑覆盖索引:尽量让查询的字段都在索引中
-- 假设查询条件是:WHERE status = 1 AND type = 2 AND create_time > '2024-01-01'-- 好的设计:idx_status_type_time(status, type, create_time)-- 等值查询在前,范围查询在后

3. 避免过多索引

  • 索引不是越多越好,会影响写入性能
  • 一般一张表的索引控制在5个以内
  • 定期清理无用索引

4. 主键索引的设计

  • 推荐使用自增ID,顺序插入效率高
  • 分布式场景用雪花算法,也是趋势递增的
  • 避免使用UUID或随机字符串,会导致页分裂

5. 避免冗余索引

-- 冗余INDEX idx_a (a)INDEX idx_a_b (a, b)-- idx_a是冗余的,idx_a_b可以覆盖它-- 不冗余INDEX idx_a_b (a, b)INDEX idx_b_a (b, a)-- 顺序不同,不冗余

6. 使用前缀索引压缩长字段

-- 对于很长的字符串字段CREATEINDEX idx_email ONuser(email(10));-- 只索引前10个字符

7. 实践建议

  • 先有业务查询,再设计索引(不要凭空设计)
  • 定期用EXPLAIN检查关键SQL
  • 用慢查询日志发现问题
  • 数据量小的表不需要太多索引

Read more

Flutter 三方库 appium_driver 分布式泛鸿蒙场景下协同适配研讨:推进开放设备移动控制终端指令执行自动化体系及构筑强容错弹性高可用运维集成底座-适配鸿蒙 HarmonyOS ohos

Flutter 三方库 appium_driver 分布式泛鸿蒙场景下协同适配研讨:推进开放设备移动控制终端指令执行自动化体系及构筑强容错弹性高可用运维集成底座-适配鸿蒙 HarmonyOS ohos

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 appium_driver 分布式泛鸿蒙场景下协同适配研讨:推进开放设备移动控制终端指令执行自动化体系及构筑强容错弹性高可用运维集成底座 在鸿蒙应用进入大规模商业化部署的阶段,如何确保应用在各种型号的鸿蒙设备上表现一致?如何实现高效的回归测试?appium_driver 是一个强大的自动化测试驱动库,它让我们可以使用 Dart 语言编写跨平台的 UI 自动化测试脚本。本文将详解该库在 OpenHarmony 上的适配要点。 前言 什么是 appium_driver?它是基于 W3C WebDriver 协议的封装,专门用于与 Appium 服务器通信。通过它,我们可以像操作浏览器一样,自动执行点击、滑动、输入文本、截图验证等操作。在鸿蒙操作系统推出的 DevEco Testing 自动化测试生态中,利用该库可以补充 Dart 生态下的自动化测试拼图。 一、

By Ne0inhk
【抽奖系统开发实战】Spring Boot 项目的用户模块设计:注册登录、权限管控与敏感数据加密

【抽奖系统开发实战】Spring Boot 项目的用户模块设计:注册登录、权限管控与敏感数据加密

文章目录 * 一、注册 * 1.1 敏感字段加密 * 1.2 用户注册 * 1.3 TypeHandler * 二、控制层通用异常处理 * 三、登录 * 3.1 发送验证码 * 3.2 Redis的配置与使用 * > 核心工具类`RedisUtil` * 3.3 JWT * > JWT 令牌介绍 * > 核心工具类`JWTUtil` * 3.4 管理员登录 * 四、强制登录 * 4.1 前端处理 * 4.2 后端处理 * 五、用户管理 * 5.1 后台管理页面

By Ne0inhk
Flutter 三方库 stream_channel 的鸿蒙化适配指南 - 实现具备跨端通讯抽象与协议分层治理的流通道架构、支持端侧多维异步指令流管道化实战

Flutter 三方库 stream_channel 的鸿蒙化适配指南 - 实现具备跨端通讯抽象与协议分层治理的流通道架构、支持端侧多维异步指令流管道化实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 stream_channel 的鸿蒙化适配指南 - 实现具备跨端通讯抽象与协议分层治理的流通道架构、支持端侧多维异步指令流管道化实战 前言 在进行 Flutter for OpenHarmony 的复杂通讯系统(如实现自定义的二进制协议、跨进程 IPC 或与嵌入式设备进行长连接)开发时,如何将原始的、读写分离的 IO 映射为统一、双工的指令流?stream_channel 是一款专注于流通讯抽象的核心库。它将一个 Stream(入站)和一个 StreamSink(出站)封装为单一、可组合的对象。本文将探讨如何在鸿蒙端构建极致、清亮的流通讯底座。 一、原直观解析 / 概念介绍 1.1 基础原理 该库建立在“双工通道(

By Ne0inhk
【spring02】Spring 管理 Bean-IOC,基于 XML 配置 bean

【spring02】Spring 管理 Bean-IOC,基于 XML 配置 bean

文章目录🌍一. bean 创建顺序🌍二. bean 对象的单例和多例❄️1. 机制❄️2. 使用细节🌍三. bean 的生命周期🌍四. 配置 bean 的后置处理器 【这个比较难】🌍五. 通过属性文件给 bean 注入值🌍六. 基于 XML 的 bean 的自动装配🌍七. spring el 表达式[知道即可] 🙋‍♂️ 作者:@whisperrr.🙋‍♂️ 👀 专栏:spring👀 💥 标题:【spring01】Spring 管理 Bean-IOC,基于 XML 配置 bean💥 ❣️ 寄语:比较是偷走幸福的小偷❣️ 🌍一.

By Ne0inhk