连载中 9/22

锁:行锁、间隙锁与一场死锁事故复盘

2026-05-06 · 3234 阅读 · 0 评论 · 0 赞

一场深夜死锁

凌晨两点,老王的库存扣减服务报出 Deadlock found when trying to get lock,QPS 一掉,订单堆积。查下来两个事务的写法都很「无害」,却互相掐死了。这篇先把 MySQL 的锁体系铺开,再复盘这场事故——死锁不可怕,可怕的是看不懂案发现场。

三层锁:全局、表、行

锁命令 / 场景备注
全局锁FLUSH TABLES WITH READ LOCK,全库只读逻辑备份用;InnoDB 场景建议改用 mysqldump --single-transaction(第 21 篇)
表级锁显式表锁、MDL 元数据锁、意向锁、AUTO-INC 锁MDL 最常坑人:长事务 + DDL 互堵(第 19 篇展开)
行级锁记录锁、间隙锁、next-key lockInnoDB 实现,本文主角

行锁的三件套

  • 记录锁(Record Lock):锁住索引上的一条记录。
  • 间隙锁(Gap Lock):锁住两条记录之间的开区间,RR 级别防幻读的武器——别人往这个区间插不进数据。间隙锁之间不冲突(都挡插入,不挡读)。
  • next-key lock:记录锁 + 前面的间隙(前开后闭区间),RR 下当前读的默认加锁单位。等值查询命中唯一索引时退化成纯记录锁。

先立一个最容易翻车的认知:行锁是加在索引上的。WHERE 条件没走索引,InnoDB 没法精确定位,只能对扫过的所有记录加锁(近似锁全表)——所以「更新条件必须走索引」不只是性能问题,是并发安全问题。

快照读与当前读

MVCC 管的是快照读(普通 SELECT,不加锁,读历史版本)。但 SELECT ... FOR UPDATE、FOR SHARE、以及所有 update / delete 是当前读:必须读最新版本并加锁。RR 防幻读的完整答案是:快照读靠 MVCC,当前读靠 next-key lock。

事故复盘:库存扣减死锁

简化后的案发代码,事务 A 和事务 B 几乎同时执行( goods_id 均不在索引上):

-- 事务 A                      -- 事务 B
BEGIN;                          BEGIN;
UPDATE stock SET num = num - 1
WHERE goods_id = 1001;
                                UPDATE stock SET num = num - 1
                                WHERE goods_id = 1002;
INSERT INTO stock_log (goods_id, num)
VALUES (1001, -1);
                                INSERT INTO stock_log (goods_id, num)
                                VALUES (1002, -1);
-- A 的 insert 等待 B 持有的主键间隙
-- B 的 insert 等待 A 持有的主键间隙 → 死锁

两条 UPDATE 因为没走索引,各扫全表并锁住途中所有 next-key 区间,随后两个 INSERT 都撞进对方持有的间隙,形成环路。MySQL 的死锁检测(innodb_deadlock_detect 默认开)会选代价小的事务回滚,报 Deadlock。案发现场用这条命令回看:

SHOW ENGINE INNODB STATUS;   -- 看 LATEST DETECTED DEADLOCK 段
-- 两个事务分别持有(HOLDS THE LOCK)与等待(WAITING FOR)哪把锁,一目了然
SET GLOBAL innodb_print_all_deadlocks = ON;   -- 把所有死锁记进错误日志

死锁治理四板斧

  • 让更新走索引:锁范围从「扫过的一切」缩到目标行,本案的第一修复。
  • 固定加锁顺序:所有事务按同一顺序访问资源(比如按 goods_id 升序),环路就不成立。
  • 事务要小:锁持有时间短,冲突窗口小;别在事务里做 RPC、慢查询。
  • 兜底重试:捕获死锁异常(错误码 1213)自动重试一次——死锁被回滚的一方没有污染,重试是安全的。

改完索引 + 排序加锁后,老王的死锁率归零。其实 Redis 系列第 8 篇的分布式锁之争里那句「单点不能拍板」,在 MySQL 锁这里有了新注脚:数据库的锁单机自洽,一旦上了主从,另一本账(binlog)的顺序就变得致命——下一篇讲 binlog 与两阶段提交。

咖啡凉了,记得趁热喝。

☕
503

10 年全栈工程师 · 503咖啡馆主理人

#MySQL#锁#间隙锁#next-key lock#死锁

评论 (0)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10101 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9873 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9294 阅读 · 21 评论 · 287 赞