当前位置: 首页 > news >正文

MySQL死锁问题分析与解决方案

MySQL死锁问题分析与解决方案

问题现象

在高并发的业务系统中,我们遇到了一个典型的MySQL死锁问题。腾讯云数据库系统日志显示如下错误信息:

*** (1) TRANSACTION:  
TRANSACTION 17581947971, ACTIVE 32 sec fetching rows  
UPDATE business_table SET field1='value1', field2='value2', status=2, step_id=3  
WHERE order_id = 'ORDER_001' AND server_id = 12345  *** (1) WAITING FOR THIS LOCK TO BE GRANTED:  
RECORD LOCKS space id 728897 page no 253458 n bits 448 index `idx_order_asset` waiting  *** (2) TRANSACTION:  
TRANSACTION 17581942880, ACTIVE 64 sec starting index read  
UPDATE business_table SET ready_msg='操作失败:系统繁忙'  
WHERE order_id = 'ORDER_002' AND asset_tag = 'ASSET_ABC'  *** (2) HOLDS THE LOCK(S):  
RECORD LOCKS space id 728897 page no 253458 n bits 448 index `idx_order_asset` lock_mode X locks rec but not gap  *** WE ROLL BACK TRANSACTION (1)  

问题分析

死锁产生的根本原因

通过分析死锁日志,我们发现了问题的核心:

持有
等待
持有
等待
事务A
记录锁1
记录锁2
事务B

两个不同的更新操作

  1. 操作类型1:使用 order_id + server_id 作为WHERE条件
  2. 操作类型2:使用 order_id + asset_tag 作为WHERE条件

代码层面的问题

在业务代码中,我们发现了两个关键的Mapper方法:

// 方法1:更新业务状态  
@Update("UPDATE business_table SET field1=#{field1}, status=#{status} " +  "WHERE order_id = #{orderId} AND server_id = #{serverId}")int updateBusinessStatus(@Param("orderId") String orderId,   @Param("serverId") Integer serverId, ...);  // 方法2:更新准备状态消息  @Update("UPDATE business_table SET ready_msg=#{readyMsg} " +  "WHERE order_id = #{orderId} AND asset_tag = #{assetTag}")int updateReadyMessage(@Param("orderId") String orderId,  @Param("assetTag") String assetTag, ...);  

死锁时序图

事务A事务B数据库开始事务A开始事务BUPDATE WHERE order_id='O1' AND server_id=123获得记录锁(O1, ASSET_X)UPDATE WHERE order_id='O2' AND asset_tag='ASSET_Y'获得记录锁(O2, ASSET_Y)UPDATE WHERE order_id='O2' AND server_id=456等待记录锁(O2, ASSET_Y)UPDATE WHERE order_id='O1' AND asset_tag='ASSET_X'等待记录锁(O1, ASSET_X)循环等待形成,死锁发生!ROLLBACK (死锁牺牲者)继续执行事务A事务B数据库

解决方案设计

核心思路:统一锁获取顺序

死锁的根本原因是不同事务以不同顺序获取锁资源。解决方案是强制所有事务按相同顺序获取锁

graph LR  A[无序访问] -->|可能死锁| B[事务1: 锁A→锁B<br/>事务2: 锁B→锁A]  C[有序访问] -->|不会死锁| D[事务1: 锁A→锁B<br/>事务2: 锁A→锁B]  style A fill:#ff9999  style B fill:#ff9999    style C fill:#99ff99  style D fill:#99ff99  

实现方案

