前言
重试任务,这个在我们日常工作并不陌生。当调用一个接口超时或者返回不明确的状态码时,我们可以选择尝试重新请求该接口,这也是分布式系统和任务处理中比较常见的容错做法。
snail-job的重试任务可以说是该项目的一大亮点或者特色,其功能和可扩展性无疑在所有重试框架中最全面和最贴心的。一提到重试,我们可能很容易想到SpringRetry
或者GuavaRetry
.但是这些框架都是基于内存重试的,在遇到一些极端情况下,最终一致性可能很难得到保证。snail-job的重试任务,是想提供数据一致性的解决方案。当然,我们也可以完全无视它高大上的功能,把它当内存重试方案也不是不行。
本文会比较详尽的介绍重试注解的各个参数,并且做对应的测试。力求在测试的过程中,对注解的每个参数有个深入的认知。读本文,最好放平心态,嗑着瓜子吃着瓜。
本节测试代码:gitee.com/mayuanfei/s...
本节目标
- 会用
Retryable
注解
开发环境及其依赖
客户端开发环境
-
JDK版本:openjdk-21.0.2
-
snail-job版本:1.5.0-beta1
估计月底1.5.0的正式版本就出来了。
客户端Maven依赖
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- snail-job 客户端依赖 -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-job-client-starter</artifactId>
<version>1.5.0-beta1</version>
</dependency>
<!-- snail-job 重试相关依赖 -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-job-client-retry-core</artifactId>
<version>1.5.0-beta1</version>
</dependency>
<!-- snail-job 客户端核心依赖 -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-job-client-job-core</artifactId>
<version>1.5.0-beta1</version>
</dependency>
</dependencies>
快速使用Retryable注解
这里先来初步感受一下Retryable
注解。我们先用基于内存重试的最简方式,领略下它的用法。这里多说一句,依赖中要有重试的依赖。
xml
<!-- snail-job 重试相关依赖 -->
<dependency>
<groupId>com.aizuda</groupId>
<artifactId>snail-job-client-retry-core</artifactId>
<version>1.5.0-beta1</version>
</dependency>
客户端代码
常量类
java
public class SceneConstant {
/**
* 本地重试场景
*/
public static final String LOCAL_RETRY = "local_retry";
/**
* 远程重试场景
*/
public static final String REMOTE_RETRY = "remote_retry";
/**
* 本地远程重试场景
*/
public static final String LOCAL_REMOTE_RETRY = "local_remote_retry";
}
这个常量类还是建议要有的。便于维护重试任务的场景,后面会对场景做解释。
接口类
java
public interface LocalRetryService {
/**
* 本地重试
* @param params 参数
*/
void localRetry(String params);
}
实现类
java
@Service
public class LocalRetryServiceImpl implements LocalRetryService {
@Override
@Retryable(scene = SceneConstant.LOCAL_RETRY, retryStrategy = RetryType.ONLY_LOCAL)
public void localRetry(String params) {
System.out.println("local retry 方法开始执行");
throw new RuntimeException("local retry 方法执行异常");
}
}
Controller测试类
java
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/local")
public class LocalRetryController {
private final LocalRetryService localRetryService;
@RequestMapping("/retry")
public void localRetry(String params) {
localRetryService.localRetry(params);
}
}
测试观察
-
通过postman或者apifox发送测试请求
-
观察客户端控台输出
shelllocal retry 方法开始执行 2025-04-09 09:01:07 [http-nio-8081-exec-2] INFO c.a.s.c.c.s.ExecutorAnnotationMethod - 执行原重试方法:[com.mayuanfei.service.impl.LocalRetryServiceImpl],参数为:[["123456"]] local retry 方法开始执行 2025-04-09 09:01:07 [http-nio-8081-exec-2] ERROR c.a.s.c.c.s.LocalRetryStrategies$1$1 - [local_retry] 本地重试执行失败,第[1]次重试 2025-04-09 09:01:09 [http-nio-8081-exec-2] INFO c.a.s.c.c.s.ExecutorAnnotationMethod - 执行原重试方法:[com.mayuanfei.service.impl.LocalRetryServiceImpl],参数为:[["123456"]] local retry 方法开始执行 2025-04-09 09:01:09 [http-nio-8081-exec-2] ERROR c.a.s.c.c.s.LocalRetryStrategies$1$1 - [local_retry] 本地重试执行失败,第[2]次重试 2025-04-09 09:01:11 [http-nio-8081-exec-2] INFO c.a.s.c.c.s.ExecutorAnnotationMethod - 执行原重试方法:[com.mayuanfei.service.impl.LocalRetryServiceImpl],参数为:[["123456"]] local retry 方法开始执行 2025-04-09 09:01:11 [http-nio-8081-exec-2] ERROR c.a.s.c.c.s.LocalRetryStrategies$1$1 - [local_retry] 本地重试执行失败,第[3]次重试 2025-04-09 09:01:11 [http-nio-8081-exec-2] INFO c.a.s.c.c.s.LocalRetryStrategies - 内存重试完成且异常未被解决 scene:[local_retry] 2025-04-09 09:01:11 [http-nio-8081-exec-2] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: local retry 方法执行异常] with root cause java.lang.RuntimeException: local retry 方法执行异常
说明:
- 从输出日志中看到,重试了3次
- 重试的时间间隔是2秒
- 很贴心的告诉你
内存重试完成且异常未被解决 scene:[local_retry]
- 最后把异常抛出来了,这里也可以通过参数设置,不进行抛出。后面章节会介绍到。
Retryable注解
在这个注解中,其实就场景scene
是必须指定的,其他都是有默认值的。这里结合上面的本地重试的例子介绍几个平时经常用到的。不过在介绍之前先看下这个注解的全貌:
参数 | 描述 | 默认值 | 必须指定 |
---|---|---|---|
scene | 场景 | 无 | ✅ |
include | 包含的异常 | 无 | ❌ |
exclude | 排除的异常 | 无 | ❌ |
retryStrategy | 重试策略 | LOCAL_REMOTE | ✅ |
retryMethod | 重试处理入口 | RetryAnnotationMethod | ✅ |
idempotentId | 幂等id生成器 | SimpleIdempotentIdGenerate | ✅ |
retryCompleteCallback | 服务端重试完成(重试成功、重试到达最大次数)回调客户端 | SimpleRetryCompleteCallback | ❌ |
isThrowException | 本地重试完成后是否抛出异常 | true | ❌ |
bizNo | 标识具有业务特点的值比如订单号、物流编号等,可根据具体的业务场景生成 | 无 | ❌ |
localTimes | 本地重试次数 次数必须大于等于1 | 3 | ✅ |
localInterval | 本地重试间隔时间(s) | 2 | ✅ |
timeout | 同步(async:false)上报数据需要配置超时时间 | 60 * 1000 | ❌ |
unit | 超时时间单位 | TimeUnit.MILLISECONDS | ❌ |
forceReport | 是否强制上报数据到服务端 | false | ❌ |
async | 是否异步上报数据到服务端 | true | ❌ |
propagation | REQUIRED: 当设置为REQUIRED时,如果当前重试存在,就加入到当前重试中,即外部入口触发重试 如果当前重试不存在,就创建一个新的重试任务。 REQUIRES_NEW:当设置为REQUIRES_NEW时, 无论当前重试任务是否存在,都会一个新的重试任务。 | REQUIRED | ✅ |
场景scene
scene也称场景id,借着解释这个概念的机会,顺便复习下以前说的命名空间和组。我这里按照自己的理解总结如下:
名词 | 自己的理解 |
---|---|
命名空间 | 理解为开发环境。dev、prod等 |
组 | 理解为一个项目。 |
场景 | 理解为针对某个具体接口的重试 |
按照概念大小的顺序:命名空间 > 组 > 场景。这个场景在同一命名空间的同组下不能重复,否则启动时会报错。
重试策略
重试策略主要配置的是你要从本地内存重试还是远程服务端重试。默认值为:LOCAL_REMOTE
策略 | 说明 |
---|---|
ONLY_LOCAL | 本地重试。发生异常时,通过内存方式进行重试 |
ONLY_REMOTE | 远程重试。发生异常时将数据上报到服务端进行重试。这里就进行持久化了 |
LOCAL_REMOTE | 默认策略。先本地重试,再远程重试。即:先在本地进行内存重试N次,如果本地重试未解决,将异常数据上报到服务端进行重试 |
重试次数和重试间隔
样例@Retryable( ...... localTimes = 2, localInterval = 3)
参数 | 说明 |
---|---|
localTimes | 本地重试次数 次数必须大于等于1。默认值:3次 |
localInterval | 本地重试间隔时间(单位:秒)。默认值:2秒 |
远程重试任务
前面介绍的本地重试,基本算是和SpringRetry
或者GuavaRetry
等效的作用,并不能体现它的特长。而远程重试这种方式,就很有其特点了。因为这些任务会被持久化,可以通过管理页面进行监控和更个性化的配置,酷!
客户端代码
接口类
java
public interface RemoteRetryService {
/**
* 远程重试
*
* @param params 参数
*/
void remoteRetry(String params);
}
实现类
java
@Service
public class RemoteRetryServiceImpl implements RemoteRetryService {
@Override
@Retryable(scene = SceneConstant.REMOTE_RETRY, retryStrategy = RetryType.ONLY_REMOTE)
public void remoteRetry(String params) {
System.out.println("remote retry 方法开始执行");
throw new RuntimeException("remote retry 方法执行异常");
}
}
Controller测试类
java
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/remote")
public class RemoteRetryController {
private final RemoteRetryService remoteRetryService;
@RequestMapping("/retry")
public void localRetry(String params) {
remoteRetryService.remoteRetry(params);
}
}
管理端页面
当你没有访问这个测试接口时,服务端管理页面中啥都没有的。必须得触发这个重试接口才行!
调用测试接口,观察客户端输出:

