目录
前言
Spring-Retry(重试机制)
一、spring-retry是什么?
二、引入依赖
三、启用@Retryable
四、在方法上添加@Retryable
五、单元测试
六、@Retryable注解中参数的含义:
Guava Retry
一、guava-retrying 是什么
二、引入依赖
三、构建retryer
四、 主逻辑放在callable里,传给retryer进行调用
五、guava-retrying支持多种条件下重试,来具体看看。
retryIfException()
retryIfRuntimeException()
retryIfExceptionOfType
当我们单体应用时,所有的逻辑计算都在单一的进程中,除了进程断电外几乎不可能有处理失败的情况。然而,当我们把单体应用拆分为一个个细分的子服务后,服务间的互相调用无论是RPC还是HTTP,都是依赖于网络。
网络是脆弱的,不时请求会出现抖动失败。例如我们的 网关Gateway 调用 订单微服务 进行下单时,可能网络超时了,这个时候 网关Gateway 就需要返回给用户提示「网络错误」,这样我们的服务质量就下降了,可能会收到用户的投诉吐槽,降低产品竞争力。
对于网络抖动这种情况,解决的最简单办法之一就是重试。
Spring-Retry(重试机制)
在实际工作中,重处理是一个非常常见的场景,比如:
发送消息失败,保存Redis失败。
调用远程服务失败。
支付成功回调失败。
这些错误可能是因为网络波动造成的,等待过后重处理就能成功。通常来说,会用try/catch,while循环之类的语法来进行重处理,但是这样的做法缺乏统一性,会侵入业务代码,难以维护。spring-retry却可以通过注解,在不入侵原有业务逻辑代码的方式下,优雅的实现重处理功能,使业务代码和重处理解耦。
一、spring-retry是什么?
spring系列的spring-retry是另一个实用程序模块,可以帮助我们以标准方式处理任何特定操作的重试。在spring-retry中,所有配置都是基于注解的。
二、引入依赖
基于AOP实现需引入aop相关的依赖
org.springframework.retry
spring-retry
1.2.4.RELEASE
org.springframework.boot
spring-boot-starter-aop
2.3.4.RELEASE
三、
启用@Retryable
/**
* 启动类
*
* @author yangyanping
* @date 2022-11-17
*/
@Slf4j
@EnableRetry
@EnableDynamicConfigEvent
@EnableTransactionManagement
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class WebApplication {
public static void main(String[] args) {
SpringApplication.run(WebApplication.class, args);
log.info(">>>>>>>>>>>workId={}", System.getProperty("workId"));
}
}
四、
在方法上添加@Retryable
定义接口UserService
public interface UserService {
String getUserInfo(String userId);
}
定义接口实现类 UserServiceImpl ,使用@Retryable
@Slf4j
@Service
public class UserServiceImpl implements UserService {
/**
* value:抛出指定异常才会重试
* include:和value一样,默认为空,当exclude也为空时,默认所有异常
* exclude:指定不处理的异常
* maxAttempts:最大重试次数,默认3次
* backoff:重试等待策略,
* 默认使用@Backoff,@Backoff的value默认为1000L,我们设置为2000; 以毫秒为单位的延迟(默认 1000)
* multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。
*/
@Override
@Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 1.5))
public String getUserInfo(String userId) {
log.info("getUserInfo#userId={}", userId);
throw ExceptionFactory.bizException("-1", "接口异常");
}
/**
* Spring-Retry还提供了@Recover注解,用于@Retryable重试失败后处理方法。
* 如果不需要回调方法,可以直接不写回调方法,那么实现的效果是,重试次数完了后,如果还是没成功没符合业务判断,就抛出异常。
* 可以看到传参里面写的是 Exception e,这个是作为回调的接头暗号(重试次数用完了,还是失败,我们抛出这个Exception e通知触发这个回调方法)。
* 注意事项:
* 方法的返回值必须与@Retryable方法一致
* 方法的第一个参数,必须是Throwable类型的,建议是与@Retryable配置的异常一致,其他的参数,需要哪个参数,写进去就可以了(@Recover方法中有的)
* 该回调方法与重试方法写在同一个实现类里面
*
* 由于是基于AOP实现,所以不支持类里自调用方法
* 如果重试失败需要给@Recover注解的方法做后续处理,那这个重试的方法不能有返回值,只能是void
* 方法内不能使用try catch,只能往外抛异常
*
* @param e
* @param userId
* @return
* @Recover注解来开启重试失败后调用的方法(注意,需跟重处理方法在同一个类中),此注解注释的方法参数一定要是@Retryable抛出的异常,否则无法识别,可以在该方法中进行日志处理。
*/
@Recover
public String recover(Exception e, String userId) {
log.info("回调方法执行,recover#userId={}",userId);
//记日志到数据库 或者调用其余的方法
log.info("异常信息:" + e.getMessage());
return userId;
}
}
五、单元测试
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = WebApplication.class)
public class BaseTest {
@Resource
private UserService userService;
@Test
public void getUserInfo(){
userService.getUserInfo("123");
}
}
运行结果:
[2023/07/26 20:21:13.948][INFO][UserServiceImpl:25] getUserInfo#userId=123
[2023/07/26 20:21:15.950][INFO][UserServiceImpl:25] getUserInfo#userId=123
[2023/07/26 20:21:18.955][INFO][UserServiceImpl:25] getUserInfo#userId=123
[2023/07/26 20:21:18.958][INFO][UserServiceImpl:50] 回调方法执行,recover#userId=123
[2023/07/26 20:21:18.958][INFO][UserServiceImpl:52] 异常信息:接口异常
六、@Retryable
注解中参数的含义:从中挑出一些重要的属性说明
属性名称 | 说明 |
---|---|
interceptor | 重试方法使用的重试拦截器bean名称,和其他的属性互斥(哪个优先待确认) |
value | 哪些异常可以触发重试 ,是include的同义词,复制将会应用到include,默认为空 |
include | 哪些异常可以触发重试 ,默认为空 |
exclude | 哪些异常将不会触发重试,默认为空,如果和include属性同时为空,则所有的异常都将会触发重试 |
stateful | 是否是无状态 |
maxAttempts | 重试策略之最大尝试次数,默认3次 |
maxAttemptsExpression | 字面意思是使用表达式来提供最大重试次数,默认3次 |
backoff | 重试等待策略,默认使用@Backoff,@Backoff的value默认为1000(单位毫秒),我们设置为2000;multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒 |
Spring-Retry还提供了@Recover注解,用于@Retryable重试失败后处理方法。如果不需要回调方法,可以直接不写回调方法,那么实现的效果是,重试次数完了后,如果还是没成功没符合业务判断,就抛出异常。
@Recover
public String recover(Exception e, String userId) {
log.info("回调方法执行,recover#userId={}",userId);
//记日志到数据库 或者调用其余的方法
log.info("异常信息:" + e.getMessage());
return userId;
}
一、
guava-retrying 是什么guava-retrying是Google Guava库的一个扩展包,可以为任意函数调用创建可配置的重试机制。该扩展包比较简单,大约包含了10个方法和类
二、引入依赖
com.github.rholder
guava-retrying
2.0.0
三、
构建retryer Retryer是最核心的类,是用于执行重试策略的类,通过RetryerBuilder类进行构造,并且RetryerBuilder负责将设置好的重试策咯添加到Retryer中,最终通过执行Retryer的核心方法call来执行重试策略(一次任务的执行是如何进行的?)
Retryer retryer = RetryerBuilder.newBuilder()
.retryIfException()
.withStopStrategy(StopStrategies.stopAfterAttempt(5))
.withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
.build();
四、
主逻辑放在callable里,传给retryer进行调用public class RetryTest {
public static void main(String[] args) {
mock();
}
private static Retryer retryer = RetryerBuilder.newBuilder()
.retryIfException()
.withStopStrategy(StopStrategies.stopAfterAttempt(5))
.withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
.build();
public static int mock() {
Callable callable =() -> doQuery();
int result;
try {
result = retryer.call(callable);
} catch (Exception e) {
result = -1;
}
return result;
}
private static int doQuery() {
Random r = new Random(System.currentTimeMillis());
int num = r.nextInt(5);
System.out.println("query result " + num);
if (num == 0) {
return 0;
} else if (num == 1) {
System.out.println("DBException");
throw ExceptionFactory.bizException("-1", "bizException");
} else if (num == 2) {
System.out.println("IllegalArgumentException");
throw new IllegalArgumentException("IllegalArgumentException");
} else if (num == 3) {
System.out.println("NullPointerException");
throw new NullPointerException("NullPointerException");
} else {
System.out.println("IndexOutOfBoundsException");
throw new IndexOutOfBoundsException("IndexOutOfBoundsException");
}
}
}
运行结果
query result 3
NullPointerException
query result 0进程已结束,退出代码0
五、
guava-retrying支持多种条件下重试,来具体看看。这个就是在任何异常发生时,都会进行重试。上面的例子中已经用到。
这个是指,只有runtime exception发生时,才会进行重试。
发生某种指定异常时,才重试。例如
.retryIfExceptionOfType(DBException.class)