数据库死锁处理与重试机制实现指南
1. 业务场景
1.1 问题现象
- 高并发批量数据处理时频繁出现数据库死锁
- 主要发生在"先删除历史数据,再重新计算"的业务流程中
- 原有逐条处理方式:
list.forEach(item -> { delete(); calculate(); })
1.2 死锁原因分析
- 锁竞争:多个线程同时对相同数据进行删除和插入操作
- 事务时间过长:删除和计算在同一事务中,持锁时间长
- 锁升级:行锁升级为表锁,增加死锁概率
2. 改造步骤
2.1 添加依赖
在 pom.xml
中添加Spring Retry相关依赖:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
2.2 创建重试配置类
@Configuration
@EnableRetry
public class RetryConfig {
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate();
// 重试策略:最多重试3次
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(3);
retryTemplate.setRetryPolicy(retryPolicy);
// 退避策略:指数退避,初始延迟1秒
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(1000);
backOffPolicy.setMultiplier(2.0);
backOffPolicy.setMaxInterval(5000);
retryTemplate.setBackOffPolicy(backOffPolicy);
return retryTemplate;
}
}
2.3 启用重试机制
在主应用类上添加 @EnableRetry
注解:
@EnableRetry
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2.4 改造核心处理逻辑
改造前(容易死锁):
public void processData(List<DataDto> list, QueryParam param) {
// 逐条处理:删除 + 计算
list.forEach(item -> {
deleteRelatedData(item.getId()); // 单条删除
calculateData(item); // 单条计算
});
}
改造后(两阶段处理):
public void processData(List<DataDto> list, QueryParam param) {
if (CollectionUtils.isEmpty(list)) {
return;
}
// 第一阶段:范围删除(有重试机制)
deleteDataByRange(param.getType(), param.getCategory(),
param.getStartTime(), param.getEndTime(),
param.getTempId());
// 第二阶段:批量计算(纯计算,无删除操作)
for (DataDto item : list) {
calculateData(item);
}
}
2.5 在关键方法上添加重试注解
@Retryable(
value = {DeadlockLoserDataAccessException.class, DataAccessException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 5000)
)
public void deleteDataByRange(String type, String category,
LocalDateTime startTime, LocalDateTime endTime, String tempId) {
log.info("开始范围删除数据:type={}, category={}", type, category);
dataMapper.deleteByRange(type, category, startTime, endTime, tempId);
log.info("范围删除完成");
}
@Recover
public void recoverFromDeadlock(Exception ex, String type, String category,
LocalDateTime startTime, LocalDateTime endTime, String tempId) {
log.error("删除数据重试失败,最终放弃。参数:type={}, category={}",
type, category, ex);
throw new BusinessException("数据删除失败,请稍后重试");
}
2.6 移除冗余删除操作
检查并移除业务流程中的冗余删除调用:
public void businessProcess(ProcessParam param) {
// 移除冗余的删除调用
// deleteRelatedData(param); // 删除这行
// 保留必要的删除操作
deleteSpecificTypeData(param);
// 业务计算逻辑
processBusinessLogic(param);
}
3. 关键改造点总结
3.1 核心改造思路
- 分离删除和计算:避免在同一循环中进行删除和计算
- 范围删除替代逐条删除:减少数据库操作次数和锁竞争
- 添加重试机制:对不可避免的死锁进行自动重试
- 清理冗余操作:移除不必要的删除调用
3.2 改造前后对比
改造前 | 改造后 |
---|---|
逐条删除 + 计算 | 范围删除 + 批量计算 |
长事务持锁 | 短事务快速释放锁 |
无重试机制 | 自动重试死锁异常 |
多处冗余删除 | 精简删除操作 |
3.3 效果验证
- 死锁发生频率显著降低
- 数据处理性能提升
- 系统稳定性增强
- 无数据丢失问题
4. 注意事项
4.1 重试配置要点
- 只对特定异常类型重试(如死锁异常)
- 设置合理的重试次数和间隔
- 必须提供
@Recover
方法处理最终失败
4.2 两阶段处理要点
- 确保删除和计算之间没有其他操作干扰
- 删除操作要支持范围查询
- 计算逻辑要保证幂等性
4.3 数据一致性保证
- 关键保存操作不能遗漏
- 事务边界要合理设置
- 必要时使用分布式锁