开放问答
MySQL 8.0 InnoDB 下 SELECT FOR UPDATE 走非唯一二级索引,gap lock 到底锁了哪些间隙导致死锁
该问题的正文、回答与评论。
提问者 陈清和-122 · 状态 open
MySQLInnoDBFOR UPDATE间隙锁死锁
背景:MySQL 8.0.36 / InnoDB,隔离级别 REPEATABLE READ。用户地址表 user_address,PK=id (bigint auto),二级索引 idx_user (user_id bigint, addr_type tinyint),addr_type ∈ {1,2,3} 分别代表家/公司/其他。单用户下通常 2~3 条记录。
现象:业务方报死锁,两条并发事务同时执行:
- 事务 A:SELECT * FROM user_address WHERE user_id = 1001 AND addr_type = 1 FOR UPDATE;
- 事务 B:SELECT * FROM user_address WHERE user_id = 1001 AND addr_type = 2 FOR UPDATE;
直觉上两条 SQL 命中 idx_user 的不同叶子节点(addr_type=1 vs addr_type=2),不应冲突。但 SHOW ENGINE INNODB STATUS 抓到的 LATEST DETECTED DEADLOCK 显示:A 在等 B 持有的 lock_mode X locks rec but gap,B 也在等 A 持有的 lock_mode X locks rec but gap。两个事务互相持对方需要的 gap 锁。
已尝试与结果:
- 把隔离级别降到 READ COMMITTED 重放,死锁消失——业务方不能接受全局降级;
- 改写为两步:先 SELECT id FROM user_address WHERE user_id=? AND addr_type=? FOR UPDATE 只锁主键,再 SELECT * WHERE id=?——死锁消失,但多一次回表且代码改动大;
- 翻 InnoDB 文档确认 RR 下非唯一二级索引会加 next-key lock(record + gap),但"两条 SQL 命中不同 addr_type 叶子,为什么 gap 范围会跨到对方行"这一点文档没讲透,我也没在脑子里拼出完整的锁图;
- 试过把 idx_user 改成 UNIQUE (user_id, addr_type),死锁也消失,但不确定是"唯一索引少加 gap"还是"record lock 精确命中不再扫间隙"。
期望的回答形态:
- 给一个 RR 隔离级别下,FOR UPDATE 命中非唯一二级索引时的锁范围判定规则:哪些行被 record lock、哪些区间被 gap lock、next-key lock 的边界到底落在索引 B+ 树的哪一级?最好配合 user_id=1001 下有三条记录(addr_type=1,2,3)的具体示意,画出 A 和 B 各自持有与等待的锁区间;
- 唯一二级索引 (user_id, addr_type) 下 gap lock 行为是否变化?变化点在哪?
- 在不降级隔离级别、不拆两步查询的前提下,有没有索引设计或 SQL 写法能避免这个死锁?
- 顺带:如果后续 WHERE 条件扩展成 WHERE user_id = ? AND addr_type IN (1,2),锁范围会怎么变?
评论
暂无评论。