一次真实的死锁排查

发布时间:2026/7/5 2:45:35

一次真实的死锁排查 什么是死锁死锁是指两个或多个事务互相持有对方所需的锁资源形成循环等待导致所有相关事务都无法继续执行的状态。事务A: 持有资源1的锁 → 等待资源2的锁 事务B: 持有资源2的锁 → 等待资源1的锁死锁产生的四个必要条件互斥条件— 资源同一时刻只能被一个事务持有持有并等待— 事务持有已获得的锁同时等待其他锁不可剥夺— 已获得的锁不能被强制释放只能由持有者主动释放循环等待— 事务之间形成环形的锁等待链四个条件同时满足死锁才会发生。常见死锁场景1. 不同顺序访问多行记录-- 事务A UPDATE account SET balance balance - 100 WHERE id 1; -- 锁住 id1 UPDATE account SET balance balance 100 WHERE id 2; -- 等待 id2 -- 事务B UPDATE account SET balance balance - 50 WHERE id 2; -- 锁住 id2 UPDATE account SET balance balance 50 WHERE id 1; -- 等待 id1 → 死锁2. 非唯一索引/组合条件导致的锁范围不确定使用非唯一索引作为 WHERE 条件时InnoDB 的加锁行为不像主键那样精确定位单行可能涉及间隙锁Gap Lock和临键锁Next-Key Lock导致不同事务锁住的范围产生重叠和冲突。-- 表: user_coupon有 idx_user_coupon(user_id, coupon_id) 非唯一索引 -- 事务A: 核销用户100的优惠券 UPDATE user_coupon SET status 1 WHERE (user_id, coupon_id) IN ((100, 201), (100, 202)); -- 事务B: 过期用户100的优惠券 UPDATE user_coupon SET status 2 WHERE (user_id, coupon_id) IN ((100, 202), (100, 203));在非唯一索引上InnoDB 会对索引记录及其间隙加锁。两个事务的锁范围存在交叉时就可能产生死锁。3. 间隙锁Gap Lock冲突-- 表中 id 有 1, 5, 10 -- 事务A SELECT * FROM t WHERE id 5 FOR UPDATE; -- 间隙锁 (5, ∞) -- 事务B INSERT INTO t (id) VALUES (7); -- 等待间隙锁4. 批量操作未排序-- 事务A: UPDATE t SET ... WHERE id IN (1, 2, 3) 加锁顺序 1→2→3 -- 事务B: UPDATE t SET ... WHERE id IN (3, 2, 1) 加锁顺序 3→2→1真实案例优惠券批量核销死锁问题背景电商大促期间用户下单时需要批量核销优惠券标记为已使用。高并发场景下批量更新优惠券状态频繁出现死锁。表结构简化如下CREATE TABLE user_coupon ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, coupon_id INT NOT NULL, status TINYINT DEFAULT 0 COMMENT 0-未使用 1-已使用 2-已过期, update_time INT, INDEX idx_user_coupon (user_id, coupon_id) );原始代码有死锁风险!-- MyBatis Mapper通过 user_id coupon_id 组合条件批量更新 -- update idbatchUseCoupons UPDATE user_coupon SET status #{status}, update_time #{updateTime} WHERE (user_id, coupon_id) IN foreach collectionpairs itempair open( separator, close) (#{pair.userId}, #{pair.couponId}) /foreach /update并发场景复现-- 事务A用户下单核销优惠券 (user_id100, coupon_id201), (user_id100, coupon_id202) UPDATE user_coupon SET status 1 WHERE (user_id, coupon_id) IN ((100,201),(100,202)); -- 事务B后台定时任务过期同一用户的优惠券 (user_id100, coupon_id202), (user_id100, coupon_id203) UPDATE user_coupon SET status 2 WHERE (user_id, coupon_id) IN ((100,202),(100,203)); -- 两个事务通过非唯一索引 idx_user_coupon 加锁锁范围重叠 → 死锁死锁原因分析(user_id, coupon_id)是非唯一组合索引不是主键通过非唯一索引定位行时InnoDB 使用 Next-Key Lock锁定范围比实际匹配行更大并发请求中不同事务的锁范围相互交叉形成循环等待每个事务内多个(user_id, coupon_id)组合的加锁顺序不固定进一步增大冲突概率修复方案改为主键更新// Service 层先查主键再按主键更新 public void batchUseCoupons(ListUserCouponPair pairs, int status) { int updateTime DateUtil.currentSecond(); // 第一步通过业务条件查出主键列表 ListLong ids couponDao.getIdsByUserAndCoupon(pairs); if (ids ! null !ids.isEmpty()) { // 第二步按主键批量更新加锁精确到行 couponDao.batchUpdateStatusByIds(ids, status, updateTime); } }!-- 第一步查询主键 -- select idgetIdsByUserAndCoupon resultTypejava.lang.Long SELECT id FROM user_coupon WHERE (user_id, coupon_id) IN foreach collectionpairs itempair open( separator, close) (#{pair.userId}, #{pair.couponId}) /foreach /select !-- 第二步按主键更新锁范围精确 -- update idbatchUpdateStatusByIds UPDATE user_coupon SET status #{status}, update_time #{updateTime} WHERE id IN foreach collectionids itemid open( separator, close) #{id} /foreach /update为什么有效对比项修复前修复后WHERE 条件非唯一组合索引 (user_id, coupon_id)主键 id锁类型Next-Key Lock行间隙Record Lock仅行锁锁范围可能锁住多行及间隙精确锁住目标行并发冲突锁范围重叠导致死锁锁不重叠无死锁核心原理通过主键唯一索引定位行时InnoDB 只加行锁Record Lock不需要间隙锁锁的范围最小且确定从根本上消除了锁交叉的可能性。通用解决方案总结预防层面策略做法原理用主键更新先查主键再按主键批量更新消除间隙锁精确加行锁固定加锁顺序按 id 升序排列后再操作破坏循环等待缩小锁粒度只锁必要的行减少冲突范围缩短事务时间事务中不做 RPC、不做耗时计算减少持锁时间代码层面// 1. 批量操作前排序 ListLong ids getTargetIds(); Collections.sort(ids); for (Long id : ids) { updateById(id); } // 2. 乐观锁代替悲观锁 UPDATE account SET balance balance - 100, version version 1 WHERE id 1 AND version #{oldVersion}; // 3. 合理的锁等待超时 SET innodb_lock_wait_timeout 5;处理层面// 死锁重试 Retryable(value DeadlockLoserDataAccessException.class, maxAttempts 3) public void doBatchUpdate(...) { ... }排查工具-- 查看死锁日志 SHOW ENGINE INNODB STATUS\G -- 查看当前锁等待 SELECT * FROM information_schema.INNODB_LOCK_WAITS; -- 查看当前事务 SELECT * FROM information_schema.INNODB_TRX;总结阶段关键动作设计时更新操作尽量走主键、统一加锁顺序编码时先查主键再更新、批量操作排序、设置超时运行时自动重试、监控告警、定期分析死锁日志死锁不可能完全避免核心思路是降低发生概率 快速检测恢复。本次案例的核心教训批量更新时非唯一索引条件会引入间隙锁造成不可预测的锁范围。改为主键条件更新让锁精确落在目标行上是最直接有效的死锁修复手段。

相关新闻