重试场景
在管理页面中
能够看到我们客户端定义为remote_retry的场景名:

这里可以通过点击编辑,来对该场景进行调整。编辑页面:
先对这个编辑页面中的退避策略做一个简单的说明。后续随着理解的深入,再接触更多。
在编辑页面中,这个退避策略是个专有名词,其实就是重试时间间隔的方式。目前包括随机等待、cron表达式、固定时间和延迟等级这4个策略。页面中的间隔时间会随着这个策略的不同而有所变化。
-
随机等待
随机区间是:>=10秒 且 <=你填写的间隔时间,这里是30秒
-
cron表达式
根据cron表达式配置,执行重试任务。
-
固定时间
以固定的时间间隔,执行重试任务
-
延迟等级
延迟等级是参考RocketMQ的messageDelayLevel设计实现最大重试次数为21次,具体延迟时间如下: 【10s,15s,30s,35s,40s,50s,1m,2m,4m,6m,8m,10m,20m,40m,1h,2h,3h,4h,5h,6h,7h,8h,9h,10h,11h,12h】
这里随着你修改最大重试次数,间隔时间逐渐缩短。OK,我们就是要修改这一项。上面知道怎么用就可以了,结合具体情况选用适合的策略。
任务管理

该页面就是需要重试的任务列表。为了在页面上能够显示重试任务,那么就要请求上面客户端接口了。我这里把上面的客户端代码,启动了两个服务。
shell
-Dserver.port=7001 -Dsnail-job.port=7771
-Dserver.port=7002 -Dsnail-job.port=7772
分别请求这两个端口,注意这里的参数值是不同的,后面解释为什么:


过一会就能看到这个列表中显示两个重试任务

在此页面中点击操作-任务。会进入重试任务列表。
重试任务
如果是通过上面的方式进入重试任务的,那么仅仅显示该重试任务的列表。如果是在管理页面中直接点击重试任务,那么会显示所有重试任务的重试列表。可通过重试ID区分是哪个任务的重试列表。

死信任务
当任务管理中的重试任务的状态是最大重试次数
时,默认过7天会自动加入到死信任务列表中,也不会再重试了。如果需要重试,可以通过点击管理中的回滚,将它放回任务管理中,再次执行重试。