1. 创建统一的更新DTO
@Data  
@Builder  
public class BatchUpdateDto {  private String orderId;  private String assetTag;  // 统一使用assetTag作为定位字段  private Integer updateType; // 1-业务状态更新,2-消息更新  // 业务状态更新字段  private String field1;  private Integer status;  private String stepId;  // 消息更新字段    
private String readyMsg;  
}  
2. 批量更新服务(使用MyBatis BatchExecutor)
@Service  
public class BatchUpdateService {@Autowiredprivate SqlSessionFactory sqlSessionFactory;@Transactionalpublic void updateSafely(List<BatchUpdateDto> updateList) {if (CollectionUtils.isEmpty(updateList)) {return;}// 关键:按orderId + assetTag排序,统一锁获取顺序List<BatchUpdateDto> sortedList = updateList.stream().sorted(Comparator.comparing(BatchUpdateDto::getOrderId).thenComparing(BatchUpdateDto::getAssetTag)).collect(Collectors.toList());// 使用BatchExecutor进行高效批量更新try (SqlSession batchSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {BatchMapper batchMapper = batchSession.getMapper(BatchMapper.class);int batchSize = 50;int count = 0;for (BatchUpdateDto updateDto : sortedList) {batchMapper.updateSingle(updateDto);count++;// 定期提交,避免内存溢出if (count % batchSize == 0) {batchSession.flushStatements();}}// 提交剩余记录if (count % batchSize != 0) {batchSession.flushStatements();}batchSession.commit();}}
}
3. 优化的SQL实现
<!-- 单条更新语句,配合BatchExecutor使用 -->
<update id="updateSingle">UPDATE business_table<set><if test="updateType == 1"><!-- 业务状态更新 --><if test="field1 != null">field1=#{field1},</if><if test="status != null">status=#{status},</if><if test="stepId != null">step_id=#{stepId},</if></if><if test="updateType == 2"><!-- 消息更新 --><if test="readyMsg != null">ready_msg=#{readyMsg},</if></if></set>WHERE order_id = #{orderId} AND asset_tag = #{assetTag}
</update>

改造现有业务代码

改造前(会死锁)
@Service  
public class BusinessService {public void processOrders(List<Order> orders) {// 危险:循环中的单独更新,锁顺序不可控for (Order order : orders) {if (needUpdateStatus(order)) {mapper.updateBusinessStatus(order.getId(), order.getServerId(), ...);}if (needUpdateMessage(order)) {mapper.updateReadyMessage(order.getId(), order.getAssetTag(), ...);}}}
}
改造后(不会死锁)
@Service
public class BusinessService {@Autowiredprivate BatchUpdateService batchUpdateService;public void processOrders(List<Order> orders) {// 收集所有更新操作List<BatchUpdateDto> batchUpdates = new ArrayList<>();for (Order order : orders) {if (needUpdateStatus(order)) {BatchUpdateDto dto = BatchUpdateDto.builder().orderId(order.getId()).assetTag(order.getAssetTag()).updateType(1).status(order.getNewStatus()).build();batchUpdates.add(dto);}if (needUpdateMessage(order)) {BatchUpdateDto dto = BatchUpdateDto.builder().orderId(order.getId()).assetTag(order.getAssetTag()).updateType(2).readyMsg(order.getMessage()).build();batchUpdates.add(dto);}}// 批量安全更新,避免死锁batchUpdateService.updateSafely(batchUpdates);}
}

也可以事先对orders排序,然后保持原来的更新逻辑不变。
核心是统一访问顺序。

原理深度解析

死锁的四个必要条件

graph TD  A[死锁的四个必要条件] --> B[互斥条件<br/>资源不能共享]  A --> C[持有并等待<br/>持有资源的同时等待其他资源]    A --> D[不可剥夺<br/>资源不能被强制释放]  A --> E[循环等待<br/>形成资源等待环路]  B --> F[❌ 无法破除<br/>数据库锁的本质特性]  C --> G[❌ 无法破除<br/>业务逻辑需要]  D --> H[❌ 无法破除<br/>事务ACID特性]  E --> I[✅ 可以破除<br/>通过排序统一访问顺序]  style I fill:#99ff99  style F fill:#ffcccc    style G fill:#ffcccc    style H fill:#ffcccc  

排序解决死锁的数学原理

无序访问的问题

  • N个资源,可能的访问顺序:N! 种
  • 任意两种不同顺序都可能产生死锁
  • 死锁概率随并发度指数增长

有序访问的优势

