select for share与for update互相阻塞,因共享锁(s锁)与排他锁(x锁)互斥:一方持有s锁时另一方无法获取x锁,反之亦然;非唯一索引下二者均可能触发next-key lock,扩大锁范围并引发死锁。

SELECT FOR SHARE 和 FOR UPDATE 为什么会互相阻塞
因为 FOR SHARE 加的是共享锁(S 锁),FOR UPDATE 加的是排他锁(X 锁),而 S 锁与 X 锁互斥——不是“读不干扰写”,而是“谁先抢到谁说了算”。一旦事务 A 持有某行的 S 锁,事务 B 就无法对该行加 X 锁;反之,若事务 A 已持 X 锁,事务 B 连 S 锁都拿不到。
常见错误现象:
- 误以为
FOR SHARE是“只读无害锁”,在关键路径中混用却没统一加锁顺序 - 一个事务先
SELECT ... FOR SHARE查 A 行,再SELECT ... FOR UPDATE改 B 行;另一个事务反着来:先FOR UPDATE改 B 行,再FOR SHARE查 A 行 → 死锁立现 - 缓存里随机取一批 ID(如
[101, 205, 98]),遍历执行FOR SHARE,但不同请求顺序不一致 → 锁序混乱
死锁复现的关键条件:非唯一索引 + 临键锁范围重叠
当 WHERE 条件命中非唯一索引(比如 user_id 普通索引)时,FOR SHARE 不只锁住匹配行,还会加 Next-Key Lock(记录锁 + 间隙锁)。例如:
SELECT * FROM order WHERE user_id = 123 FOR SHARE;
若 user_id 无唯一约束,且当前索引中存在 user_id = 122 和 user_id = 124 的记录,则该语句实际锁住区间 (122, 124] —— 包括 123 这一行,以及 122 和 124 之间的空隙。此时另一事务执行:
SELECT * FROM order WHERE user_id = 123 FOR UPDATE;
会直接等待,而如果它同时持有另一行(如 user_id = 125)的 X 锁,就极易形成循环等待。
实操建议:
- 用
EXPLAIN确认FOR SHARE是否走了索引;没走索引 → 全表扫描 → 所有行都加 S 锁 → 死锁概率飙升 - 对高频并发查询字段,优先建唯一索引;若做不到,至少加普通索引并配合
ORDER BY显式控制扫描边界 - 避免在
FOR SHARE后紧跟FOR UPDATE操作同一张表的不同行 —— 宁可合并为一条UPDATE或拆成两个独立事务
如何让 FOR SHARE 不拖累整体并发性
FOR SHARE 本身不改数据,但它的锁会阻塞后续所有 X 锁请求,包括 UPDATE、DELETE 和 FOR UPDATE。它不像快照读那样无感,而是真实参与锁竞争。
性能影响点:
- 隔离级别为
REPEATABLE READ时,默认启用 Next-Key Lock,锁范围比READ COMMITTED大得多 - 事务未及时提交,S 锁长期持有 → 其他事务卡在
Waiting for table metadata lock或更常见的Waiting for X lock on ... - 高并发下大量短事务争抢同一行 S 锁,虽不直接死锁,但造成明显排队延迟
实操建议:
- 把
FOR SHARE放在事务最末尾(靠近COMMIT),缩短锁持有时间 - 能用快照读(普通
SELECT)就别用FOR SHARE;仅在必须防止幻读或确保“查到即能用”的场景才启用 - 考虑降级隔离级别至
READ COMMITTED:该级别下FOR SHARE只加记录锁,不加间隙锁,锁粒度更小
排查和验证死锁的最小可行动作
不要靠猜。MySQL 提供了即时反馈工具,关键是用对时机和方式。
立即可用的操作:
- 执行
SHOW ENGINE INNODB STATUS\G,重点看LATEST DETECTED DEADLOCK区块,它会明确写出哪两个事务、哪几行、分别持有什么锁、等待什么锁 - 开启
innodb_print_all_deadlocks=ON,所有死锁细节都会记入 MySQL 错误日志(路径通常为/var/log/mysql/error.log),方便回溯高频模式 - 在应用层捕获
Deadlock found when trying to get lock错误码(1213),并自动重试——这不是补丁,而是必要设计
最容易被忽略的一点:死锁日志里显示的 SQL 往往是“最后执行的那条”,但真正埋雷的,常是前面几条没加索引的 FOR SHARE 或顺序混乱的多行锁操作。定位要往前翻事务上下文,不能只盯报错那一行。











