update 和 select ... for update 易触发死锁,因 innodb 行锁+间隙锁下访问路径不一致或加锁顺序不同所致,常见于未走索引导致全表扫描、函数/类型转换使索引失效、联合索引顺序不匹配、事务内锁序冲突等情况。

为什么 UPDATE 和 SELECT ... FOR UPDATE 容易触发死锁
死锁不是数据库“卡了”,而是两个及以上事务互相等待对方释放锁,谁都动不了。MySQL 的 InnoDB 在行级锁 + 间隙锁(Gap Lock)机制下,只要访问路径不一致、加锁顺序不统一,就极易撞上死锁。
常见现象:Deadlock found when trying to get lock; try restarting transaction 频繁报出,尤其在高并发更新同一张表的场景下。
- 根本原因常是 SQL 没走索引,导致全表扫描加锁,锁范围远超预期
-
WHERE条件用函数(如WHERE DATE(create_time) = '2024-01-01')或隐式类型转换(如字符串 ID 跟数字比较),让索引失效 - 多条件查询时,
WHERE子句字段顺序和联合索引定义顺序不一致,无法命中索引最左前缀 - 事务里先
SELECT ... FOR UPDATE再UPDATE,但另一路事务反着来(先UPDATE再SELECT ... FOR UPDATE),锁序天然冲突
如何确认当前 SQL 是否走了索引
别猜,用 EXPLAIN 看执行计划。重点盯 type、key、rows 和 Extra 四列。
-
type值为ALL或index:基本等于全表/全索引扫描,危险信号 -
key为空:没走任何索引,哪怕有索引也白搭 -
rows远大于实际匹配行数:说明优化器估算失准,往往伴随锁范围扩大 -
Extra出现Using filesort或Using temporary:虽不直接导致死锁,但意味着查询低效,事务持有锁时间拉长,间接增加死锁概率
示例:EXPLAIN SELECT * FROM order WHERE user_id = 123 AND status = 'paid'; —— 如果 user_id 和 status 没建联合索引,或索引顺序是 (status, user_id),那这条语句大概率只用上第一个字段,第二个字段靠遍历过滤,锁住更多无关行。
哪些索引设计能真正降低死锁概率
索引不是越多越好,而是要覆盖「高频更新+高并发争抢」的查询模式。核心原则:让每条关键 DML 语句都能精准定位到最小行集,并保持加锁顺序绝对一致。
- 对
UPDATE ... WHERE a = ? AND b = ?类语句,优先建联合索引(a, b),而非单列索引a和b各一个 - 如果经常按
created_at范围查再更新,又常带user_id,不要建(created_at)单列索引,改用(user_id, created_at)—— 把等值条件放前面,范围条件放后面 - 避免在索引列上做运算:比如
WHERE ABS(score) > 80会让score索引失效;应改写为WHERE score > 80 OR score - 主键尽量用自增整型。UUID 或字符串主键在插入时可能引发页分裂,影响聚簇索引结构稳定性,间接增加锁竞争
事务内 SQL 顺序与锁粒度控制技巧
即使索引没问题,SQL 执行顺序不对,照样死锁。InnoDB 加锁是语句执行时逐行进行的,顺序决定锁序,锁序决定是否死锁。
- 所有事务更新同一组数据时,强制按相同字段排序后再操作。例如:更新用户订单前,先
SELECT id FROM order WHERE user_id = 123 ORDER BY id ASC,再按顺序逐个UPDATE - 避免在事务里混合使用
SELECT ... FOR UPDATE和普通SELECT。前者加锁,后者不加,但若后续又UPDATE,可能因读取旧值而重复加锁,逻辑混乱 - 把
UPDATE拆成「先查后更」时,务必用SELECT ... FOR UPDATE,且确保查和更的WHERE条件完全一致(包括参数类型),否则可能查到 A 行,却去更新 B 行,锁错对象 - 能用
UPDATE ... WHERE一行搞定的,别在应用层查出来再拼UPDATE。减少网络往返和事务空转时间,就是缩短锁持有窗口
真正难处理的,是业务上必须跨多张表更新、且不同入口调用顺序天然不一致的情况——这时候光靠 SQL 和索引不够,得在应用层引入分布式锁或严格约定更新路由规则,否则死锁只是早晚问题。











