在 MySQL 数据库的并发场景中,死锁、事务隔离、锁机制等问题一直是开发和运维人员面临的核心挑战。本文将从事务并发问题出发,逐步深入数据库隔离级别、InnoDB 锁机制、死锁原理,最后详解 MVCC 多版本并发控制,帮助大家全面掌握 MySQL 并发控制的核心知识。
一、事务并发产生的问题
当多个事务同时操作数据库时,若缺乏有效的并发控制,会引发以下四类问题,其中前三者针对数据行,幻读针对数据表。
1. 脏写
定义 :一个事务修改了另一个未提交事务修改过的数据,随后未提交的事务回滚,导致前者的修改丢失。示例 :数据库中某行数据初始值为null。事务 A 将其修改为A(未提交),事务 B 紧接着将其修改为B(未提交)。此时事务 A 发生回滚,数据恢复为null,事务 B 的修改被覆盖丢失。
2. 脏读
定义 :一个事务读取了另一个未提交事务修改过的数据,随后该未提交事务回滚,导致读取到的数据无效。示例 :事务 A 将数据null修改为A(未提交),事务 B 查询到该数据为A。之后事务 A 回滚,数据恢复为null,事务 B 再次查询时得到null,前后不一致导致业务逻辑异常。
3. 不可重复读
定义 :同一事务内多次读取同一主键的数据,因其他事务提交修改,导致每次读取的字段值不一致。示例 :事务 A 首次查询数据为null,事务 B 修改该数据为B并提交,事务 A 再次查询得到B;随后事务 C 修改该数据为C并提交,事务 A 第三次查询得到C,三次结果不同。
4. 幻读
定义 :同一事务内多次查询符合相同条件的数据,因其他事务插入 / 删除数据,导致每次查询的结果条数不一致。示例 :事务 A 查询human表有 10 条数据,事务 B 向该表插入 3 条数据并提交,事务 A 再次查询时得到 13 条数据,出现 "幻影" 数据。
小结:脏写、脏读、不可重复读针对数据行,幻读针对数据表。
二、数据库的隔离级别
为解决事务并发问题,数据库定义了四种隔离级别,不同级别对并发问题的解决程度不同。
1. 读未提交(Read Uncommitted)
- 特点:所有事务可查看其他未提交事务的执行结果。
- 并发问题:脏读、脏写、不可重复读、幻读均会出现。
- 实际应用:几乎不用,安全性极低。
2. 读已提交(Read Committed)
- 特点:事务只能查看已提交事务的修改结果(多数数据库默认隔离级别,非 MySQL)。
- 并发问题:避免脏读、脏写,仍存在不可重复读、幻读。
3. 可重复读(Repeated Read)
- 特点:同一事务多次读取同一数据行时,结果始终一致(MySQL 默认隔离级别)。
- 并发问题:理论上存在幻读,MySQL 的 InnoDB 引擎通过 MVCC 机制解决。
4. 序列化(Serializable)
- 特点:最高隔离级别,强制事务串行执行,对读取的数据行加共享锁。
- 并发问题:完全避免脏读、脏写、不可重复读、幻读。
- 缺点:大量锁竞争,易导致超时和性能下降。
隔离级别与并发问题对应关系:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| Read Uncommitted | ✅ | ✅ | ✅ |
| Read Committed | ❌ | ✅ | ✅ |
| Repeated Read | ❌ | ❌ | ❌ |
| Serializable | ❌ | ❌ | ❌ |
三、InnoDB 锁机制
InnoDB 提供七种锁类型,按兼容性可分为共享 / 排它锁,按粒度可分为表锁和行锁,核心用于解决并发修改冲突。
1. 共享 / 排它锁(行级锁)
按兼容性分类的行级锁,控制数据读写权限:
- 共享锁(S 锁) :允许事务读数据,阻止其他事务加排他锁。多个事务可同时加 S 锁(读读兼容)。
- 语法:
select ... lock in share mode;
- 语法:
- 排他锁(X 锁) :允许事务读写数据,阻止其他事务加 S 锁或 X 锁(写写、读写互斥)。
- 语法:
select ... for update;
- 语法:
- 注意:加 X 锁后,其他事务仍可进行无锁查询(普通 select)。
2. 意向锁(表级锁)
为支持行级锁与表级锁共存而设计的表级锁,用于声明事务的加锁意向:
- 分类 :
- 意向共享锁(IS 锁):事务意向对表中某些行加 S 锁。
- 意向排它锁(IX 锁):事务意向对表中某些行加 X 锁。
- 兼容性:IS 锁与 IX 锁兼容,IS 锁与 S 锁兼容,IX 锁与 X 锁冲突(具体兼容关系见文档图示)。
3. 间隙锁(Gap Locks)
- 定义:当使用范围条件检索数据并加锁时,InnoDB 不仅对符合条件的索引项加锁,还对条件范围内不存在的 "间隙" 加锁。
- 示例 :表中 id 为 1-101,执行
select * from lock_example where id > 100 for update;,InnoDB 会对 id=101 的记录加锁,同时对 id>101 的间隙加锁。 - 作用:防止幻读,但会阻塞范围内的并发插入,导致锁等待。
- 优化建议:尽量使用相等条件访问数据,避免范围查询加锁。
4. 记录锁(Record Locks)
- 定义:对单个索引记录加锁,仅锁定指定行。
- 使用条件 :
- 查询字段必须是主键或唯一索引。
- 查询条件为精准匹配(=),不能是 >、<、like 等(否则退化为临键锁)。
- 示例 :
select * from lock_example where id = 1 for update;锁定 id=1 的记录。
5. 临键锁(Next-Key Locks)
- 定义:结合间隙锁与记录锁的锁机制,锁定左开右闭的区间,仅作用于非唯一索引列(唯一索引列会降级为记录锁)。
- 示例 :表中 age 为非唯一索引,执行
update lock_example set name = 'Vladimir' where age = 24;,会锁定 (10,24] 区间,同时锁定下一个区间的间隙,最终锁定 (10,32]。
6. 插入意向锁(Insert Intention Locks)
- 定义:间隙锁的一种,专门针对 insert 操作,支持同一区间内非冲突插入的并发执行。
- 示例:事务 A 在 age=10 和 24 之间插入 age=23 的记录,事务 B 可同时插入 age=24 的记录(位置不冲突),不会相互阻塞。
7. 自增锁(Auto-inc Locks)
- 定义:表级锁,针对 AUTO_INCREMENT 列的插入操作,确保主键值连续。
- 特点:一个事务插入时,其他事务的插入需等待,直到当前事务提交。
锁机制总结:
| 分类维度 | 锁类型 |
|---|---|
| 兼容性 | 共享锁(S)、排他锁(X) |
| 粒度 | 表锁(意向锁、自增锁)、行锁(记录锁、间隙锁、临键锁、插入意向锁) |
四、死锁
1. 核心概念
- 锁等待:事务需操作的资源被其他事务锁定,需等待资源释放(超时则报错)。
- 阻塞:因锁兼容性,一个事务的锁等待另一个事务的锁释放。
- 死锁:两个或多个事务相互争夺资源,形成循环等待,若无外力干预则无法推进。
2. 死锁示例
事务 A 修改数据 B 后等待修改数据 A,事务 B 修改数据 A 后等待修改数据 B,双方相互锁定对方所需资源,导致死锁。
3. MySQL 死锁解决方案
- 超时机制:当等待时间超过阈值,其中一个事务回滚,释放资源。
- 智能回滚:InnoDB 默认回滚 undo log 量最小的事务(减少性能损耗)。
五、多版本并发控制(MVCC)
MVCC 是 InnoDB 实现非阻塞读的核心机制,通过多版本数据实现读写不冲突,提升并发性能。
1. 核心概念
- MVCC 定义:多版本并发控制,通过维护数据的多个版本,允许读操作访问历史版本,避免加锁阻塞。
- 当前读:读取数据最新版本,加锁保证一致性(如 select for update、update、insert、delete)。
- 快照读:读取数据历史版本,不加锁(普通 select),仅在非串行隔离级别生效。
- MVCC 优势 :
- 读写不阻塞,提升并发性能。
- 解决脏读、不可重复读、幻读(无法解决更新丢失)。
2. 实现原理
MVCC 依赖隐式字段 、undo 日志 、Read View三大组件实现。
(1)隐式字段
每行数据除自定义字段外,包含三个隐式字段:
- DB_TRX_ID(6 字节):最近修改 / 插入该记录的事务 ID。
- DB_ROLL_PTR(7 字节):回滚指针,指向 undo 日志中的上一版本数据。
- DB_ROW_ID(6 字节):隐含自增主键(无主键时自动生成)。
- 隐藏删除标识:记录删除 / 更新时仅标记,不实际删除。
(2)undo 日志
记录数据的历史版本,分为两类:
- 插入 undo log:insert 操作产生,事务提交后可立即删除。
- 更新 undo log:update/delete 操作产生,供快照读和事务回滚使用,由 purge 线程清理。
- 数据版本链:多次修改后,undo 日志形成链表,通过 DB_ROLL_PTR 串联。
(3)Read View
事务快照读时生成的读视图,用于判断数据版本的可见性,包含三个属性:
- trx_list:当前活跃事务 ID 列表。
- up_limit_id:活跃事务最小 ID。
- low_limit_id:下一个未分配的事务 ID(最大事务 ID+1)。
(4)可见性判断规则
- 若数据的 DB_TRX_ID < up_limit_id:数据在当前事务之前提交,可见。
- 若数据的 DB_TRX_ID >= low_limit_id:数据在当前事务之后生成,不可见。
- 若 DB_TRX_ID 在 trx_list 中:事务未提交,数据不可见。
- 不可见时,通过 DB_ROLL_PTR 遍历 undo 日志,直到找到可见版本。
3. 执行流程示例
- 事务 1 插入数据(DB_TRX_ID=null,DB_ROLL_PTR=null)。
- 事务 1 修改数据,将旧数据写入 undo 日志,更新 DB_TRX_ID=1,DB_ROLL_PTR 指向 undo 日志。
- 事务 2 修改同一数据,将当前数据写入 undo 日志,更新 DB_TRX_ID=2,DB_ROLL_PTR 指向事务 1 的版本。
- 事务 3 快照读时,生成 Read View,通过可见性规则遍历版本链,找到符合条件的历史版本。
总结
MySQL 并发控制的核心是平衡一致性与性能:
- 事务隔离级别提供基础一致性保障,InnoDB 默认可重复读通过 MVCC 解决幻读。
- 锁机制解决写冲突,细粒度行锁提升并发,表锁保证全局一致性。
- MVCC 通过多版本数据实现非阻塞读,是高并发场景的关键优化。
- 死锁需通过合理的事务设计(如统一加锁顺序)和 MySQL 的自动回滚机制规避。