跳到主要内容

锁机制

锁的分类​

按粒度从大到小:

锁说明
全局锁FLUSH TABLES WITH READ LOCK(FTWRL),全库只读,用于全量逻辑备份
表锁LOCK TABLES ... READ/WRITE,InnoDB 下很少手动用
元数据锁 MDL自动加。增删改查加 MDL 读锁,改表结构加 MDL 写锁
意向锁 IS / IX表级,标记「表内有行被加了共享/排他锁」
行锁记录锁、间隙锁、临键锁、插入意向锁

意向锁解决什么问题? 事务要加表级写锁时,必须确认表里没有任何行锁。没有意向锁就得逐行检查,O(n)。有了意向锁只需看表上有没有 IX/IS 标记,O(1)。意向锁之间互相兼容,只和表级读写锁冲突。

:::warning MDL 导致的线上事故模式 给大表加字段时,如果有一个慢查询正持有 MDL 读锁,ALTER TABLE 要等的 MDL 写锁拿不到,于是排队;而后续所有新查询都要排在这个 MDL 写锁之后,整张表瞬间不可用。

规避:改表前先查 information_schema.innodb_trx 确认无长事务;ALTER 语句加 WAIT n 超时放弃(MariaDB)或用 gh-ost / pt-online-schema-change 做在线改表。 :::

行锁的三种形态​

InnoDB 的行锁都是加在索引上的,这是理解一切的前提。

形态范围作用
记录锁 Record Lock单条索引记录锁住一行
间隙锁 Gap Lock两条记录之间的开区间阻止插入,防幻读
临键锁 Next-Key Lock前开后闭区间 (a, b]记录锁 + 间隙锁,RR 下的默认形态
插入意向锁间隙中的插入点INSERT 前加,与间隙锁冲突

间隙锁只在 RR 下存在,RC 下退化为纯记录锁。所以「RC 下没有间隙锁、并发更高但有幻读」是一个常见的取舍答法。

间隙锁之间互相兼容 —— 两个事务可以同时持有同一个间隙的间隙锁。这是死锁的常见来源:

-- 事务 A -- 事务 B
SELECT * FROM t WHERE id = 5
FOR UPDATE; -- 持有 (1,10) 间隙锁
SELECT * FROM t WHERE id = 6
FOR UPDATE; -- 也能拿到 (1,10) 间隙锁
INSERT INTO t VALUES (5);
-- 需要插入意向锁,被 B 的间隙锁阻塞
INSERT INTO t VALUES (6);
-- 被 A 的间隙锁阻塞 → 死锁

加锁范围怎么判定​

高频手撕题。核心规则(RR 级别):

  1. 加锁基本单位是临键锁 (上一条记录, 当前记录]
  2. 查找过程中访问到的对象才加锁
  3. 唯一索引上的等值查询命中时,临键锁退化为记录锁
  4. 等值查询向右遍历且最后一个值不满足条件时,临键锁退化为间隙锁
  5. 非唯一索引上的等值查询要继续向右扫到第一个不满足的值才停

举例:表有 id(主键)值 1, 5, 10,执行 UPDATE t SET v=1 WHERE id = 7:

  • 不存在 id=7,在唯一索引上找不到 → 不退化为记录锁
  • 命中临键锁 (5, 10],但 10 不满足条件 → 规则 4 退化为间隙锁 (5, 10)
  • 结果:锁住 (5, 10),此区间无法插入,但可以更新 id=10 的行

二级索引的等值查询会锁两棵树:先在二级索引上加临键锁,回表后再对主键索引上的对应记录加记录锁。

死锁​

检测与处理:InnoDB 默认开启死锁检测(innodb_deadlock_detect = ON),发现环路后回滚持有锁最少的那个事务,报 ERROR 1213。另有 innodb_lock_wait_timeout(默认 50 秒)兜底锁等待超时。

高并发下死锁检测本身是开销 —— 每个新来的等待线程都要遍历锁等待图,热点行上 1000 个并发就是 100 万次检测。极端场景可以关掉检测只靠超时,但要先评估超时时间。

排查手段:SHOW ENGINE INNODB STATUS 看 LATEST DETECTED DEADLOCK 段,里面有两个事务各自持有和等待的锁。

预防:

  • 所有事务按同一顺序访问多张表 / 多行
  • 事务尽量短小,把非数据库操作(RPC、发邮件)移出事务
  • 批量更新时按主键排序
  • 用唯一索引代替「先查再插」的逻辑,靠 INSERT ... ON DUPLICATE KEY UPDATE 把两步合一

乐观锁与悲观锁​

  • 悲观锁:SELECT ... FOR UPDATE,先加锁再改。适合冲突频繁、重试代价高的场景
  • 乐观锁:加版本号列,UPDATE t SET v = v+1, version = version+1 WHERE id = ? AND version = ?,靠影响行数判断是否成功。适合冲突少的场景,但要处理重试

扣库存这类场景常用乐观锁的变体:UPDATE stock SET num = num - 1 WHERE id = ? AND num >= 1,把判断条件下推到 SQL 里,靠行锁天然互斥,无需版本号。