日常编码中,经常会遇到这样的场景:执行一次接口调用,如RPC调用,偶现失败,原因可能是接口超时、连接数耗尽、http网络抖动等,出现异常时我们并不能立即知道原因并作出反应,可能只是一个普通的RpcException或RuntimeException,这时候,我们最常见的做法就是重试,本文将和大家介绍一下如何正确实现重试。
一般我们处理偶发异常的流程如下:异常时,
(1)循环的进行远程调用若干次数,记录一下调用失败的记录;
(2)休眠一段时间,尝试等待下游系统自恢复或释放连接数,继续循环调用失败的请求;
(3)如果再调用失败、通过人工二次调用进行修复;
技术思想源于生活,让我们从恋爱中体会下如何设计一套优雅的重试机制:
1、从恋爱行为中获取反馈,成功的或是失败的(异常)
2、根据不同的反馈决定要不要继续发展(重试)
3、不存在没有异常的代码,也没有一直情绪稳定的伴侣,相识不易偶尔有摩擦要懂得包容和理解(重试)。当然磨合不了也不能无休止的纠缠,要给予彼此尊重和理解。要有底线,程序也要有兜底逻辑
4、多说一句哈也许两个人的解决不了的问题,是不是可以寻求更专业的第三方解决呢?所以重试到最后也可以考虑第三方的介入看是否能解决~~~
回到代码我们也同样会思考:
因为某些失败可能是因为网络短暂出现了问题,只要重试一次或者较少次数就可以成功了,没必要人工介入。
对于因为缺少关键参数导致业务处理不下去,或者其他原因导致业务处理不下去的情况,即使重试一万次也是失败,所以我们不能无限制的重试下去,这也是必须要加上重试次数的原因!
再举个例子。
我们在项目中应该遇到过通过HttpClient调用第三方接口的场景,当你调用对方的接口失败之后,是直接抛出异常呢?还是再次请求呢?如果是再次请求,使用的专门的重试框架,还是简单一个for循环重试呢?
try {
// 业务逻辑
} catch (Exception e) {
for (int i = 0; i < 3; i++) {
// 重试
}
}
如果是上面这种方式,我只能说太low了,这种方式不但将重试的次数固定死了,而且每次都要写一个for循环,太繁琐。
所谓术业有专攻,专业的人干专业的事,重试就应该交给重试框架。下面为大家介绍我常用的重试机制的工具:spring-retry (当然还有guava-retrying,我们在这一篇文章中暂不做说明)
Talk is cheap Show me the code
想要使用这个重试框架,需要如下依赖:
org.springframework.retry
spring-retry
1.3.4
并在项目的启动类上开启重试功能:
@EnableRetry
@EnableAspectJAutoProxy
@SpringBootApplication
public class RetryDemoApplication {
public static void main(String[] args) {
SpringApplication.run(RetryDemoApplication.class, args);
}
}
Retryable注解
标注一个方法被重试。
recover属性
指定重试失败时的兜底处理方法的方法名。
该方法必须被Recover注解标注,且必须和重试方法在同一个类中。
interceptor属性
重试方法应用的拦截器的bean名称。
与exclude互斥。
value属性
需要进行重试的异常,和includes意思相同。
也就是说,我们可以指定抛出哪些异常时,进行重试。
默认为空(如果excludes也为空,则重试所有异常)。
include属性
需要进行重试的异常,和value意思相同。
也就是说,我们可以指定抛出哪些异常时,进行重试。
默认为空(如果excludes也为空,则重试所有异常)。
exclude属性
指定不需要重试的异常。
如果有些异常是我们不想要重试的,可以通过这个属性指定这些异常。
默认为空(如果include也为空,则重试所有异常)。
如果include为空但exclude为非空,则重试所有未排除的异常。
label属性
为了统计重试相关信息,我们可以给每个重试方法提供一个唯一的标签。
如果没有提供,调用者可以选择忽略它,或者提供默认值。
stateful属性
重试是否有状态。
会重新抛出异常,但使用相同的策略将重试策略应用于具有相同参数的后续调用。
如果为false,则不会重新引发可重试的异常。
注意!!!
远程方法调用的时候不需要设置本属性,因为远程方法调用不涉及事务。
但是,当异常引发事务失效的时候需要注意这个属性。
在涉及到数据库更新操作的时候需要设置该属性值为true,抛出异常时,异常会往外抛,导致事务回滚,重试的时候会启用一个新的事务。
maxAttempts属性
指定最大重试次数(包括第一次失败)。
默认是3次。
maxAttemptsExpression属性
通过表达式指定最大重试次数(包括第一次失败)。
默认是3次。
类似,maxAttemptsExpression ="${retry.attempts}"等等
backoff属性
回避策略,默认为空。
该参数为空时,表示失败立即重试,重试的时候会阻塞线程。
exceptionExpression属性
异常处理表达式。
ExpressionRetryPolicy中使用,执行完父类的 canRetry(SimpleRetryPolicy.canRetry) 之后,需要校验 exceptionExpression 的值,为 true 则可以重试,才会触发重试机制。
可以通过这个属性指定表达式用于有条件地抑制重试。
如果抛出多个异常,只会检查最后那个。
可以在exceptionExpression中指定其他Bean。
例如,"message.contains('you can retry this')"、 "@someBean.shouldRetry(#root)"。
listeners属性
指定要使用的重试侦听器的Bean名称,而不是Spring上下文中定义的默认名称。
Backoff注解
重试回退策略,失败后立即重试还是等一会再重试。
value属性
失败后延迟(等待)重试的时间(单位:毫秒)。
默认值是1000毫秒。
和delay属性意思相同。
如果delay不是0,则以delay为准。如果delay是0,则以value为准。
当没有设置multiplier时,表示每隔value毫秒重试,直到重试次数到达maxAttempts设置的最大允许重试次数。当设置了multiplier参数时,该值作为幂运算的初始值。
delay属性
同value属性。
maxDelay属性
重试之间的最大等待时间(毫秒)。
如果该值小于delay,则默认值是30000。
multiplier属性
指定延迟的倍数。
如果为正,作为乘数用于计算下次延迟的时间。
计算公式:当前延迟时间= 上一次延迟时间 * multiplier。
delayExpression属性
指定延迟时间的表达式。
maxDelayExpression属性
指定最大延迟时间的表达式。
multiplierExpression属性
指定multiplier的表达式。
random属性
是否启用随机退避策略。
默认false。
设置为true时表示启用随即退避策略,重试延迟时间将是介于delay和maxDelay间的一个随机数。
设置该参数的目的是重试的时候避免同时发起重试请求,造成DDOS攻击。
randomExpression属性
指定启用随机退避策略的表达式。
Recover注解
标注重试失败时的兜底方法。
用于在全部重试失败时,进行兜底处理。
返回值类型必须和@Retryable修饰的方法返回值类型完全一样。
且被本注解标注的方法的第一个参数必须是Throwable 或者其子类,其他参数和@Retryable修饰的方法参数顺序一致。
CircuitBreaker注解
断路器,用于标注重试方法或者类。
这个注解的属性和Retryable注解的属性差不多。
value属性
指定要重试的异常类型。
和include属性意思相同。
默认为空(如果excludes也为空,则重试所有异常)。
include属性
指定要重试的异常类型。
和value属性意思相同。
默认为空(如果excludes也为空,则重试所有异常)。
exclude属性
指定不需要重试的异常类型。
默认为空(如果include也为空,则重试所有异常)。
如果include为空但exclude为非空,则重试所有未被exclude指定的异常。
maxAttempts属性
最大重试次数(包括第一次失败)。
默认值是3。
maxAttemptsExpression属性
最大重试次数的表达式(包括第一次失败)。
默认值是3。
label属性
一个唯一的标签,用于统计重试相关信息。
默认为声明注解的方法签名。
resetTimeout属性
断路器重置前的超时(毫秒)。
默认20s。
如果断路器打开的时间超过resetTimeout,则在下一次调用时重置,以使下游组件有机会再次响应。
resetTimeoutExpression属性
断路器重置前的超时表达式(毫秒)。
openTimeout属性
配置断路器打开的超时时间。
默认5s。
当超过openTimeout之后断路器变成半打开状态(只要有一次重试成功,则闭合)。
在openTimeout时间内,达到了重试次数达到了maxAttempts,且都失败了,电路将自动打开,从而阻止访问下游组件。
openTimeoutExpression属性
通过表达式指定openTimeout。
exceptionExpression属性
异常处理表达式。
ExpressionRetryPolicy中使用,执行完父类的 canRetry(SimpleRetryPolicy.canRetry) 之后,需要校验 exceptionExpression 的值,为 true 则可以重试,才会触发重试机制。
可以通过这个属性指定表达式用于有条件地抑制重试。
如果抛出多个异常,只会检查最后那个。
可以在exceptionExpression中指定其他Bean。
例如,"message.contains('you can retry this')"、 "@someBean.shouldRetry(#root)"。
listeners属性
指定要使用的重试侦听器的Bean名称,而不是Spring上下文中定义的默认名称。
了解以上内容,我们就可以简单使用spring-retry重试框架了,更多的组合应用可以在使用熟悉的过程中不断摸索。
@Retryable(value = Exception.class, maxAttemptsExpression = "${retry.maxAttempts:3}",
backoff = @Backoff(delayExpression = "${retry.delay:100}",
maxDelayExpression = "${retry.maxDelay:10}",
multiplierExpression = "${retry.multiplier:1}"),
recover = "recover",
listeners = {"smsRetryListener"})
@Override
public Boolean sendSsmExtend(String phone, String msg) {
log.info("向 {} 发送短信:{}", phone, msg);
throw new RuntimeException("发送短信失败");
}
@Recover
public Boolean recover(Throwable throwable, String phone, String msg) {
log.info("进入recover方法......");
log.info("注册成功后短信发送失败-存入DB-后续处理!!!......");
return true;
}
举个完整的栗子:
代码示例: 注解方式
POM依赖
com.panda
common
0.0.1-SNAPSHOT
org.springframework.retry
spring-retry
1.3.4
org.springframework
spring-aspects
5.3.23
com.google.guava
guava
org.apache.commons
commons-lang3
org.apache.httpcomponents
httpclient
com.github.rholder
guava-retrying
2.0.0
主要依赖是spring-retry、spring-aspects、guava-retrying。
RetryDemoApplication.java
package com.panda.retry;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;
@EnableRetry
@SpringBootApplication
public class RetryDemoApplication {
public static void main(String[] args) {
SpringApplication.run(RetryDemoApplication.class, args);
}
}
项目启动类。
声明EnableRetry注解,开启重试功能。
User.java
package com.panda.retry.entity;
import lombok.Data;
@Data
public class User {
private String userName;
private String password;
private String phone;
}
用户信息实体类。
SsmService.java
package com.panda.retry.service;
import common.core.Result;
public interface SsmService {
Result sendSsm(String phone, String msg);
}
短信服务接口,包含一个发送短信的方法。
UserService.java
package com.panda.retry.service;
import com.panda.retry.entity.User;
import common.core.Result;
public interface UserService {
Result register(User user);
}
用户接口,包含一个注册方法。
UserServiceImpl.java
package com.panda.retry.service.impl;
import com.panda.retry.entity.User;
import com.panda.retry.service.SsmService;
import com.panda.retry.service.UserService;
import common.core.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Resource
private SsmService ssmService;
@Override
public Result register(User user) {
log.info("保存用户信息......");
log.info("初始化积分信息......");
ssmService.sendSsm(user.getPhone(), "恭喜,用户注册成功!");
return Result.success();
}
}
用户接口实现类,实现用户注册功能。
在该功能中,会调用短信服务的发送短信功能。
SsmServiceImpl.java
package com.panda.retry.service.impl;
import com.panda.retry.service.SsmService;
import common.core.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class SsmServiceImpl implements SsmService {
@Retryable(recover = "recover", maxAttempts = 5, backoff = @Backoff(value = 2000, multiplier = 2))
@Override
public Result sendSsm(String phone, String msg) {
log.info("向 {} 发送短信:{}", phone, msg);
throw new RuntimeException("发送短信失败");
}
@Recover
public Result recover(Throwable throwable, String phone, String msg){
log.info("进入recover方法......");
return Result.success();
}
}
短信服务实现类。
实现发送短信的功能。
通过Retryable注解标注sendSsm方法,使得短信在发送失败时,可以进行重试。主要指定了最大重试次数 5,回避策略backoff ,以及兜底方法recover。
被Recover注解标注的方法是一个兜底方法,该方法签名有一定的要求,参见上面的讲解。
注意,Retryable注解通过recover属性指定的兜底方法必须在当前类中!
包括以下几种:
NeverRetryPolicy
只允许调用RetryCallback一次,但不允许重试。
AlwaysRetryPolicy
总是允许重试。
使用不当会导致死循环,慎重使用!
SimpleRetryPolicy
固定次数的重试策略。
默认重试最大次数是3次,是RetryTemplate默认使用的策略。
ExpressionRetryPolicy
继承SimpleRetryPolicy,表达式重试策略。
TimeoutRetryPolicy
超时重试策略。
默认超时时间为1秒,在指定的超时时间内允许重试。
CircuitBreakerRetryPolicy
具有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate。
CompositeRetryPolicy
组合重试策略。
有两种组合方式:乐观组合重试策略是指只要有一个策略允许重试就可以重试;悲观组合重试策略是指只要有一个策略不允许重试就不能重试。
ExceptionClassifierRetryPolicy
根据产生的异常选择重试策略。
BackOffPolicy 退避策略
包括以下几种:
NoBackOffPolicy
无退避策略,重试时立即重试。
FixedBackOffPolicy
固定时间的退避策略,需设置参数sleeper和backOffPeriod,前者指定等待策略,默认是Thread.sleep,即线程休眠;后者指定休眠时间,默认1秒。
UniformRandomBackOffPolicy
随机时间退避策略。
需设置sleeper、minBackOffPeriod和maxBackOffPeriod,该策略在[minBackOffPeriod, maxBackOffPeriod] 之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒。
ExponentialBackOffPolicy
指数退避策略。
需设置参数sleeper、initialInterval、maxInterval以及multiplier。
其中initialInterval指定初始休眠时间,默认100毫秒;maxInterval指定最大休眠时间,默认30秒,multiplier指定乘数,即下一次休眠时间为当前休眠时间 * multiplier;
ExponentialRandomBackOffPolicy
随机指数退避策略。
引入一个随机乘数,避免固定乘数可能会引起很多服务同时重试导致DDos的情况。
RetryConfig
package com.panda.retry.config;
import com.panda.retry.listener.MyRetryListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.backoff.ThreadWaitSleeper;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
@Configuration
public class RetryConfig {
@Bean
public RetryTemplate retryTemplate() throws InterruptedException {
// 退避策略
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
ThreadWaitSleeper sleeper = new ThreadWaitSleeper();
sleeper.sleep(1);
backOffPolicy.withSleeper(sleeper);
backOffPolicy.setMultiplier(3D);
backOffPolicy.setInitialInterval(1000);
// 重试策略
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(5);
RetryTemplate retryTemplate = RetryTemplate.builder()
.customPolicy(retryPolicy)
.customBackoff(backOffPolicy)
.retryOn(Exception.class)
.build();
retryTemplate.registerListener(new MyRetryListener());
return retryTemplate;
}
}
重试配置。
配置退避策略、重试策略、重试异常,并注册监听器。
MyRetryListener
package com.panda.retry.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryListener;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class MyRetryListener implements RetryListener {
@Override
public boolean open(RetryContext context, RetryCallback callback) {
log.info("重试过程打开了");
return true;
}
@Override
public void close(RetryContext context, RetryCallback callback, Throwable throwable) {
log.info("重试过程关闭了");
}
@Override
public void onError(RetryContext context, RetryCallback callback, Throwable throwable) {
log.info("重试过程发生错误");
}
}
监听器,实现RetryListener接口。
在重试开始之前调用open方法,重试结束后调用close方法。
重试抛出异常的时候调用onError方法。
SsmService
package com.panda.retry.service;
import common.core.Result;
public interface SsmService {
Result sendSsm(String phone, String msg);
Result sendSsm1(String phone, String msg);
Result recover(Throwable throwable, String phone, String msg);
}
短信接口,新增了两个方法。
UserService
package com.panda.retry.service;
import com.panda.retry.entity.User;
import common.core.Result;
public interface UserService {
Result register(User user);
Result register1(User user);
}
用户接口,新增了一个方法。
SsmServiceImpl
package com.panda.retry.service.impl;
import com.panda.retry.service.SsmService;
import common.core.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class SsmServiceImpl implements SsmService {
@Retryable(recover = "recover", maxAttempts = 5, backoff = @Backoff(value = 2000, multiplier = 2))
@Override
public Result sendSsm(String phone, String msg) {
log.info("向 {} 发送短信:{}", phone, msg);
throw new RuntimeException("发送短信失败");
}
@Override
public Result sendSsm1(String phone, String msg) {
log.info("向 {} 发送短信:{}", phone, msg);
throw new RuntimeException("发送短信失败");
}
@Recover
public Result recover(Throwable throwable, String phone, String msg) {
log.info("进入recover方法......");
return Result.success();
}
}
短信实现类。
UserServiceImpl
package com.panda.retry.service.impl;
import com.panda.retry.entity.User;
import com.panda.retry.service.SsmService;
import com.panda.retry.service.UserService;
import common.core.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Resource
private SsmService ssmService;
@Resource
private RetryTemplate retryTemplate;
@Override
public Result register(User user) {
log.info("保存用户信息......");
log.info("初始化积分信息......");
ssmService.sendSsm(user.getPhone(), "恭喜,用户注册成功!");
return Result.success();
}
@Override
public Result register1(User user) {
log.info("保存用户信息......");
log.info("初始化积分信息......");
retryTemplate.execute(
retry -> ssmService.sendSsm1(user.getPhone(), "恭喜,用户注册成功!"),
recover -> ssmService.recover(recover.getLastThrowable(), user.getPhone(), "恭喜,用户注册成功!")
);
return Result.success();
}
}
注意register1方法中的逻辑,通过retryTemplate的execute方法来实现重试逻辑和兜底策略。
UserController
package com.panda.retry.controller;
import com.panda.retry.entity.User;
import com.panda.retry.service.UserService;
import common.core.Result;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("user")
public class UserController {
@Resource
private UserService userService;
@PostMapping("register")
public Result register(@RequestBody User user) {
return userService.register(user);
}
@PostMapping("register1")
public Result register1(@RequestBody User user) {
return userService.register1(user);
}
}
总结
本文为大家介绍了spring-retry的两种用法:通过注解的方式,以及通过RetryTemplate编码的方式。并介绍了spring-retry的常用注解,重试策略以及退避策略等等。
一个重试模块应该具备以下属性:
(1)重试时执行的方法,这个方法执行成功后就不用再重试,这个方法要求幂等,并且失败时需要抛出异常来标识执行失败;
(2)重试策略:重试最大次数、重试最大时间即XXms后不再重试、重试次数之间的间隔、对于哪些异常才需要重试;
(3)重试次数达到上限仍未成功,最后的兜底逻辑,如插入重试任务表、发送消息给系统管理员等;
结合以上的思考,我们看下spring-retry重试模块应该具备的属性及作用:
RetryTemplate: 封装了Retry基本操作,是进入spring-retry框架的整体流程入口,通过RetryTemplate可以指定监听、回退策略、重试策略等。
RetryCallback:该接口封装了业务代码,且failback后,会再次调用RetryCallback接口,直到达到重试次数/时间上限;
RecoveryCallback:当RetryCallback不能再重试的时候,如果定义了RecoveryCallback,就会调用RecoveryCallback,并以其返回结果作为最终的返回结果。此外,RetryCallback和RecoverCallback定义的接口方法都可以接收一个RetryContext上下文参数,通过它可以获取到尝试次数、异常,也可以通过其setAttribute()和getAttribute()来传递一些信息。
RetryPolicy:重试策略,描述什么条件下可以尝试重复调用RetryCallback接口;策略包括最大重试次数、指定异常集合/忽略异常集合、重试允许的最大超时时间;RetryTemplate内部默认时候用的是SimpleRetryPolicy,SimpleRetryPolicy默认将对所有异常进行尝试,最多尝试3次。还有其他多种更为复杂功能更多的重试策略;
BackOffPolicy:回退策略,用来定义在两次尝试之间需要间隔的时间,如固定时间间隔、递增间隔、随机间隔等;RetryTemplate内部默认使用的是NoBackOffPolicy,其在两次尝试之间不会进行任何的停顿。对于一般可重试的操作往往是基于网络进行的远程请求,它可能由于网络波动暂时不可用,如果立马进行重试它可能还是不可用,但是停顿一下,过一会再试可能它又恢复正常了,所以在RetryTemplate中使用BackOffPolicy往往是很有必要的;
RetryListener:RetryTemplate中可以注册一些RetryListener,可以理解为是对重试过程中的一个增强,它可以在整个Retry前、整个Retry后和每次Retry失败时进行一些操作;如果只想关注RetryListener的某些方法,则可以选择继承RetryListenerSupport,它默认实现了RetryListener的所有方法;
代码有异常需要重试,女朋友有情绪也要记得哄哦。人无完人,感情也需要debug,发现问题解决问题。
最后祝大家有情人终成眷属 哈哈哈哈~~~~~~~~~~~~~~~~~~~~~~~~~~~~