说明:
这里通过修改服务端的配置来调整几天后放入到死信任务列中。
重试之我理解
之前的练习中,已经知道如何使用本地重试和远程重试了。而Retryable注解默认的重试策略是LOCAL_REMOTE
,先进行本地重试,全都失败了,那么就会异步上报到服务端,再由服务端调度客户端进行重试。这里如果细心的话,会发现本地重试是同步返回结果的;而远程重试是异步返回结果的。正是由于这种同步异步的差异,所以我们就要结合具体的应用场景来抉择到底选择哪种重试策略。这里我举两个例子适合本地和远程重试的例子。
-
适合本地重试的场景【ONLY_LOCAL】
POS机刷卡,用户等着支付结果呢。这时后台,如果调用第三方接口超时或者返回不明确的错误码时,重试的策略就是本地重试。这个场景的本质就是希望同步返回。
-
适合远程重试的场景【ONLY_REMOTE】
订单支付完成后可能存在一些非核心业务的功能需要通知,比如:积分业务、信用级别等,需要得到这笔订单的某些信息。此时不管使用MQ也好,还是开启单独异步线程处理通知也好。都挺适合用到远程重试的。总之,这种场景的本质是能进行异步的操作。另外我感觉适用远程重试的场景,基本也都适用
LOCAL_REMOTE
。
再识Retryable注解
这里根据本地、远程把注解的参数进行分类。
基础(本地和远程都起效):scene、retryStrategy、retryMethod、propagation、isThrowException、include、exclude
本地:localTimes、localInterval、
远程:idempotentId、retryCompleteCallback、bizNo、async、timeout、unit
这里就逐个把这些参数做一个较深入的了解。
基础
scene
场景这个在快速使用Retryable注解时,已经介绍过。这里提别说明一下,这个名字不能重复。否则客户端启动时会报错。而且会告诉你重复的场景是哪个。如:
shell
2025-04-16 15:29:00 [main] ERROR o.s.boot.SpringApplication
- Application run failed
com.aizuda.snailjob.client.core.exception.SnailRetryClientException: 类:[com.mayuanfei.service.impl.RemoteRetryServiceImpl]中已经存在场景:[remote_retry]
retryStrategy
重试策略在快速使用Retryable注解时,也已经介绍过。这里结合"重试之我理解"一节中自己的理解。要结合具体的业务来决定用什么策略。如果是希望得到同步响应结果,那么用本地重试;异步响应的话基本都可以用ONLY_REMOTE或者LOCAL_REMOTE。
retryMethod
重试方法的入口。默认是通过反射执行注解的那个方法。也可以自己指定要重试的方法。只不过你要通过实现ExecutorMethod
接口。做一个简单的示例应用下这个参数。
-
实现
ExecutorMethod
接口Java@Slf4j @Component public class MyRetryMethod implements ExecutorMethod { @Override public Object doExecute(Object params) { log.info("执行自定义方法。参数值:{}", params); throw new BizException("10002", "测试自定义方法执行异常"); } }
-
注解中使用
Java@Retryable(scene = SceneConstant.LOCAL_RETRY + "3", retryStrategy = RetryType.ONLY_LOCAL, retryMethod = MyRetryMethod.class) @Override public void localRetryMyMethod(String params) { System.out.println("local retry my method 方法开始执行"); throw new RuntimeException("local retry my method 方法执行异常"); }
-
测试结果
propagation
这个是传播方式,很像spring事务的传播方式。这里目前仅支持两种:
-
REQUIRED
如果当前重试存在,就加入到当前重试中;否则创建一个新的重试任务。
-
REQUIRES_NEW
无论当前重试任务是否存在,都会创建一个新的重试任务。
这个对于理解snail-job的重试任务有很大帮助。后面结合测试单独介绍。这里有个印象即可。
isThrowException
是否抛出异常。默认:true。
-
本地-不抛异常
java@Retryable(scene = SceneConstant.LOCAL_RETRY + "2", retryStrategy = RetryType.ONLY_LOCAL, isThrowException = false)
观察客户端控台输出
shell测试异常方法开始执行 2025-04-17 11:25:12 [http-nio-7001-exec-1] INFO c.a.s.c.c.s.ExecutorAnnotationMethod - 执行原重试方法:[com.mayuanfei.service.impl.LocalRetryServiceImpl],参数为:[["2345"]] 测试异常方法开始执行 2025-04-17 11:25:12 [http-nio-7001-exec-1] ERROR c.a.s.c.c.s.LocalRetryStrategies$1$1 - [local_retry2] 本地重试执行失败,第[1]次重试 2025-04-17 11:25:14 [http-nio-7001-exec-1] INFO c.a.s.c.c.s.ExecutorAnnotationMethod - 执行原重试方法:[com.mayuanfei.service.impl.LocalRetryServiceImpl],参数为:[["2345"]] 测试异常方法开始执行 2025-04-17 11:25:14 [http-nio-7001-exec-1] ERROR c.a.s.c.c.s.LocalRetryStrategies$1$1 - [local_retry2] 本地重试执行失败,第[2]次重试 2025-04-17 11:25:16 [http-nio-7001-exec-1] INFO c.a.s.c.c.s.ExecutorAnnotationMethod - 执行原重试方法:[com.mayuanfei.service.impl.LocalRetryServiceImpl],参数为:[["2345"]] 测试异常方法开始执行 2025-04-17 11:25:16 [http-nio-7001-exec-1] ERROR c.a.s.c.c.s.LocalRetryStrategies$1$1 - [local_retry2] 本地重试执行失败,第[3]次重试 2025-04-17 11:25:16 [http-nio-7001-exec-1] INFO c.a.s.c.c.s.LocalRetryStrategies - 内存重试完成且异常未被解决 scene:[local_retry2]
用postman进行模拟请求的,从前端页面看,没有任何异常信息返回。观察客户端输出可以看到没有任何的异常抛出来了,这意味着如果这个方法是个事务方法的话,也没法进行回滚了。这点需要特别注意。相当于给你原来的方法加上了try-catch。
-
本地-抛异常
java@Retryable(scene = SceneConstant.LOCAL_RETRY + "2", retryStrategy = RetryType.ONLY_LOCAL)
观察客户端控台输出
java测试异常方法开始执行 2025-04-17 14:32:32 [http-nio-7001-exec-2] INFO c.a.s.c.c.s.ExecutorAnnotationMethod - 执行原重试方法:[com.mayuanfei.service.impl.LocalRetryServiceImpl],参数为:[["2345"]] 测试异常方法开始执行 2025-04-17 14:32:32 [http-nio-7001-exec-2] ERROR c.a.s.c.c.s.LocalRetryStrategies$1$1 - [local_retry2] 本地重试执行失败,第[1]次重试 2025-04-17 14:32:34 [http-nio-7001-exec-2] INFO c.a.s.c.c.s.ExecutorAnnotationMethod - 执行原重试方法:[com.mayuanfei.service.impl.LocalRetryServiceImpl],参数为:[["2345"]] 测试异常方法开始执行 2025-04-17 14:32:34 [http-nio-7001-exec-2] ERROR c.a.s.c.c.s.LocalRetryStrategies$1$1 - [local_retry2] 本地重试执行失败,第[2]次重试 2025-04-17 14:32:36 [http-nio-7001-exec-2] INFO c.a.s.c.c.s.ExecutorAnnotationMethod - 执行原重试方法:[com.mayuanfei.service.impl.LocalRetryServiceImpl],参数为:[["2345"]] 测试异常方法开始执行 2025-04-17 14:32:36 [http-nio-7001-exec-2] ERROR c.a.s.c.c.s.LocalRetryStrategies$1$1 - [local_retry2] 本地重试执行失败,第[3]次重试 2025-04-17 14:32:36 [http-nio-7001-exec-2] INFO c.a.s.c.c.s.LocalRetryStrategies - 内存重试完成且异常未被解决 scene:[local_retry2] 2025-04-17 14:32:36 [http-nio-7001-exec-2] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: BizException(code=10001, msg=测试异常方法执行异常)] with root cause com.mayuanfei.common.exception.BizException: null ...//省略异常信息
用postman进行模拟请求的,从前端页面看,返回的是code=500系统内部错误。
-
远程-不抛异常
Java@Retryable(scene = SceneConstant.REMOTE_RETRY + "2", retryStrategy = RetryType.ONLY_LOCAL, isThrowException = false)
观察客户端输出【这里简略记录】
java测试异常方法开始执行 2025-04-17 13:18:13 [http-nio-7001-exec-5] INFO c.a.s.c.c.s.LocalRetryStrategies$1$1 - [remote_retry2] 上报服务端执行成功. - 执行原重试方法:[com.mayuanfei.service.impl.RemoteRetryServiceImpl],参数为:[["3456"]] 测试异常方法开始执行 2025-04-17 13:18:45 [snail-retry-dispatcher-1] ERROR c.a.s.c.c.s.RemoteRetryStrategies$1$1 - snail-job 远程重试失败,第[0]次调度 com.mayuanfei.common.exception.BizException: null ...//省略异常信息 - 执行原重试方法:[com.mayuanfei.service.impl.RemoteRetryServiceImpl],参数为:[["3456"]] 测试异常方法开始执行 2025-04-17 13:18:54 [snail-retry-dispatcher-3] ERROR c.a.s.c.c.s.RemoteRetryStrategies$1$1 - snail-job 远程重试失败,第[1]次调度 com.mayuanfei.common.exception.BizException: null ...//省略异常信息
用postman进行模拟请求的,从前端页面看,没有任何异常信息返回。服务端会调用客户端进行重试任务。从这里可以看出,即便是不抛出异常,同样会上报服务端进行重试。
-
远程-抛异常
Java@Retryable(scene = SceneConstant.REMOTE_RETRY + "2", retryStrategy = RetryType.ONLY_REMOTE)
观察客户端输出【这里简略记录】
Java测试异常方法开始执行 2025-04-17 14:40:54 [http-nio-7001-exec-4] INFO c.a.s.c.c.s.LocalRetryStrategies$1$1 - [remote_retry2] 上报服务端执行成功. 2025-04-17 14:40:54 [http-nio-7001-exec-4] ERROR c.m.controller.RemoteRetryController - 远程异常捕获: com.mayuanfei.common.exception.BizException: null ...//省略异常信息 测试异常方法开始执行 2025-04-17 14:41:26 [snail-retry-dispatcher-1] ERROR c.a.s.c.c.s.RemoteRetryStrategies$1$1 - snail-job 远程重试失败,第[0]次调度 com.mayuanfei.common.exception.BizException: null ...//省略异常信息 测试异常方法开始执行 2025-04-17 14:41:35 [snail-retry-dispatcher-3] ERROR c.a.s.c.c.s.RemoteRetryStrategies$1$1 - snail-job 远程重试失败,第[1]次调度 com.mayuanfei.common.exception.BizException: null ...//省略异常信息
用postman进行模拟请求的,从前端页面看,返回的是code=500系统内部错误。
include和exclude
include参数表示仅包含当前的异常才进行重试;exclude参数表示不包含当前的异常才进行重试。观察下面的示例进行理解。
-
本地-include-包含
Java@Retryable(scene = SceneConstant.LOCAL_RETRY + "5", retryStrategy = RetryType.ONLY_LOCAL, include = {BizException.class}) @Override public void localRetryIncludeException(String params) { System.out.println("local retry include exception 方法开始执行"); throw new BizException("10001", "local retry include exception 方法执行异常"); }
观察客户端控台输出
Javalocal retry include exception 方法开始执行 2025-04-17 15:52:55 [http-nio-7001-exec-2] ERROR c.a.s.c.c.s.LocalRetryStrategies$1$1 - [local_retry5] 本地重试执行失败,第[1]次重试 2025-04-17 15:52:57 [http-nio-7001-exec-2] INFO c.a.s.c.c.s.ExecutorAnnotationMethod - 执行原重试方法:[com.mayuanfei.service.impl.LocalRetryServiceImpl],参数为:[["Include"]] local retry include exception 方法开始执行 2025-04-17 15:52:57 [http-nio-7001-exec-2] ERROR c.a.s.c.c.s.LocalRetryStrategies$1$1 - [local_retry5] 本地重试执行失败,第[2]次重试 2025-04-17 15:52:59 [http-nio-7001-exec-2] INFO c.a.s.c.c.s.ExecutorAnnotationMethod - 执行原重试方法:[com.mayuanfei.service.impl.LocalRetryServiceImpl],参数为:[["Include"]] local retry include exception 方法开始执行 2025-04-17 15:52:59 [http-nio-7001-exec-2] ERROR c.a.s.c.c.s.LocalRetryStrategies$1$1 - [local_retry5] 本地重试执行失败,第[3]次重试 2025-04-17 15:52:59 [http-nio-7001-exec-2] INFO c.a.s.c.c.s.LocalRetryStrategies - 内存重试完成且异常未被解决 scene:[local_retry5] 2025-04-17 15:52:59 [http-nio-7001-exec-2] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: BizException(code=10001, msg=local retry include exception 方法执行异常)] with root cause com.mayuanfei.common.exception.BizException: null ...//省略异常信息
-
本地-include-不包含
java@Retryable(scene = SceneConstant.LOCAL_RETRY + "5", retryStrategy = RetryType.ONLY_LOCAL, include = {BizException.class}) @Override public void localRetryIncludeException(String params) { System.out.println("local retry include exception 方法开始执行"); throw new RuntimeException("local retry include exception 方法执行异常"); }
观察客户端控台输出
Javalocal retry include exception 方法开始执行 2025-04-17 15:57:56 [http-nio-7001-exec-1] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: local retry include exception 方法执行异常] with root cause java.lang.RuntimeException: local retry include exception 方法执行异常 ..//省略异常信息
-
远程-include-包含
Java@Retryable(scene = SceneConstant.REMOTE_RETRY + "3" , retryStrategy = RetryType.ONLY_REMOTE , include = {BizException.class}) public void remoteRetryIncludeException(String params) { System.out.println("测试include异常方法开始执行"); throw new BizException("10001", "测试include异常方法开始执行"); }
观察客户端控台输出
Java测试include异常方法开始执行 2025-04-17 16:03:45 [http-nio-7001-exec-2] INFO c.a.s.c.c.s.LocalRetryStrategies$1$1 - [remote_retry3] 上报服务端执行成功. 2025-04-17 16:03:45 [http-nio-7001-exec-2] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: BizException(code=10001, msg=测试include异常方法开始执行)] with root cause com.mayuanfei.common.exception.BizException: null - snail-job 远程重试失败,第[0]次调度 com.mayuanfei.common.exception.BizException: null ..//省略异常信息 - snail-job 远程重试失败,第[1]次调度 com.mayuanfei.common.exception.BizException: null ..//省略异常信息和其他重试信息
-
远程-include-不包含
Java@Retryable(scene = SceneConstant.REMOTE_RETRY + "3" , retryStrategy = RetryType.ONLY_REMOTE , include = {BizException.class}) public void remoteRetryIncludeException(String params) { System.out.println("测试include异常方法开始执行"); throw new RuntimeException("测试include异常方法开始执行"); }
观察客户端控台输出
Java测试include异常方法开始执行 2025-04-17 16:10:35 [http-nio-7001-exec-2] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: 测试include异常方法开始执行] with root cause java.lang.RuntimeException: 测试include异常方法开始执行
在管理端页面的重试任务管理页面中也无此重试任务。
本地
localTimes
本地重试的次数。
localInterval
本地重试的时间间隔。这个是在任务完成之后间隔的时长。单位:秒。注意示例中的时间间隔:
java
@Retryable(scene = SceneConstant.LOCAL_RETRY,
retryStrategy = RetryType.ONLY_LOCAL,
localTimes = 2, localInterval = 3)
public void localRetry(String params) {
System.out.println("local retry 方法开始执行");
ThreadUtil.sleep(4000);
throw new RuntimeException("local retry 方法执行异常");
}
控台输出

