锁机制
锁的分类
按粒度从大到小:
| 锁 | 说明 |
|---|---|
| 全局锁 | 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 级别):
- 加锁基本单位是临键锁
(上一条记录, 当前记录] - 查找过程中访问到的对象才加锁
- 唯一索引上的等值查询命中时,临键锁退化为记录锁
- 等值查询向右遍历且最后一个值不满足条件时,临键锁退化为间隙锁
- 非唯一索引上的等值查询要继续向右扫到第一个不满足的值才停
举例:表有 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 里,靠行锁天然互斥,无需版本号。