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)
问题分析
死锁产生的根本原因
通过分析死锁日志,我们发现了问题的核心:
两个不同的更新操作:
- 操作类型1:使用
order_id + server_id作为WHERE条件 - 操作类型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, ...);
死锁时序图
解决方案设计
核心思路:统一锁获取顺序
死锁的根本原因是不同事务以不同顺序获取锁资源。解决方案是强制所有事务按相同顺序获取锁。
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
监控指标
最佳实践总结
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死锁问题。这个方案的核心思想是:
将潜在的循环等待转换为线性等待,从根本上消除死锁的可能性。
这种方法不仅解决了死锁问题,还带来了性能提升,是一个典型的"一石二鸟"的优化方案。在面对类似并发问题时,我们应该优先考虑从设计层面避免问题,其次一定要对数据库进行运行时的检测和恢复机制,将设计中的隐患暴露并解决。
