MySQL 单点详解(07):锁加在哪、间隙锁锁什么、死锁怎么查
面试官:update 一行没走索引,为什么会锁住整张表?
面试官:间隙锁锁的是数据吗?两个事务能同时拿到同一个间隙的锁吗?
锁的对象不是行,是索引上的位置。
这句话能解释锁相关题目的一半,另一半在索引上。
一、锁是加在索引上的
InnoDB 加行锁,锁的是索引项,不是数据行本身。
表① 三种粒度,从大到小
| 锁 | 锁住什么 | 什么时候用 |
|---|---|---|
| 全局锁 | 整个库只读 | 全库逻辑备份 |
| 表锁 | 整张表 | 改表结构、显式加锁 |
| 行锁 | 索引上的一条记录 | 改数据、加锁读 |
为什么没走索引会锁表?因为找不到定位点,只能一行一行扫过去,扫到的每一行都加上锁。
锁的不是表,是表里所有的行。效果和锁表一样,但代价更重。
所以 update 一定要带上索引。这不只是为了快,更是为了锁得少。
一张十万行的表,走索引只锁一行,不走索引锁十万行,别人全都得排队。
同一个道理还能解释锁的范围差异:按订单号改状态,只锁那一行;按用户 id 改,就锁这个用户名下的全部订单。
同一条 update,锁的范围可能差几个数量级,差别只在那句 where 用了哪个索引。
另外记一句:普通 select 不加锁,走的是第六期那条版本链。加锁读、update、delete 才加。
二、行锁有三种形态
表② 记录、间隙、临键
| 名字 | 锁住什么 | 什么时候出现 |
|---|---|---|
| 记录锁 | 一条存在的记录 | 等值命中唯一索引 |
| 间隙锁 | 两条记录中间的空隙 | 范围查询,且间隙里有空档 |
| 临键锁 | 间隙加右边那条记录 | 默认形态,范围查询常用 |
临键锁是左开右闭:锁住 (3, 8] 这个区间,含 8,不含 3。
唯一索引等值命中时,临键锁会退化成记录锁。因为唯一,右边不可能再有相同的键,那个间隙没必要锁。
间隙锁有个反直觉的地方:它只挡插入,不挡别人拿同一把锁。
两个事务能同时拿到同一个间隙的锁,谁也不挡谁。
真正被挡住的,是想往这个空隙里插数据的那一方。间隙锁只跟插入过不去。
所以间隙锁防的是幻读:不让新行插进刚查过的范围。
代价是并发下降。这也是很多团队把隔离级别调成 RC 的原因:RC 下基本不用间隙锁,幻读的风险自己扛。
三、死锁怎么查
死锁是两个事务互相等对方手里的锁,谁也动不了。
InnoDB 默认开着死锁检测。发现环以后,会挑一个代价小的回滚,另一个放行。
检测本身要遍历等待关系,事务一多就有开销。所以有的团队宁可关掉检测、等超时,用慢一点换稳一点。
表③ 三种常见死法
| 场景 | 为什么会死 | 怎么改 |
|---|---|---|
| 两个事务改两行,顺序相反 | 各拿一把,各等一把 | 约定统一顺序 |
| 唯一键冲突后改加锁读 | 间隙锁互相等 | 用 insert ignore,或先查后插 |
| 大事务分批改 | 锁范围大、持有久 | 缩小批次,尽快提交 |
排查看 show engine innodb status 的输出,里面有一段专门记最近一次死锁。
它会写清谁等了谁、各自持有什么锁、最后回滚了哪个事务。
线上真正管用的不是事后排查,是三条习惯:事务尽量短、加锁顺序统一、批量操作分批提交。
四、面试官会往下问的三层
第一层:意向锁是干什么的?
它是个表级标记,表示"这张表里有行被锁了"。
有了它,别人想加表锁时看一眼标记就知道要不要等,不用一行一行去查。
第二层:为什么 RC 没有间隙锁?
因为 RC 允许幻读,不需要用间隙锁去挡插入。
副作用是并发更高,代价是同一事务里两次范围查询可能条数不同。
第三层:锁等待超时和死锁有什么区别?
锁等待是排队排太久,自己放弃,报超时。
死锁是绕成了一个环,永远排不到,得靠检测来打破。
一个是等不到,一个是等不到还互相等。
收尾
这一期可以压成三句:锁加在索引上,没索引就锁全表;间隙锁挡的是插入,不是数据;死锁靠顺序和短事务防,不靠事后查。