Spring Retry死锁场景:如何控制服务内线程的重试逻辑?
解决方案:基于Spring Retry管控异步线程的重试逻辑
核心思路是将异步任务的执行状态与重试上下文绑定,确保死锁重试时能根据前一次执行的任务触发状态决定是否重复执行。以下是具体落地方案:
1. 用ThreadLocal存储重试上下文标记
在重试入口处初始化ThreadLocal变量,标记当前重试轮次中已触发的异步任务。死锁抛出后,Spring Retry重新执行重试方法时,读取标记跳过已执行过的任务。
// 存储已执行任务标识的ThreadLocal private static final ThreadLocal<Set<String>> EXECUTED_TASKS = ThreadLocal.withInitial(HashSet::new); // 带重试注解的业务方法 @Retryable(retryFor = DeadlockLoserDataAccessException.class) public void processWithRetry() { Set<String> executedTasks = EXECUTED_TASKS.get(); try { // 前置业务逻辑 // 执行异步任务前先校验标记 if (!executedTasks.contains("send_email_task")) { asyncTaskExecutor.execute(() -> sendEmail()); executedTasks.add("send_email_task"); } // 可能触发死锁的数据库操作 updateDatabase(); } catch (Exception e) { // 非死锁异常时清空标记,避免干扰后续正常执行 if (!(e instanceof DeadlockLoserDataAccessException)) { EXECUTED_TASKS.remove(); } throw e; } finally { // 重试成功完成后清理标记 if (!RetrySynchronizationManager.getContext().isRetryActive()) { EXECUTED_TASKS.remove(); } } }
2. 结合Spring Retry上下文传递状态
利用RetrySynchronizationManager获取当前重试上下文,将任务执行状态存入上下文属性,实现重试轮次间的状态共享。
@Retryable(retryFor = DeadlockLoserDataAccessException.class) public void processWithRetry() { RetryContext context = RetrySynchronizationManager.getContext(); Set<String> executedTasks = (Set<String>) context.getAttribute("executed_tasks"); // 首次执行时初始化状态集合 if (executedTasks == null) { executedTasks = new HashSet<>(); context.setAttribute("executed_tasks", executedTasks); } // 校验并执行异步任务 if (!executedTasks.contains("send_email_task")) { asyncTaskExecutor.execute(() -> sendEmail()); executedTasks.add("send_email_task"); } // 数据库操作(死锁风险点) updateDatabase(); }
3. 异步任务与重试事务解耦
如果异步任务无需和主业务事务绑定,可将异步任务触发时机放在死锁风险操作之后,这样死锁重试时不会重复执行异步任务。若必须在死锁操作前执行,则采用上述状态标记方案。
4. 自定义RetryListener封装状态管理
实现RetryListener接口,在重试生命周期的开始/结束阶段统一管理任务执行状态,避免业务代码混入过多重试逻辑:
@Component public class TaskTrackingRetryListener implements RetryListener { @Override public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) { // 初始化任务执行标记集合 context.setAttribute("executed_tasks", new HashSet<String>()); return true; } @Override public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) { // 重试结束后清理状态 context.removeAttribute("executed_tasks"); } }
在重试方法中指定该监听器:
@Retryable(retryFor = DeadlockLoserDataAccessException.class, listeners = "taskTrackingRetryListener") public void processWithRetry() { // 业务逻辑... }
关键注意事项
- 同步重试场景下,ThreadLocal和RetryContext的状态存储都是线程安全的,因为重试复用当前线程;若使用异步重试,需将状态存入Redis等共享存储。
- 幂等性异步任务(如可重复发送的邮件)可跳过管控,但非幂等任务必须严格校验执行状态。
内容的提问来源于stack exchange,提问作者Mark Park
相关产品推荐
相关产品推荐