  • N个资源,访问顺序:1种(固定排序)
  • 不可能形成环路等待
  • 死锁概率:0
graph LR  A[3个资源的访问] --> B[无序访问<br/>3! = 6种可能顺序]  A --> C[有序访问<br/>1种固定顺序]  B --> D[可能的死锁场景:<br/>T1: A→B→C<br/>T2: C→A→B<br/>形成环路等待]  C --> E[不可能死锁:<br/>T1: A→B→C<br/>T2: A→B→C<br/>线性等待]  style D fill:#ff9999  style E fill:#99ff99  

监控指标

监控指标
死锁检测
SHOW ENGINE INNODB STATUS
事务等待时间
innodb_lock_wait_timeout
活跃事务数
INFORMATION_SCHEMA.INNODB_TRX
锁等待情况
performance_schema.data_locks

最佳实践总结

1. 预防死锁的设计原则

  • 统一访问顺序:对所有资源按固定规则排序访问
  • 减少锁粒度:使用行锁而非表锁
  • 缩短事务时间:避免长事务
  • 避免交叉访问:减少不同事务访问相同资源集合

2. 数据库层面优化

-- 优化索引,减少锁范围  
ALTER TABLE business_table ADD INDEX idx_order_asset_optimized (order_id, asset_tag, status);  -- 设置合理的锁等待超时  
SET innodb_lock_wait_timeout = 10;  -- 开启死锁检测  
SET innodb_deadlock_detect = ON;  

结论

通过统一锁获取顺序这一简单而有效的方法,我们彻底解决了MySQL死锁问题。这个方案的核心思想是:

将潜在的循环等待转换为线性等待,从根本上消除死锁的可能性。

这种方法不仅解决了死锁问题,还带来了性能提升,是一个典型的"一石二鸟"的优化方案。在面对类似并发问题时,我们应该优先考虑从设计层面避免问题,其次一定要对数据库进行运行时的检测和恢复机制,将设计中的隐患暴露并解决。

http://www.dtcms.com/a/581286.html

相关文章:

  • shell中获取达梦信息方法示例
  • calibre QRC提取寄生参数
  • 【Hot100 |5-LeetCode 11. 盛最多水的容器】
  • 【MicroPython编程-ESP32篇】-DH11温度湿度传感器驱动
  • 字节deer-flow项目模块详解
  • 【Python】Python并发与并行编程图解
  • 清城网站seodiscuz自适应模板
  • 优秀网页设计网站是wordpress php开发
  • 内部网关协议——OSPF 协议(开放最短路径优先)(链路状态路由协议)
  • rman-08137:warning:archived log not deleted
  • 专业的开发网站建设价格虚拟云电脑
  • [Linux——Lesson21.进程信号:信号概念 信号的产生]
  • 浙江英文网站建设嘉兴高档网站建设
  • ERP与WMS一体化构建方案
  • python+django/flask的眼科患者随访管理系统 AI智能模型
  • 实战案例:用 Guava ImmutableList 优化缓存查询系统,解决多线程数据篡改与内存浪费问题
  • AR短视频SDK,打造差异化竞争壁垒
  • 什么是AR人脸特效sdk?
  • Angular由一个bug说起之二十:Table lazy load:防止重复渲染
  • 从0到1做一个“字母拼词”Unity小游戏(含源码/GIF)- 字母拼词正确错误判断
  • 网站建设自查情况报告做淘宝联盟网站要多少钱?
  • 重新思考 weapp-tailwindcss 的未来
  • RuoYi .net-实现商城秒杀下单(redis,rabbitmq)
  • Langchain 和LangGraph 为何是AI智能体开发的核心技术
  • C++与C#布尔类型深度解析:从语言设计到跨平台互操作
  • 贵阳 网站建设设计企业门户网站
  • Rust 练习册 :Matching Brackets与栈数据结构
  • Java基础——常用算法3
  • 【JAVA 进阶】SpringAI人工智能框架深度解析:从理论到实战的企业级AI应用开发指南
  • 对话百胜软件产品经理CC:胜券POS如何用“一个APP”,撬动智慧零售的万千场景?