{T}

深入理解事务与锁机制

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 UPDATESELECT ... LOCK IN SHARE MODEUPDATEDELETEINSERT读取最新版本并加锁
sql
-- 快照读:不加锁,走 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 并不覆盖旧值,而是生成新版本并串入版本链:

text
记录行(最新版,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自增计数器并发控制
元数据锁 MDLDDL 与 DML 互斥,8.0 支持 LOCK WAIT 超时

4.2 next-key lock 的区间

以索引记录 10, 11, 13, 20 为例,RR 下范围查询会锁定如下区间:

text
(负无穷, 10]  (10, 11]  (11, 13]  (13, 20]  (20, 正无穷)
sql
-- 锁定 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 排他锁。长事务未提交会阻塞 DDLWaiting 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 UPDATERR主键等值:仅记录锁;记录不存在:间隙锁
SELECT ... WHERE name='x' FOR UPDATERR非唯一索引等值:记录锁 + 相邻间隙锁
UPDATE WHERE age>100RR全区间 next-key lock(防幻读)
无索引条件 UPDATERR锁全表所有记录与间隙(退化为表级锁效果)
RC + binlog=ROWRC仅记录锁(间隙锁关闭,幻读不防但可接受)

黄金法则:UPDATE/DELETE 必须走索引,否则 InnoDB 会锁住全表记录——这是生产事故的高发点。

6. 死锁

6.1 成因与检测

死锁 = 两个及以上事务互相持有对方需要的锁且都不释放。InnoDB 通过等待图(wait-for graph)检测,发现后回滚代价最小的事务Deadlock found when trying to get lock),并返回 1213 错误码。

图表渲染中…

6.2 常见死锁场景

  1. 交叉加锁:事务 1 先 A 后 B,事务 2 先 B 后 A(按固定顺序加锁可避免);
  2. 间隙锁冲突:两个事务同时插入相邻区间,插入意向锁互相等待;
  3. 唯一键冲突:插入唯一键冲突时先加 S 锁再升级 X 锁,两个插入互相等待;
  4. 范围与点查交错:全表扫描与点查锁定范围重叠。

6.3 排查与规避

sql
-- 查看最近一次死锁的完整信息(事务、持锁/等锁、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、生成列、不可见列)。