深入理解事务与锁机制
0. 引言
事务与锁是关系型数据库最核心也最复杂的机制。事务回答"并发操作如何保证正确性",锁与 MVCC回答"如何在保证正确性的前提下最大化并发度"。本文以 MySQL 8.0 InnoDB 为基准,从 ACID 到隔离级别,从 MVCC 版本链到各类锁的加锁行为,再到死锁的排查与规避,建立完整体系。
1. 事务的 ACID 特性
| 特性 | 含义 | InnoDB 的实现 |
|---|---|---|
| 原子性 Atomicity | 事务内操作要么全部成功、要么全部回滚 | undo log:记录变更前数据,回滚时反向执行 |
| 一致性 Consistency | 事务执行前后数据满足约束(完整性约束/业务规则) | 应用层保证 + 数据库约束 + 原子性/隔离性/持久性协同 |
| 隔离性 Isolation | 并发事务互不干扰 | 锁 + MVCC(本文核心) |
| 持久性 Durability | 事务提交后数据不丢失 | redo log(WAL:先写日志后写数据) |
关键认知:提交 ≠ 数据页已落盘,而是 redo 已落盘。这就是持久性与性能能兼得的原因——顺序写 redo 日志远比随机写数据页快。
2. 隔离级别与并发问题
2.1 三种并发异常
| 异常 | 现象 | 产生条件 |
|---|---|---|
| 脏读 | 读到其他事务未提交的修改 | 隔离级别 < Read Committed |
| 不可重复读 | 同一事务内两次读取同一行,结果不同(行被其他事务 UPDATE/删除) | 隔离级别 < Repeatable Read |
| 幻读 | 同一事务内两次范围查询,结果行数不同(其他事务 INSERT 新行) | 隔离级别 < Serializable |
2.2 四种隔离级别(SQL 标准 vs InnoDB)
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | InnoDB 默认 |
|---|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 | ❌ |
| READ COMMITTED | 不可能 | 可能 | 可能 | ❌(RC,MySQL 5.6+ binlog 可配) |
| REPEATABLE READ | 不可能 | 不可能 | 不可能(MVCC + 临键锁) | ✅ |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 | ❌(全表加锁) |
MySQL 与标准的差异:SQL 标准认为 RR 不能防幻读,但 InnoDB 通过 MVCC 快照读 + next-key lock(临键锁)当前读 在 RR 级别就消除了幻读。因此 MySQL 默认 RR 即可达到接近 Serializable 的正确性,同时保持高并发。
2.3 快照读与当前读
- 快照读(Snapshot Read):普通
SELECT(无FOR UPDATE/LOCK IN SHARE MODE),读 undo 版本链中的快照,不加锁; - 当前读(Current Read):
SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE、DELETE、INSERT,读取最新版本并加锁。
-- 快照读:不加锁,走 MVCC
SELECT * FROM account WHERE id = 1;
-- 当前读:加行锁(FOR UPDATE = 排他锁)
SELECT * FROM account WHERE id = 1 FOR UPDATE;3. MVCC 实现原理
3.1 undo 版本链
InnoDB 每行记录隐藏两个字段:trx_id(最后修改该行的事务 ID)与 roll_pointer(指向 undo 日志,形成版本链)。每次 UPDATE 并不覆盖旧值,而是生成新版本并串入版本链:
记录行(最新版,trx_id=20) ← roll_pointer ← 版本2(trx_id=18) ← 版本1(trx_id=15)3.2 ReadView(读视图)
快照读时,InnoDB 生成 ReadView,记录"当前活跃事务 ID 集合":
m_ids:生成 ReadView 时活跃事务 ID 列表;min_trx_id:m_ids 最小值;max_trx_id:下一个将分配的事务 ID;creator_trx_id:创建 ReadView 的事务 ID。
可见性判断:沿版本链回溯,若版本 trx_id < min_trx_id → 已提交,可见;若 trx_id ∈ m_ids → 未提交,不可见,继续找更早版本;若 trx_id ≥ max_trx_id → 生成 ReadView 后才开始的事务,不可见。
- RC 级别:每次快照读都生成新 ReadView(能看到其他事务新提交的数据 → 不可重复读);
- RR 级别:第一次快照读生成 ReadView 后整个事务复用(→ 可重复读)。这就是 RR 消除不可重复读的原理。
3.3 当前读与幻读的消除
当前读不走版本链,而是加锁。RR 级别下,InnoDB 对范围查询使用 next-key lock(临键锁)= 记录锁 + 间隙锁,锁住扫描区间内所有记录及记录之间的"间隙",其他事务无法在间隙内 INSERT,从而消除幻读。
4. InnoDB 锁机制全景
4.1 锁类型
| 锁 | 粒度 | 说明 |
|---|---|---|
| 共享锁 S | 行 | 读读兼容,LOCK IN SHARE MODE |
| 排他锁 X | 行 | 读写/写写互斥,FOR UPDATE/DML |
| 意向锁 IS/IX | 表 | 表级"意图"标记,避免逐行检查表锁冲突 |
| 记录锁 Record Lock | 行 | 锁定单条索引记录 |
| 间隙锁 Gap Lock | 区间 | 锁定记录之间的间隙,防插入(RC 级别关闭) |
| 临键锁 Next-Key Lock | 区间 | 记录锁 + 间隙锁,默认 RR 下范围查询使用 |
| 插入意向锁 | 区间 | 插入前的意向声明,多个可共存(互不阻塞) |
| 自增锁 AUTO-INC Lock | 表 | 自增计数器并发控制 |
| 元数据锁 MDL | 表 | DDL 与 DML 互斥,8.0 支持 LOCK WAIT 超时 |
4.2 next-key lock 的区间
以索引记录 10, 11, 13, 20 为例,RR 下范围查询会锁定如下区间:
(负无穷, 10] (10, 11] (11, 13] (13, 20] (20, 正无穷)-- 锁定 10~20 之间的所有记录与间隙,阻止插入 15
SELECT * FROM t WHERE c1 BETWEEN 10 AND 20 FOR UPDATE;4.3 自增锁(8.0 变化)
innodb_autoinc_lock_mode:
- 0/1(传统/连续):
INSERT ... SELECT等批量插入时持表级锁到语句结束; - 2(交错,8.0 默认):所有插入都不持表级自增锁,仅保证单语句内自增值连续。注意:基于 statement 的 binlog 复制下,交错模式可能导致主从自增值不一致——这也是 8.0 主从建议统一
binlog_format=ROW的原因之一。
4.4 元数据锁(MDL)
MySQL 5.5+ 引入:任何 DML 前自动获取表级 MDL 共享锁,DDL 需要 MDL 排他锁。长事务未提交会阻塞 DDL(Waiting for table metadata lock),8.0 通过 lock_wait_timeout 控制等待,且 ALTER TABLE ... ALGORITHM=INSTANT 等在线 DDL 降低了对业务的影响。
5. 加锁行为分析(面试高频)
| 场景 | 隔离级别 | 加锁行为 |
|---|---|---|
SELECT ... WHERE id=1(主键) | RR | 不加锁(快照读) |
SELECT ... WHERE id=1 FOR UPDATE | RR | 主键等值:仅记录锁;记录不存在:间隙锁 |
SELECT ... WHERE name='x' FOR UPDATE | RR | 非唯一索引等值:记录锁 + 相邻间隙锁 |
UPDATE WHERE age>100 | RR | 全区间 next-key lock(防幻读) |
| 无索引条件 UPDATE | RR | 锁全表所有记录与间隙(退化为表级锁效果) |
| RC + binlog=ROW | RC | 仅记录锁(间隙锁关闭,幻读不防但可接受) |
黄金法则:UPDATE/DELETE 必须走索引,否则 InnoDB 会锁住全表记录——这是生产事故的高发点。
6. 死锁
6.1 成因与检测
死锁 = 两个及以上事务互相持有对方需要的锁且都不释放。InnoDB 通过等待图(wait-for graph)检测,发现后回滚代价最小的事务(Deadlock found when trying to get lock),并返回 1213 错误码。
6.2 常见死锁场景
- 交叉加锁:事务 1 先 A 后 B,事务 2 先 B 后 A(按固定顺序加锁可避免);
- 间隙锁冲突:两个事务同时插入相邻区间,插入意向锁互相等待;
- 唯一键冲突:插入唯一键冲突时先加 S 锁再升级 X 锁,两个插入互相等待;
- 范围与点查交错:全表扫描与点查锁定范围重叠。
6.3 排查与规避
-- 查看最近一次死锁的完整信息(事务、持锁/等锁、SQL)
SHOW ENGINE INNODB STATUS\G
-- 8.0 性能表:实时锁等待
SELECT * FROM performance_schema.data_lock_waits\G
SELECT * FROM performance_schema.data_locks\G规避策略:
- 业务侧:所有事务按同一顺序访问资源;事务尽量短小;
- SQL 侧:
UPDATE/DELETE走索引;避免大范围无索引更新; - 架构侧:高并发下用 RC 隔离级别 + binlog=ROW(阿里等互联网大厂标配),减少间隙锁冲突;
- 兜底:应用捕获
1213错误码自动重试。
7. 小结
- ACID 四个特性分别由 undo(原子性)、锁+MVCC(隔离性)、redo(持久性)支撑,一致性由三者协同保证;
- MySQL 默认 RR 级别即防幻读:快照读走 MVCC 版本链,当前读走 next-key lock;
- ReadView 的复用时机(首次快照读 vs 每次快照读)是 RR 与 RC 的本质差异;
- 锁体系:S/X 行锁 + 意向锁 + 临键锁 + 自增锁 + MDL,加锁行为高度依赖索引与隔离级别;
- 死锁由等待图检测并回滚 victim,生产上以"顺序加锁 + 短事务 + RC 兜底重试"组合规避。
下一章讲解高性能数据库表设计:数据类型选择、范式与反范式、8.0 的新特性(JSON、生成列、不可见列)。