尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

一次真实的死锁排查

一次真实的死锁排查
📅 发布时间:2026/7/5 2:50:47

什么是死锁

死锁是指两个或多个事务互相持有对方所需的锁资源,形成循环等待,导致所有相关事务都无法继续执行的状态。

事务A: 持有资源1的锁 → 等待资源2的锁 事务B: 持有资源2的锁 → 等待资源1的锁

死锁产生的四个必要条件

  1. 互斥条件— 资源同一时刻只能被一个事务持有
  2. 持有并等待— 事务持有已获得的锁,同时等待其他锁
  3. 不可剥夺— 已获得的锁不能被强制释放,只能由持有者主动释放
  4. 循环等待— 事务之间形成环形的锁等待链

四个条件同时满足,死锁才会发生。

常见死锁场景

1. 不同顺序访问多行记录

-- 事务A UPDATE account SET balance = balance - 100 WHERE id = 1; -- 锁住 id=1 UPDATE account SET balance = balance + 100 WHERE id = 2; -- 等待 id=2 -- 事务B UPDATE account SET balance = balance - 50 WHERE id = 2; -- 锁住 id=2 UPDATE account SET balance = balance + 50 WHERE id = 1; -- 等待 id=1 → 死锁

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 id="batchUseCoupons"> UPDATE user_coupon SET status = #{status}, update_time = #{updateTime} WHERE (user_id, coupon_id) IN <foreach collection="pairs" item="pair" open="(" separator="," close=")"> (#{pair.userId}, #{pair.couponId}) </foreach> </update>

并发场景复现:

-- 事务A:用户下单,核销优惠券 (user_id=100, coupon_id=201), (user_id=100, coupon_id=202) UPDATE user_coupon SET status = 1 WHERE (user_id, coupon_id) IN ((100,201),(100,202)); -- 事务B:后台定时任务,过期同一用户的优惠券 (user_id=100, coupon_id=202), (user_id=100, coupon_id=203) UPDATE user_coupon SET status = 2 WHERE (user_id, coupon_id) IN ((100,202),(100,203)); -- 两个事务通过非唯一索引 idx_user_coupon 加锁,锁范围重叠 → 死锁

死锁原因分析

  1. (user_id, coupon_id)是非唯一组合索引,不是主键
  2. 通过非唯一索引定位行时,InnoDB 使用 Next-Key Lock,锁定范围比实际匹配行更大
  3. 并发请求中,不同事务的锁范围相互交叉,形成循环等待
  4. 每个事务内多个(user_id, coupon_id)组合的加锁顺序不固定,进一步增大冲突概率

修复方案:改为主键更新

// Service 层:先查主键,再按主键更新 public void batchUseCoupons(List<UserCouponPair> pairs, int status) { int updateTime = DateUtil.currentSecond(); // 第一步:通过业务条件查出主键列表 List<Long> ids = couponDao.getIdsByUserAndCoupon(pairs); if (ids != null && !ids.isEmpty()) { // 第二步:按主键批量更新,加锁精确到行 couponDao.batchUpdateStatusByIds(ids, status, updateTime); } }
<!-- 第一步:查询主键 --> <select id="getIdsByUserAndCoupon" resultType="java.lang.Long"> SELECT id FROM user_coupon WHERE (user_id, coupon_id) IN <foreach collection="pairs" item="pair" open="(" separator="," close=")"> (#{pair.userId}, #{pair.couponId}) </foreach> </select> <!-- 第二步:按主键更新,锁范围精确 --> <update id="batchUpdateStatusByIds"> UPDATE user_coupon SET status = #{status}, update_time = #{updateTime} WHERE id IN <foreach collection="ids" item="id" open="(" separator="," close=")"> #{id} </foreach> </update>

为什么有效

对比项修复前修复后
WHERE 条件非唯一组合索引 (user_id, coupon_id)主键 id
锁类型Next-Key Lock(行+间隙)Record Lock(仅行锁)
锁范围可能锁住多行及间隙精确锁住目标行
并发冲突锁范围重叠导致死锁锁不重叠,无死锁

核心原理:通过主键(唯一索引)定位行时,InnoDB 只加行锁(Record Lock),不需要间隙锁,锁的范围最小且确定,从根本上消除了锁交叉的可能性。

通用解决方案总结

预防层面

策略做法原理
用主键更新先查主键,再按主键批量更新消除间隙锁,精确加行锁
固定加锁顺序按 id 升序排列后再操作破坏循环等待
缩小锁粒度只锁必要的行减少冲突范围
缩短事务时间事务中不做 RPC、不做耗时计算减少持锁时间

代码层面

// 1. 批量操作前排序 List<Long> 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;

总结

阶段关键动作
设计时更新操作尽量走主键、统一加锁顺序
编码时先查主键再更新、批量操作排序、设置超时
运行时自动重试、监控告警、定期分析死锁日志

死锁不可能完全避免,核心思路是:降低发生概率 + 快速检测恢复。

本次案例的核心教训:批量更新时,非唯一索引条件会引入间隙锁,造成不可预测的锁范围。改为主键条件更新,让锁精确落在目标行上,是最直接有效的死锁修复手段。

相关新闻

  • AI 科普组件:复杂概念要给读者台阶
  • MagicWorld 实现长时交互视频世界建模
  • 数据指标 SLA:报表准时不代表指标可信

最新新闻

  • 如何在Linux上使用FSearch实现极速文件搜索:完整效率指南
  • Django模板AJAX局部更新实战:零侵入增强交互体验
  • 萌新入坑不用到处找资源!老宅私藏一站式 ACG 社区二次元之家分享
  • TLSFOWARD:如何识别UA与TLS指纹不一致
  • 成都知名的中央空调公司有哪些
  • MyBatis-Plus 批量操作与 rewriteBatchedStatements 优化

日新闻

  • 基于YOLOv12的番茄成熟度智能检测系统开发
  • 终极RimWorld模组管理指南:用RimSort告别模组冲突烦恼
  • AI Agent框架开发:从理论到实践的完整指南

周新闻

  • 基于YOLOv12的番茄成熟度智能检测系统开发
  • 终极RimWorld模组管理指南:用RimSort告别模组冲突烦恼
  • AI Agent框架开发:从理论到实践的完整指南

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号