远程
idempotentId
幂等id。该参数是用以区别是否为同一个重试任务用的。远程重试任务->管理端页面->任务管理一节中曾特别提到了分别请求这两个端口,注意这里的参数值是不同的,后面解释为什么
。其实就是这个幂等性算法,如果我们参数都一样则会判定为一个重试任务,而不是我们想演示的两个任务。咱们来看看这个默认的幂等id生成类是咋生成的。
java
public class SimpleIdempotentIdGenerate implements IdempotentIdGenerate {
@Override
public String idGenerate(IdempotentIdContext context) throws Exception {
String str = context.toString();
if (StrUtil.isBlankIfStr(str)) {
return StrUtil.EMPTY;
}
return Hashing.md5().hashBytes(str.getBytes(StandardCharsets.UTF_8)).toString();
}
}
从这个默认幂等生成器中,很容易看到是对IdempotentIdContext
上下文类做了一个md5的哈希值。那关键就是这个上下文类的内容了
java
@Data
@AllArgsConstructor
public class IdempotentIdContext {
/**
* 场景名称
*/
private String scene;
/**
* 执行器名称
*/
private String targetClassName;
/**
* 参数列表
*/
private Object[] args;
/**
* 执行的方法名称
*/
private String methodName;
}
这个上下文字符串构成了幂等id,如果这个幂等id相同,不是说就有问题。服务端还要看是不是有处理中
的任务。如果有的话,则不创建了新的重试任务了。当处理中
的任务最终测试完成变成最大重试册数
后,那么这个相同的id又能在重试列表中出现了。我们拿远程重试任务
章节的代码演示下这个过程。
首先参数传的一样
访问传参:http://localhost:7001/remote/retry?params=sameIdempotentId
在服务端重试期间,只能在任务管理中看到一个处理中
的重试任务。

