一、索引原理与优化
1. 为什么InnoDB选择B+树作为索引结构?对比B树、哈希、红黑树
- B+树 vs B树:
- B+树非叶子节点不存数据,仅存索引键,能容纳更多索引项,树高更低(减少磁盘I/O);
- B+树叶子节点通过双向链表连接,范围查询性能远优于B树(B树需中序遍历)。
- vs 哈希索引:
- 哈希索引仅支持等值查询,无法范围查询、排序;
- 哈希冲突时性能退化,且不支持最左前缀匹配。
- vs 红黑树:
- 红黑树是内存数据结构,树高随数据量增长快(磁盘I/O次数多);
- B+树利用磁盘预读特性(页大小通常16KB),一次I/O读取更多数据。
2. 聚簇索引 vs 非聚簇索引?InnoDB与MyISAM的区别
- 聚簇索引(InnoDB):
- 索引和数据存储在一起,叶子节点即数据行(表数据按聚簇索引顺序组织);
- 一张表只能有一个聚簇索引(默认主键,无主键则选唯一非空索引,再无则隐式生成rowid);
- 优点:通过聚簇索引查询直接获取数据,性能高;范围查询高效。
- 非聚簇索引(MyISAM):
- 索引和数据分离,叶子节点存数据指针(需回表查询);
- 一张表可多个非聚簇索引;
- InnoDB的二级索引也是非聚簇索引,叶子节点存主键值(回表需再查聚簇索引)。
3. 最左前缀原则?联合索引底层存储结构
- 最左前缀原则:
- 联合索引(a,b,c)按a→b→c顺序排序,查询时必须从最左列开始匹配,且不能跳过中间列;
- 例如:
where a=1 and c=3仅用a列,where b=2完全失效。
- 底层存储:
- B+树叶子节点按联合索引列顺序存储,先按a排序,a相同再按b排序,以此类推;
- 数据行物理存储按聚簇索引顺序,与联合索引无关。
4. 索引失效的常见场景
- 违反最左前缀原则;
- 使用函数/表达式/类型转换(如
where age+1=10、where id='123'); - 模糊查询
%在前(如like '%abc'); - OR条件中存在未索引列;
- 数据分布不均(如性别字段,索引区分度低,优化器选择全表扫描);
- 使用NOT IN、NOT EXISTS(部分场景)。
5. 覆盖索引?如何避免回表?
- 覆盖索引:查询的列完全包含在索引中(无需回表查聚簇索引);
- 例如:联合索引(a,b),查询
select a,b from t where a=1,直接从二级索引获取数据。
- 例如:联合索引(a,b),查询
- 避免回表的方法:
- 设计联合索引,将查询列包含在索引中;
- 若需查询大量列,优先考虑聚簇索引(如主键查询)。
二、事务与隔离级别
6. 事务ACID特性及InnoDB实现
- 原子性(Atomicity):
- 事务内操作要么全成功,要么全失败;
- 实现:undo log(记录数据修改前的版本,回滚时恢复)。
- 一致性(Consistency):
- 事务前后数据从一个合法状态到另一个合法状态;
- 实现:原子性+隔离性+持久性+业务逻辑保证。
- 隔离性(Isolation):
- 并发事务间互不干扰;
- 实现:锁机制+MVCC(多版本并发控制)。
- 持久性(Durability):
- 事务提交后数据永久丢失;
- 实现:redo log(先写日志再刷盘,保证崩溃恢复)。
7. 四种隔离级别及解决的问题
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式 |
|---|---|---|---|---|
| 读未提交(Read Uncommitted) | ❌ | ❌ | ❌ | 无特殊机制 |
| 读已提交(Read Committed) | ✅ | ❌ | ❌ | MVCC(语句级Read View) |
| 可重复读(Repeatable Read,默认) | ✅ | ✅ | ❌(部分解决) | MVCC(事务级Read View)+ Next-Key Lock |
| 串行化(Serializable) | ✅ | ✅ | ✅ | 全表锁/范围锁 |
- 脏读:事务A读取到事务B未提交的数据;
- 不可重复读:同一事务内,两次读取同一行数据结果不同(因其他事务修改并提交);
- 幻读:同一事务内,两次范围查询结果行数不同(因其他事务插入/删除并提交)。
8. InnoDB如何解决幻读?
- MVCC(快照读):
- 普通SELECT(不加锁)使用快照读,通过Read View保证事务内看到的数据版本一致;
- RC隔离级别:每次SELECT生成新的Read View;RR隔离级别:事务第一次SELECT生成Read View,后续复用。
- Next-Key Lock(当前读):
- 加锁的SELECT(
for update、lock in share mode)、INSERT、UPDATE、DELETE使用当前读; - Next-Key Lock = 记录锁(行锁)+ 间隙锁(锁定索引间隙),防止其他事务在范围内插入数据;
- 例如:
select * from t where id between 10 and 20 for update,锁定(10,20]的记录及间隙,避免插入id=15的行。
- 加锁的SELECT(
9. MVCC实现原理(隐藏字段、undo log、Read View)
- 隐藏字段:
DB_TRX_ID:最后修改该行的事务ID;DB_ROLL_PTR:指向undo log中该行的上一个版本;DB_ROW_ID:隐式主键(无主键时生成)。
- undo log(版本链):
- 每次修改数据前,将旧版本写入undo log,通过
DB_ROLL_PTR形成链表; - 链表头是最新版本,链表尾是最旧版本。
- 每次修改数据前,将旧版本写入undo log,通过
- Read View(可见性判断):
- 包含字段:
m_ids(当前活跃事务ID列表)、min_trx_id(最小活跃ID)、max_trx_id(下一个要分配的ID)、creator_trx_id(创建Read View的事务ID); - 可见性规则:
- 若
DB_TRX_ID == creator_trx_id,可见(自己修改的); - 若
DB_TRX_ID < min_trx_id,可见(已提交的旧事务); - 若
DB_TRX_ID >= max_trx_id,不可见(新事务); - 若
DB_TRX_ID在m_ids中,不可见(活跃未提交);否则可见(已提交)。
- 若
- 包含字段:
三、锁机制
10. InnoDB锁类型
- 按粒度分:
- 表锁:开销小,并发低(如
alter table); - 行锁:开销大,并发高(InnoDB默认)。
- 表锁:开销小,并发低(如
- 按类型分:
- 共享锁(S锁):读锁,多个事务可同时持有;
- 排他锁(X锁):写锁,仅一个事务持有;
- 意向锁(IS/IX):表级锁,表明事务即将对表内行加S/X锁(避免表锁与行锁冲突)。
- 按算法分:
- 记录锁(Record Lock):锁定单行索引记录;
- 间隙锁(Gap Lock):锁定索引间隙(不包含记录本身);
- Next-Key Lock:记录锁+间隙锁(锁定左开右闭区间)。
11. 死锁产生条件、检测与避免
- 产生条件(缺一不可):
- 互斥:锁只能被一个事务持有;
- 请求与保持:持有锁的同时请求新锁;
- 不剥夺:锁只能由持有者释放,不能被强制剥夺;
- 循环等待:事务间形成锁等待环。
- InnoDB死锁检测:
- 等待图(Wait-for Graph):检测是否有环,有环则选择回滚代价小的事务(如修改行数少的)。
- 避免方法:
- 统一加锁顺序(如先锁表A再锁表B);
- 减少事务大小,缩短锁持有时间;
- 避免大事务,拆分为小事务;
- 合理设计索引,减少锁范围(如避免全表扫描);
- 降低隔离级别(如从RR降到RC,减少间隙锁)。
12. 乐观锁 vs 悲观锁
- 悲观锁:
- 假设并发冲突高,先加锁再操作;
- 实现:
select ... for update(排他锁)、lock in share mode(共享锁); - 适用:写多读少场景。
- 乐观锁:
- 假设并发冲突低,操作时不加锁,提交时检查版本;
- 实现:版本号(如
update t set ..., version=version+1 where id=1 and version=old_version)、CAS; - 适用:读多写少场景。
四、日志与崩溃恢复
13. redo log、undo log、binlog的区别
| 特性 | redo log | undo log | binlog |
|---|---|---|---|
| 作用 | 保证持久性(崩溃恢复) | 保证原子性(回滚)+ MVCC | 数据备份、主从复制 |
| 存储引擎 | InnoDB独有 | InnoDB独有 | Server层(所有引擎) |
| 内容 | 物理日志(“在某个数据页上做了什么修改”) | 逻辑日志(“把某行数据恢复到修改前的状态”) | 逻辑日志(SQL语句或行数据变更) |
| 写入时机 | 事务执行中先写redo log buffer,再刷盘 | 事务执行中修改数据前写入 | 事务提交时写入 |
| 大小 | 固定大小(循环写入) | 不固定(随事务增长) | 不固定(可配置滚动) |
14. 两阶段提交(2PC)过程
- 目的:保证redo log和binlog的一致性(崩溃恢复时数据不丢失、不重复)。
- 过程:
- 准备阶段:InnoDB将redo log写入磁盘,状态设为“prepare”;
- 提交阶段:
- Server层将binlog写入磁盘;
- InnoDB将redo log状态改为“commit”,事务完成。
- 崩溃恢复:
- 若redo log是commit状态:直接提交;
- 若redo log是prepare状态:检查binlog是否完整,完整则提交,否则回滚。
15. binlog三种格式对比
- STATEMENT:
- 记录SQL语句(如
update t set age=age+1 where id=1); - 优点:日志量小;缺点:可能导致主从不一致(如
now()、limit无order by)。
- 记录SQL语句(如
- ROW:
- 记录行数据变更(如“id=1的行,age从10改为11”);
- 优点:主从一致;缺点:日志量大(如批量update)。
- MIXED:
- 混合模式,默认用STATEMENT,特殊场景自动切换为ROW(如涉及函数、UUID等)。
五、SQL优化与执行计划
16. EXPLAIN关键字段解读
| 字段 | 含义 | 重点关注值 |
|---|---|---|
| id | 查询序列号 | id相同:从上到下执行;id不同:id大的先执行 |
| select_type | 查询类型 | SIMPLE(简单查询)、PRIMARY(主查询)、SUBQUERY(子查询)、DERIVED(派生表)、UNION(联合查询) |
| table | 操作的表 | - |
| type | 访问类型(性能从好到差) | system > const > eq_ref > ref > range > index > ALL (至少达到range,最好ref) |
| possible_keys | 可能用到的索引 | - |
| key | 实际用到的索引 | 若为NULL,未用索引 |
| key_len | 索引使用的长度 | 越短越好(联合索引中可判断用到了几列) |
| ref | 与索引比较的列 | const(常量)、字段名 |
| rows | 预计扫描的行数 | 越少越好 |
| Extra | 额外信息 | Using index(覆盖索引,好)、Using where(需过滤)、Using temporary(临时表,差)、Using filesort(文件排序,差) |
17. 常见SQL优化技巧
- 索引优化:
- 区分度低的字段(如性别)不建索引;
- 联合索引按区分度从高到低排序;
- 避免索引失效场景(见前文)。
- 查询语句优化:
- 避免
SELECT *,只查需要的列; - 用
JOIN代替子查询(子查询可能产生临时表); LIMIT大偏移量优化:- 延迟关联:
select t.* from t join (select id from t limit 10000,10) as tmp on t.id=tmp.id; - 记录上次查询位置:
select * from t where id > last_id limit 10。
- 延迟关联:
- 避免
- 表结构优化:
- 选择合适的数据类型(如用
INT存IP,用TINYINT存状态); - 避免NULL(NULL值占用额外空间,索引统计复杂);
- 适当反范式(如冗余字段减少JOIN)。
- 选择合适的数据类型(如用
六、架构与高可用
18. 主从复制原理与流程
- 原理:
- 主库将数据变更写入binlog;
- 从库I/O线程读取主库binlog,写入中继日志(relay log);
- 从库SQL线程重放中继日志,将变更应用到从库。
- 流程:
- 主库执行事务,写入binlog;
- 主库dump线程将binlog推送给从库I/O线程;
- 从库I/O线程将binlog写入relay log;
- 从库SQL线程读取relay log,执行变更,数据同步完成。
19. 主从延迟原因与解决方案
- 原因:
- 主库并发高,binlog写入量大,从库SQL线程单线程重放慢;
- 从库硬件配置差(CPU、内存、磁盘I/O);
- 网络延迟;
- 大事务(如批量update、delete)。
- 解决方案:
- 从库升级硬件(优先SSD);
- 开启从库多线程复制(MySQL 5.7+支持基于组提交的并行复制);
- 拆分大事务为小事务;
- 读写分离,减少主库压力;
- 采用半同步复制(保证至少一个从库收到binlog)。
20. 分库分表策略
- 垂直拆分:
- 按业务模块拆分表(如用户表、订单表拆到不同库);
- 按列拆分表(如大字段拆到独立表);
- 优点:拆分规则简单;缺点:跨库JOIN困难。
- 水平拆分:
- 按行拆分表(如按用户ID哈希、按时间范围);
- 优点:单表数据量可控;缺点:拆分规则复杂,数据迁移困难。
- 常见中间件:
- ShardingSphere(Apache顶级项目,支持分库分表、读写分离、分布式事务);
- MyCat(国产中间件,功能丰富,社区活跃)。
七、InnoDB存储引擎核心特性
21. Buffer Pool(缓冲池)
- 作用:
- 缓存磁盘数据页(减少磁盘I/O),包含数据页、索引页、undo页、change buffer等;
- 大小通常设为物理内存的50%-80%(
innodb_buffer_pool_size)。
- LRU算法优化:
- 传统LRU:最近最少使用的页淘汰;
- InnoDB优化:将LRU链表分为young区(热数据)和old区(冷数据);
- 新页先插入old区,若在old区停留时间超过
innodb_old_blocks_time(默认1s),才移到young区; - 避免全表扫描时热数据被淘汰。
22. Change Buffer(变更缓冲区)
- 作用:
- 缓存二级索引的DML操作(INSERT、UPDATE、DELETE),减少磁盘I/O;
- 当查询该索引页时,将change buffer中的变更合并到数据页(merge)。
- 适用场景:
- 二级索引(聚簇索引不适用);
- 索引区分度低(如状态字段),DML频繁且查询少。
- 限制:
- 唯一索引不适用(需检查唯一性,必须读磁盘)。
23. Double Write Buffer(双写缓冲区)
- 作用:
- 解决部分写失效(partial page write)问题:若数据库崩溃时,数据页只写了一半到磁盘,redo log无法恢复(redo log是物理日志,基于完整页);
- 流程:
- 先将数据页写入double write buffer(内存),再刷到共享表空间(磁盘);
- 成功后,再将数据页刷到各自的表空间;
- 若崩溃,从double write buffer恢复完整页,再用redo log恢复。
以上内容覆盖中高级开发工程师需掌握的MySQL核心知识点,建议结合实际项目经验深入理解,并通过LeetCode数据库题目、慢查询优化实战巩固。

1763

被折叠的 条评论
为什么被折叠?



