SELECT ... LOCK IN SHARE MODE看似安全却埋雷,因其加S锁兼容,事务A、B可同时成功加锁不阻塞,但后续UPDATE需升级为X锁,而S与X互斥,导致双方等待对方释放S锁形成死锁。

SELECT ... LOCK IN SHARE MODE 为什么看似安全却埋雷
因为 S 锁之间兼容,事务 A 和事务 B 都能对同一行成功加锁,不阻塞、不报错——这恰恰是最危险的信号。它制造了一种“一切正常”的假象,掩盖了后续升级为 X 锁时的冲突本质。
常见错误现象:Deadlock found when trying to get lock 出现在 UPDATE 语句上,而非 SELECT;执行计划里明明走了主键,却还是死锁;两个事务日志几乎同时提交 SELECT,然后几乎同时执行 UPDATE。
- 关键前提:目标行当前未被任何事务修改(即不在活跃未提交的更新中)
- 此时
SELECT ... LOCK IN SHARE MODE立即返回,双方都拿到 S 锁 - 紧接着各自执行
UPDATE ... WHERE id = ?,都需要将 S 锁升级为 X 锁 - 但 InnoDB 不允许 S 锁与 X 锁共存 → 双方互相等待对方释放 S 锁 → 循环等待形成死锁
共享锁升级失败的真实锁兼容性逻辑
很多人误以为“S 锁只读不写,所以不会冲突”,其实问题出在锁模式转换的原子性缺失上:InnoDB 没有“S→X 原子升级”机制,必须先释放 S 锁再申请 X 锁(或直接等待),而这个间隙就是死锁温床。
锁兼容性不是看“能不能加 S 锁”,而是看“当一方持 S 锁、另一方要加 X 锁时是否允许”。查官方兼容矩阵可知:S 与 X 互斥,S 与 S 兼容——这正是双事务能同时成功的根源,也是后续卡死的必然条件。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 事务 A 持
S锁,请求X锁 → 等待事务 B 释放S锁 - 事务 B 持
S锁,请求X锁 → 等待事务 A 释放S锁 - InnoDB 检测到该循环后,随机回滚其中一个事务,抛出
Deadlock found when trying to get lock
前端连点 + 后端无防护 = 死锁高发组合
按钮没禁用、接口无幂等、SQL 逻辑是“查是否存在 → 加 S 锁 → INSERT/UPDATE”,这种链路在并发下极易触发双 S 锁路径。尤其当事务极短(纯 SQL,毫秒级),时间窗口压缩到微秒级,死锁概率陡增。
- 真实场景中,
SELECT ... LOCK IN SHARE MODE多用于“防重复插入”或“读取后校验再更新”,但这两类业务天然适合乐观锁 - 联合索引前缀匹配时,即使查唯一值,也可能因 Gap Lock 卷入额外行,扩大冲突面
-
SHOW ENGINE INNODB STATUS中的lock_mode S locks rec but not gap能确认是否仅锁记录;若出现lock_mode S locks gap before rec,说明间隙也被锁住,风险更高
比修复更有效的三个落地动作
与其在死锁发生后重试或调超时,不如从设计源头切断双 S 锁路径。真正容易被忽略的点不是锁怎么加,而是:只要用了 LOCK IN SHARE MODE 做读写分离式更新,就默认接受了“成功读取 ≠ 安全更新”这个事实。
- 把
SELECT ... LOCK IN SHARE MODE换成SELECT ... FOR UPDATE:第二个事务直接阻塞,不抢 S 锁,自然没有双升级冲突 - 改用乐观锁:加
version字段,UPDATE 时带WHERE version = ?,失败则重试,避免行锁竞争 - 合并为原子语句:能用
INSERT ... ON DUPLICATE KEY UPDATE或UPDATE ... WHERE直接完成判断+变更的,绝不拆成两步