再记忆如下这句话:
同一个组的同一个场景下只会存在一个相同的idempotentId并且状态为'重试中'的任务, 若存在相同的则上报服务后会被幂等处理 。
但是此时,由于你已经知道SimpleIdempotentIdGenerate` 了,所以不信邪,非要相同参数的也能在任务管理中看到多个重试任务。那么就可以自己写个幂等生成器了。
-
实现自定义的幂等生成器
Javapublic class MyIdempotentId implements IdempotentIdGenerate { @Override public String idGenerate(IdempotentIdContext context) throws Exception { String str = context.toString() + IdUtil.fastSimpleUUID(); return Hashing.md5().hashBytes(str.getBytes(StandardCharsets.UTF_8)).toString(); } }
-
参数中使用自己的生成器
Java@Retryable(scene = SceneConstant.REMOTE_RETRY, retryStrategy = RetryType.ONLY_REMOTE, idempotentId = MyIdempotentId.class) public void remoteRetry(String params) { System.out.println("remote retry 方法开始执行"); throw new RuntimeException("remote retry 方法执行异常"); }
-
进行测试
访问Url不变:http://localhost:7001/remote/retry?params=sameIdempotentId
我这里点了3次,观察重试任务->任务管理
retryCompleteCallback
这个参数是用来指定重试完成之后的回调函数。这个主要用于重试逻辑结束后执行的一些自定义操作。比如:
-
通知或者日志记录
如:失败邮件或者告警短信啥的。
-
状态更新
如:标记业务数据状态,需要人工干预。
-
补偿逻辑
如:回滚、清理数据等
在本文前言中就有说过:snail-job的重试任务,是想提供数据一致性的解决方案
。而这个retryCompleteCallback
就是这个一致性很重要的一环,但是要注意这个参数用于有远程重试的场景下。做一个简单的示例应用下这个参数。
-
实现RetryCompleteCallback接口
java@Slf4j @Component public class MyRetryCompleteCallback implements RetryCompleteCallback { /** * 重试成功后的回调 * * @param sceneName 场景名称 * @param executorName 执行器名称 * @param params 参数 */ @Override public void doSuccessCallback(String sceneName, String executorName, Object[] params) { log.info("重试成功后执行自定义方法。参数值:{}", params); } /** * 重试达到最大次数后的回调 * * @param sceneName 场景名称 * @param executorName 执行器名称 * @param params 参数 */ @Override public void doMaxRetryCallback(String sceneName, String executorName, Object[] params) { log.info("重试达到最大次数后执行自定义方法。参数值:{}", params); } }
-
注解中使用
java@Retryable(scene = "retryCompleteCallback", retryStrategy = RetryType.ONLY_LOCAL, retryCompleteCallback = MyRetryCompleteCallback.class) @Override public void localRetryCompleteCallback(String params) { System.out.println("remote retry complete callback 方法开始执行"); throw new RuntimeException("remote retry complete callback 方法执行异常"); }
-
编辑场景
-
测试结果
客户端控台输出:
shellremote retry complete callback 方法开始执行 remote retry complete callback 方法开始执行 remote retry complete callback 方法开始执行 remote retry complete callback 方法开始执行 - 重试达到最大次数后执行自定义方法。参数值:retryCompleteCallback
-
服务端管理页面
可以看到场景中开启了回调的重试任务会有一个子任务进行回调。这个子任务和重试父任务,可以理解成是一个事务。重试达到最大次数时,一定会进行回调的。
bizNo
这个业务编号主要是用于标识和具体业务相关的参数值。比如:订单号,用户id等。也是便于我们在管理端页面便于查看。并且重试失败后,用这个业务编号查库、查生成日志等后续操作。这里特别说明一下,bizNo支持SpEl表达式取值的。来个实例看看:
-
请求对象
java@Data public class OrderBo { /** * 订单号 */ private String orderNo; /** * 订单金额(分) */ private Long orderAmount; }
-
注解中使用
Java@Override @Retryable(scene = "remoteRetryBizNo", retryStrategy = RetryType.ONLY_REMOTE, bizNo = "#request.orderNo") public void remoteRetryBizNo(OrderBo request) { System.out.println("remote retry BizNo 方法开始执行"); throw new RuntimeException("remote retry BizNo 方法执行异常"); }
-
管理端页面
async、timeout和unit
-
async
async用来指定是否异步上报数据到远程重试。默认是true。不建议修改为同步上报。默认异步上报采用滑动窗口的方式,而且通过配置文件能修改这个时间。能很好平衡程序内部流量和性能。本文后面的内容中有配置滑动窗口的内容介绍。
-
timeout
timeout参数指当我们选择同步上报数据时请求的超时时间。默认为1分钟。
-
unit
这个unit和timeout是配套的,仅当同步上报数据时,超时的时间单位。默认毫秒。
再识propagation
一提到这种传播方式,就牵扯到至少两个方法都要有@Retryable
注解,否则也谈不到传播。都是基于aop的注解实现,所以这里就很容易出现注解失效的情况。有兴趣的可以看看我的这篇文章:4.springboot事务-失效的情况 ,了解一下失效的场景。虽然这个注解参数在平时可能都用不到,但是它对理解整个snail-job的重试任务有很大助力,所以分情况加以测试理解。整体的调用路径如图:
基础测试方法如下,都是基于这几个方法通过调整不同的传播叠加方式来进行测试理解。
java
@Retryable(scene = "localRetryPropagation",
retryStrategy = RetryType.ONLY_LOCAL)
@Override
public void localRetryPropagation(String params) {
System.out.println("local retry propagation 方法开始执行");
// 调用A方法
LocalRetryServiceImpl agentThisClass = SpringUtil.getBean(LocalRetryServiceImpl.class);
agentThisClass.localRetryPropagationA(params);
}
@Retryable(scene = "localRetryPropagationA",
retryStrategy = RetryType.ONLY_LOCAL,
propagation = Propagation.REQUIRED)
public void localRetryPropagationA(String params) {
System.out.println("local retry propagation A");
LocalRetryServiceImpl agentThisClass = SpringUtil.getBean(LocalRetryServiceImpl.class);
// 调用B方法
agentThisClass.localRetryPropagationB(params);
}
@Retryable(scene = "localRetryPropagationB",
retryStrategy = RetryType.ONLY_LOCAL,
propagation = Propagation.REQUIRED
)
public void localRetryPropagationB(String params) {
System.out.println("local retry propagation B");
throw new RuntimeException("local retry propagation B");
}
本地 + REQUIRED + REQUIRED
java
入口方法(REQUIRED)->A方法(REQUIRED)->B方法(REQUIRED且抛出异常)
这种也是最常用到的情况,这里客户端打印结果为:
shell
local retry propagation 方法开始执行
local retry propagation A
local retry propagation B
local retry propagation 方法开始执行
local retry propagation A
local retry propagation B
local retry propagation 方法开始执行
local retry propagation A
local retry propagation B
local retry propagation 方法开始执行
local retry propagation A
local retry propagation B
从客户端打印的结果总结: 由于都是都是同一个重试任务。入口方法和A、B方法都试了4次。浏览器会收到500的错误码。下面试试B方法不抛出异常的情况。
java
入口方法(REQUIRED)->A方法(REQUIRED)->B方法(REQUIRED且不抛出异常)
@Retryable(scene = "localRetryPropagationB",
retryStrategy = RetryType.ONLY_LOCAL,
propagation = Propagation.REQUIRED,
isThrowException = false
)
public void localRetryPropagationB(String params) {
System.out.println("local retry propagation B");
throw new RuntimeException("local retry propagation B");
}
客户端打印结果:
同上面的输出结果一样的,这里不再贴出来了。说明是否抛出异常是由入口方法决定的。这里值得注意一下。
浏览器同样会收到500的错误码。
本地 + REQUIRED + REQUIRES_NEW
java
入口方法->A方法(REQUIRED)->B方法(REQUIRES_NEW且抛出异常)
@Retryable(scene = "localRetryPropagationB",
retryStrategy = RetryType.ONLY_LOCAL,
propagation = Propagation.REQUIRES_NEW
)
public void localRetryPropagationB(String params) {
System.out.println("local retry propagation B");
throw new RuntimeException("local retry propagation B");
}
客户端打印结果:
shell
local retry propagation 方法开始执行
local retry propagation A
local retry propagation B
local retry propagation B
local retry propagation B
local retry propagation B
local retry propagation 方法开始执行
local retry propagation A
local retry propagation B
local retry propagation B
local retry propagation 方法开始执行
local retry propagation A
local retry propagation B
local retry propagation B
local retry propagation 方法开始执行
local retry propagation A
local retry propagation B
local retry propagation B
local retry propagation B
local retry propagation B
从客户端打印的结果总结: 入口方法和A方法别重试了4次,而抛出异常的B方法执行了16次。浏览器会收到500的错误码。下面试试B方法不抛出异常的情况。
java
入口方法->A方法(REQUIRED)->B方法(REQUIRES_NEW且不抛出异常)
@Retryable(scene = "localRetryPropagationB",
retryStrategy = RetryType.ONLY_LOCAL,
propagation = Propagation.REQUIRES_NEW,
isThrowException = false
)
public void localRetryPropagationB(String params) {
System.out.println("local retry propagation B");
throw new RuntimeException("local retry propagation B");
}
客户端打印结果为:
shell
local retry propagation 方法开始执行
local retry propagation A
local retry propagation B
local retry propagation B
local retry propagation B
local retry propagation B
从客户端的输出可以看出来,由于创建出来的新重试任务没有抛出异常,所以入口方法和A方法是无法感知到B的异常的。所以这里入口方法和A方法仅仅执行了一次。而B方法执行了4次。并且浏览器收到200的正常返回。
本地 + REQUIRES_NEW + REQUIRES_NEW
Java
入口方法->A方法(REQUIRES_NEW)->B方法(REQUIRES_NEW且抛出异常)
@Retryable(scene = "localRetryPropagationA",
retryStrategy = RetryType.ONLY_LOCAL,
propagation = Propagation.REQUIRES_NEW)
public void localRetryPropagationA(String params) {
System.out.println("local retry propagation A");
LocalRetryServiceImpl agentThisClass = SpringUtil.getBean(LocalRetryServiceImpl.class);
// 调用B方法
agentThisClass.localRetryPropagationB(params);
}
@Retryable(scene = "localRetryPropagationB",
retryStrategy = RetryType.ONLY_LOCAL,
propagation = Propagation.REQUIRES_NEW
)
public void localRetryPropagationB(String params) {
System.out.println("local retry propagation B");
throw new RuntimeException("local retry propagation B");
}
客户端打印结果:
shell
local retry propagation 方法开始执行
local retry propagation A
local retry propagation B
local retry propagation B
local retry propagation B
local retry propagation B
local retry propagation A
local retry propagation B
local retry propagation B
local retry propagation B
local retry propagation B
local retry propagation A
local retry propagation B
local retry propagation B
local retry propagation B
local retry propagation B
local retry propagation A
local retry propagation B
local retry propagation B
local retry propagation B
local retry propagation B
从客户端打印的结果总结: 入口方法仅执行了一次;A方法重试了4次;而抛出异常的B方法执行了16次。浏览器会收到500的错误码。下面试试B方法不抛出异常的情况。
java
@Retryable(scene = "localRetryPropagationB",
retryStrategy = RetryType.ONLY_LOCAL,
propagation = Propagation.REQUIRES_NEW
,isThrowException = false
)
public void localRetryPropagationB(String params) {
System.out.println("local retry propagation B");
throw new RuntimeException("local retry propagation B");
}
客户端打印结果为:
shell
local retry propagation 方法开始执行
local retry propagation A
local retry propagation B
local retry propagation B
local retry propagation B
local retry propagation B
从客户端的输出可以看出来,由于创建出来的新重试任务没有抛出异常,所以入口方法和A方法是无法感知到B的异常的。所以这里入口方法和A方法仅仅执行了一次。而B方法执行了4次。并且浏览器收到200的正常返回。
本地 + REQUIRES_NEW + REQUIRED
Java
入口方法->A方法(REQUIRES_NEW)->B方法(REQUIRED且抛出异常)
@Retryable(scene = "localRetryPropagationA",
retryStrategy = RetryType.ONLY_LOCAL,
propagation = Propagation.REQUIRES_NEW)
public void localRetryPropagationA(String params) {
System.out.println("local retry propagation A");
LocalRetryServiceImpl agentThisClass = SpringUtil.getBean(LocalRetryServiceImpl.class);
// 调用B方法
agentThisClass.localRetryPropagationB(params);
}
@Retryable(scene = "localRetryPropagationB",
retryStrategy = RetryType.ONLY_LOCAL,
propagation = Propagation.REQUIRED
)
public void localRetryPropagationB(String params) {
System.out.println("local retry propagation B");
throw new RuntimeException("local retry propagation B");
}
客户端输出:
shell
local retry propagation 方法开始执行
local retry propagation A
local retry propagation B
local retry propagation A
local retry propagation B
local retry propagation A
local retry propagation B
local retry propagation A
local retry propagation B
local retry propagation 方法开始执行
local retry propagation A
local retry propagation B
local retry propagation A
local retry propagation B
local retry propagation A
local retry propagation B
local retry propagation A
local retry propagation B
local retry propagation 方法开始执行
local retry propagation A
local retry propagation B
local retry propagation A
local retry propagation B
local retry propagation A
local retry propagation B
local retry propagation A
local retry propagation B
local retry propagation 方法开始执行
local retry propagation A
local retry propagation B
local retry propagation A
local retry propagation B
local retry propagation A
local retry propagation B
local retry propagation A
local retry propagation B
从客户端输出可以看到由于A、B两个都合并在A这个新开的重试任务中一起重试。入口方法执行了4次;A和B方法分别被执行了16次。浏览器会收到500的错误码。下面试试B方法不抛出异常的情况。
java
入口方法->A方法(REQUIRES_NEW)->B方法(REQUIRED且不抛出异常)
@Retryable(scene = "localRetryPropagationB",
retryStrategy = RetryType.ONLY_LOCAL,
propagation = Propagation.REQUIRED,
isThrowException = false
)
public void localRetryPropagationB(String params) {
System.out.println("local retry propagation B");
throw new RuntimeException("local retry propagation B");
}
同上面的输出结果一样的,这里不再贴出来了。还是那句是否抛出异常是由入口方法决定。这里A方法开启了新重试,而并每次都加入到当前重试中。浏览器会收到500的错误码。
远程 + REQUIRED + REQUIRED
-
基础代码
java@Retryable(scene = "remoteRetryPropagation", retryStrategy = RetryType.ONLY_REMOTE) @Override public void remoteRetryPropagation(String params) { System.out.println("remote retry propagation 方法开始执行"); // 调用A方法 RemoteRetryServiceImpl agentThisClass = SpringUtil.getBean(RemoteRetryServiceImpl.class); agentThisClass.remoteRetryPropagationA(params); } @Retryable(scene = "remoteRetryPropagationA", retryStrategy = RetryType.ONLY_REMOTE, propagation = Propagation.REQUIRED) public void remoteRetryPropagationA(String params) { System.out.println("remote retry propagation A"); // 调用B方法 RemoteRetryServiceImpl agentThisClass = SpringUtil.getBean(RemoteRetryServiceImpl.class); agentThisClass.remoteRetryPropagationB(params); } @Retryable(scene = "remoteRetryPropagationB", retryStrategy = RetryType.ONLY_REMOTE, propagation = Propagation.REQUIRED) public void remoteRetryPropagationB(String params) { System.out.println("remote retry propagation B"); throw new RuntimeException("remote retry propagation B"); }
通过请求接口,来触发这个远程重试任务。
触发url : http://localhost:7001/remote/retryPropagation?params=retryPropagation
-
场景设置
这里通过编辑修改为最大重试次数 = 3。
客户端输出:
java
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation B
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation B
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation B
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation B
从客户端输出可以看到入口方法、A、B一共执行了4次。浏览器会收到500的错误码。默认客户端会在10秒后上报到服务端。当然这里也可以通过配置文件来设置上报服务端的时长:
yaml
snail-job:
retry:
# 窗口期时长,单位秒
report-sliding-window:
duration: 2
下面试试B方法不抛出异常的情况。
Java
@Retryable(scene = "remoteRetryPropagationB",
retryStrategy = RetryType.ONLY_REMOTE,
propagation = Propagation.REQUIRED,
isThrowException = false)
public void remoteRetryPropagationB(String params) {
System.out.println("remote retry propagation B");
throw new RuntimeException("remote retry propagation B");
}
同上面的输出结果一样的,这里不再贴出来了。也同样说明了是否抛出异常是由入口方法决定的。
浏览器同样会收到500的错误码。
远程 + REQUIRED + REQUIRES_NEW
java
@Retryable(scene = "remoteRetryPropagationB",
retryStrategy = RetryType.ONLY_REMOTE,
propagation = Propagation.REQUIRES_NEW
)
public void remoteRetryPropagationB(String params) {
System.out.println("remote retry propagation B");
throw new RuntimeException("remote retry propagation B");
}
客户端打印结果:
shell
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation B
remote retry propagation B
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation B
remote retry propagation 方法开始执行
remote retry propagation B
remote retry propagation A
remote retry propagation B
remote retry propagation B
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation B
我这里仅启动了一个客户端,而因为B方法的方式为:无论重试任务是否存在,都会创建一个新的重试任务。所以远程会创建两个重试任务,并发的调用这个单客户端。所以看上去打印的有点凌乱。但是不影响统计总调用次数。入口方法、A、B都是执行了4次;而B单独被执行了3次,B总共是7次。浏览器会收到500的错误码。下面再看下B方法不抛出异常的情况。
java
@Retryable(scene = "remoteRetryPropagationB",
retryStrategy = RetryType.ONLY_REMOTE,
propagation = Propagation.REQUIRES_NEW,
isThrowException = false)
public void remoteRetryPropagationB(String params) {
System.out.println("remote retry propagation B");
throw new RuntimeException("remote retry propagation B");
}
客户端打印结果:
shell
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation B
remote retry propagation B
remote retry propagation B
remote retry propagation B
由于B不抛出异常,对于入口程序来说,没有获得任何异常。所以入口方法、A方法执行1次;而B方法执行了4次。浏览器收到200的正常返回。
远程 + REQUIRES_NEW + REQUIRES_NEW
由于我们把任务A也设置为无论重试任务是否存在,都会创建一个新的重试任务。所以我们需要在管理页面中把A任务的场景设置最大常重试次数为3次。否则重试太多看不清。

Java
入口方法(REQUIRED)->A方法(REQUIRES_NEW)->B方法(REQUIRES_NEW且抛出异常)
@Retryable(scene = "remoteRetryPropagationA",
retryStrategy = RetryType.ONLY_REMOTE,
propagation = Propagation.REQUIRES_NEW)
public void remoteRetryPropagationA(String params) {
System.out.println("remote retry propagation A");
// 调用B方法
RemoteRetryServiceImpl agentThisClass = SpringUtil.getBean(RemoteRetryServiceImpl.class);
agentThisClass.remoteRetryPropagationB(params);
}
@Retryable(scene = "remoteRetryPropagationB",
retryStrategy = RetryType.ONLY_REMOTE,
propagation = Propagation.REQUIRES_NEW
)
public void remoteRetryPropagationB(String params) {
System.out.println("remote retry propagation B");
throw new RuntimeException("remote retry propagation B");
}
这个管理页面中重试任务管理就更恐怖了,一个3个重试任务

客户端打印结果:
shell
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation B
remote retry propagation B
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation A
remote retry propagation B
remote retry propagation B
remote retry propagation B
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation B
remote retry propagation A
remote retry propagation B
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation A
remote retry propagation B
remote retry propagation B
remote retry propagation B
看上去依然凌乱,但是不妨碍我们统计执行次数。入口方法:4次;A方法:7次;B方法:10次。浏览器收到500的错误码。下面再看下B方法不抛出异常的情况。
java
@Retryable(scene = "remoteRetryPropagationB",
retryStrategy = RetryType.ONLY_REMOTE,
propagation = Propagation.REQUIRES_NEW,
isThrowException = false)
public void remoteRetryPropagationB(String params) {
System.out.println("remote retry propagation B");
throw new RuntimeException("remote retry propagation B");
}
客户端打印结果:
Java
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation B
remote retry propagation B
remote retry propagation B
remote retry propagation B
入口方法和A方法执行了1次;而B方法执行了4次。浏览器收到200的正常返回。
远程 + REQUIRES_NEW + REQUIRED
Java
入口方法(REQUIRED)->A方法(REQUIRES_NEW)->B方法(REQUIRES且抛出异常)
@Retryable(scene = "remoteRetryPropagationA",
retryStrategy = RetryType.ONLY_REMOTE,
propagation = Propagation.REQUIRES_NEW)
public void remoteRetryPropagationA(String params) {
System.out.println("remote retry propagation A");
// 调用B方法
RemoteRetryServiceImpl agentThisClass = SpringUtil.getBean(RemoteRetryServiceImpl.class);
agentThisClass.remoteRetryPropagationB(params);
}
@Retryable(scene = "remoteRetryPropagationB",
retryStrategy = RetryType.ONLY_REMOTE,
propagation = Propagation.REQUIRED
)
public void remoteRetryPropagationB(String params) {
System.out.println("remote retry propagation B");
throw new RuntimeException("remote retry propagation B");
}
服务端管理页面:

客户端打印结果:
shell
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation B
remote retry propagation A
remote retry propagation 方法开始执行
remote retry propagation B
remote retry propagation A
remote retry propagation B
remote retry propagation A
remote retry propagation B
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation B
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation A
remote retry propagation B
remote retry propagation B
入口方法:4次;A方法和B方法:7次。浏览器收到500的错误码。下面再看下B方法不抛出异常的情况。
Java
@Retryable(scene = "remoteRetryPropagationB",
retryStrategy = RetryType.ONLY_REMOTE,
propagation = Propagation.REQUIRED,
isThrowException = false)
public void remoteRetryPropagationB(String params) {
System.out.println("remote retry propagation B");
throw new RuntimeException("remote retry propagation B");
}
客户端打印结果:
shell
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation B
remote retry propagation A
remote retry propagation 方法开始执行
remote retry propagation B
remote retry propagation A
remote retry propagation B
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation B
remote retry propagation A
remote retry propagation B
remote retry propagation 方法开始执行
remote retry propagation A
remote retry propagation B
remote retry propagation A
remote retry propagation B
入口方法:4次;A方法和B方法:7次。浏览器收到500的错误码。这里的原因还是一样,因为是以入口方法是否抛出异常来最终决定的,而这里B的入口方法是A,而A是抛出异常的。
测试总结
重试策略 | 入口传播 | A传播 | B传播 | B抛异常 | 入口次数 | A次数 | B次数 | 返回code |
---|---|---|---|---|---|---|---|---|
本地 | REQUIRED | REQUIRED | REQUIRED | YES | 4 | 4 | 4 | 500 |
本地 | REQUIRED | REQUIRED | REQUIRED | NO | 4 | 4 | 4 | 500 |
本地 | REQUIRED | REQUIRED | REQUIRES_NEW | YES | 4 | 4 | 16 | 500 |
本地 | REQUIRED | REQUIRED | REQUIRES_NEW | NO | 1 | 1 | 4 | 200 |
本地 | REQUIRED | REQUIRES_NEW | REQUIRES_NEW | YES | 1 | 4 | 16 | 500 |
本地 | REQUIRED | REQUIRES_NEW | REQUIRES_NEW | NO | 1 | 1 | 4 | 200 |
本地 | REQUIRED | REQUIRES_NEW | REQUIRED | YES | 4 | 16 | 16 | 500 |
本地 | REQUIRED | REQUIRES_NEW | REQUIRED | NO | 4 | 16 | 16 | 500 |
远程 | REQUIRED | REQUIRED | REQUIRED | YES | 4 | 4 | 4 | 500 |
远程 | REQUIRED | REQUIRED | REQUIRED | NO | 4 | 4 | 4 | 500 |
远程 | REQUIRED | REQUIRED | REQUIRES_NEW | YES | 4 | 4 | 7 | 500 |
远程 | REQUIRED | REQUIRED | REQUIRES_NEW | NO | 1 | 1 | 4 | 200 |
远程 | REQUIRED | REQUIRES_NEW | REQUIRES_NEW | YES | 4 | 7 | 10 | 500 |
远程 | REQUIRED | REQUIRES_NEW | REQUIRES_NEW | NO | 1 | 1 | 4 | 200 |
远程 | REQUIRED | REQUIRES_NEW | REQUIRED | YES | 4 | 7 | 7 | 500 |
远程 | REQUIRED | REQUIRES_NEW | REQUIRED | NO | 4 | 7 | 7 | 500 |
手动创建远程重试任务
这节作为扩展知识点了解一下,在平时可能不太能用能到。
-
重试方法
java@Slf4j @ExecutorMethodRegister(scene = MyHandRetryMethod.SCENE) public class MyHandRetryMethod implements ExecutorMethod { /** * 自定义场景值 */ public final static String SCENE = "myHandRetryMethod"; @Override public Object doExecute(Object params) { log.info("执行手动重试方法。参数值:{}", params); return true; } }
@ExecutorMethodRegister注解中的参数作用和@Retryable中的参数作用一致
-
服务类
java@Component public class HandRetryMethodService { public void handRetryMethod(String params) { System.out.println("handRetryMethod"); SnailJobTemplate snailJobTemplate = RetryTaskTemplateBuilder.newBuilder() // 手动指定场景名称 .withScene(MyHandRetryMethod.SCENE) // 指定要执行的任务 .withExecutorMethod(MyHandRetryMethod.class) // 指定参数 .withParam("handRetryMethod") .build(); // 执行模板 snailJobTemplate.executeRetry(); } }
总结
写本文也着实耗费了老夫20年的功力。这里从测试练习中感悟,做最后的总结:
-
scene参数不能重复,重复了启动报错,服务启动不成功。
-
retryStrategy重试策略默认是
LOCAL_REMOTE
先本地重试,再远程重试。 -
本地重试用于需要同步等待的情况;而远程重试用于异步场景。
-
propagation传播方式默认如果当前重试存在,就加入到当前重试中;否则创建一个新的重试任务。
如果是本地重试并且传播方式为REQUIRES_NEW,会重试很多次。那么浏览器那边会等很久很久,这个要考虑实际的应用场景。
-
idempotentId幂等id,用以区别是否为同一个重试任务。参数相同很容易幂等。这个在实际情况要注意。
-
通过实现IdempotentIdGenerate接口,可以定义自己的幂等生成器。
-
retryCompleteCallback用于重试完成之后的回调函数,特别适合做最终一致性的回滚操作或者是补偿性操作。
-
bizNo很适合在服务端管理页面中查看某个具体的重试任务
-
async不要设置成同步
-
通过snail-job.retry.report-sliding-window.duration可以设置异步上报的时间