8.snail-job的重试任务

前言

​ 重试任务,这个在我们日常工作并不陌生。当调用一个接口超时或者返回不明确的状态码时,我们可以选择尝试重新请求该接口,这也是分布式系统和任务处理中比较常见的容错做法。

​ 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发送测试请求

  • 观察客户端控台输出

    shell 复制代码
    local 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 方法执行异常");
    }

    观察客户端控台输出

    Java 复制代码
    local 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 方法执行异常");
    }

    观察客户端控台输出

    Java 复制代码
    local 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` 了,所以不信邪,非要相同参数的也能在任务管理中看到多个重试任务。那么就可以自己写个幂等生成器了。

  • 实现自定义的幂等生成器

    Java 复制代码
    public 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 方法执行异常");
    }
  • 编辑场景

  • 测试结果

    客户端控台输出:

    shell 复制代码
    remote 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可以设置异步上报的时间

相关推荐
撸猫7911 小时前
HttpSession 的运行原理
前端·后端·cookie·httpsession
嘵奇1 小时前
Spring Boot中HTTP连接池的配置与优化实践
spring boot·后端·http
镜舟科技1 小时前
湖仓一体架构在金融典型数据分析场景中的实践
starrocks·金融·架构·数据分析·湖仓一体·物化视图·lakehouse
Ramseyuu2 小时前
Mybatis-plus
微服务·云原生·架构
子燕若水2 小时前
Flask 调试的时候进入main函数两次
后端·python·flask
程序员爱钓鱼2 小时前
跳转语句:break、continue、goto -《Go语言实战指南》
开发语言·后端·golang·go1.19
charlie1145141912 小时前
内核深入学习3——分析ARM32和ARM64体系架构下的Linux内存区域示意图与页表的建立流程
linux·学习·架构·内存管理
Persistence___3 小时前
SpringBoot中的拦截器
java·spring boot·后端
嘵奇3 小时前
Spring Boot 跨域问题全解:原理、解决方案与最佳实践
java·spring boot·后端
堕落年代3 小时前
SpringBoot的单体和分布式的任务架构
spring boot·分布式·架构