事务、锁与 MVCC

InnoDB 通过保证并发写的正确性,通过 MVCC 让快照读不加锁。理解二者,才能正确处理并发与死锁。

InnoDB 锁类型

说明
记录锁 Record Lock锁住索引上的一条记录
间隙锁 Gap Lock锁住两条记录之间的「间隙」,防插入
Next-Key Lock记录锁 + 间隙锁,锁住记录及其前面的间隙
意向锁 IS/IX表级标志,表示表内存在行锁
插入意向锁插入前设置,多个插入意向锁互不冲突
AUTO-INC 锁自增列的轻量锁

加锁的本质是加在索引上:如果查询没有走索引,InnoDB 只能锁住更多行甚至全表。

快照读 vs 当前读

  • 快照读:普通 SELECT,读 MVCC 版本链,不加锁
  • 当前读SELECT ... FOR UPDATE / LOCK IN SHARE MODEUPDATEDELETEINSERT加锁读取最新版本。
BEGIN;
-- 排他锁:其它事务无法读取(当前读)与修改
SELECT * FROM inventory WHERE sku = 'A001' FOR UPDATE;

-- 共享锁:其它事务可读,但不能写
SELECT * FROM inventory WHERE sku = 'A001' FOR SHARE;

COMMIT;

隔离级别下的加锁行为

以可重复读(RR)为例:

查询条件加锁范围
主键等值命中记录锁(退化)
唯一索引等值命中记录锁
非唯一索引等值Next-Key Lock,锁住记录与间隙
范围查询Next-Key Lock,锁住整个扫描区间
无索引条件锁住全表记录(最坏情况)
Tip

在 RC(读已提交)级别下没有间隙锁。若业务并发写冲突多、且不依赖 RR 的可重复读语义,将隔离级别改为 RC 能显著减少锁竞争与死锁概率,这是许多互联网公司的默认配置。

死锁

死锁是「互相等待对方持有的锁」。InnoDB 会自动检测并回滚代价较小的事务。

-- 事务 A                        -- 事务 B
BEGIN;                          BEGIN;
UPDATE t SET v=1 WHERE id=1;    UPDATE t SET v=1 WHERE id=2;
UPDATE t SET v=1 WHERE id=2;    UPDATE t SET v=1 WHERE id=1;
-- 两个事务互相等待 → 死锁,InnoDB 回滚其一
-- 查看最近一次死锁信息
SHOW ENGINE INNODB STATUS\G

减少死锁的原则

  1. 事务中访问多张表/多行时,按固定顺序加锁。
  2. 缩短事务、减少持锁时间,把非数据库操作移出事务。
  3. 尽量让加锁走唯一索引等值,缩小锁范围。
  4. 必要时设置锁等待超时与重试逻辑。
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';  -- 默认 50 秒
SET SESSION innodb_lock_wait_timeout = 5;

监控锁等待

-- MySQL 8:查看当前锁等待
SELECT * FROM performance_schema.data_lock_waits;
SELECT * FROM performance_schema.data_locks;
Warning

长事务是锁问题的根源:它持锁久、undo 版本链长、还会拖慢 purging。生产上应监控并告警长事务(如 information_schema.innodb_trxtrx_started 距今过长的记录)。

小结

  • 锁加在索引上:无索引条件会导致大范围加锁甚至全表锁。
  • 快照读走 MVCC 不加锁;当前读与写操作会加锁。
  • RR 有间隙锁、RC 没有;理解这点是减少死锁的关键。
  • 固定加锁顺序、缩短事务、走唯一索引,是防死锁三件套。