Spring-retry实践

在业务系统中,经常会遇到进行业务重试的场景,当一个执行方法失败时,可能需要进行几次重试,如果重试时成功了,仍然认为业务是正确的。否则则throw相应的异常。这种通用的重试场景则可以使用标准的第三方重试库来完成,以避免自己在业务代码中写类似while try 的代码。同时,因为重试的条件以及处理的逻辑均不太一样,通过一个标准的重试库来完成此操作,也是很有必要的。

本文描述了spring提供的重试库spring-retry,通过提取相应的概念模型,了解其工作原理以及执行机制.
版本:1.2.2.RELEASE

会使用到的注释

Retryable
用于标识一个可以进行重试的方法,即一个方法如果需要在throw异常之后进行重试,则可以使用此注解进行标记.同时,此注解上标识了可以进行重试的一些条件以及次数信息。

Backoff
此标识一个方法在进行下一次重试时,需要暂停的一些手法以及参数信息,比如sleep多少秒,或者是是在一个区间范围内sleep。

Recover
此标识当一个方法执行多次都失败之后,进行的一个failback处理。

主要的业务接口

RetryContext
重试上下文,即维护了整个重试周期中相应调用对象,调用次数,以及在调用过程中的异常对象等,其它接口均从此对象中获取相应的数据.

RetryOperations
封装了一个主要可重试操作,可以理解为进行主要逻辑操作的template对象。

RetryPolicy
与@Backoff相对应的一个接口,用于在整个重试逻辑中,处理何时该重试,以及控制相应的重试上下文信息。

RetryListener
监听器,用于在重试过程中,对开始,重试出错,结束时进行拦截操作。默认情况下,此类为null,即无作用。

RetryCallback
简单的重试回调对象,用于封装原来的可重试方法,将整个操作统一使用callback来表示,以隔离相应的实现。

RecoveryCallback
与@Recover相对应,即最终进行failback的回调对象.

整个spring-retry的执行逻辑均封装在RetryTemplate这个类中,然后在外部通过RetryConfiguration来配置一些额外的业务接口信息,最终形成一个bean拦截器,通过拦截bean的调用来完成重试的处理.
一个简单的执行逻辑(以无状态重试为例)

整个逻辑参照于类RetryTemplate#doExecute方法,其中 recoveryCallback对象为null,即无failback. state为null,即不是有状态的重试.

  • 创建想重试上下文 RetryContext ,即为当前执行准备好相应的数据对象
  • 启动相应的拦截器 RetryListener, 以支持在进行实际调用前进行相应的判断,以判断是否应该继续执行操作
  • 启动可重试判定 RetryPolicy,判定的条件包括当前执行次数,以及相应的异常是否在期望异常体系之内。
  • 执行实际的业务逻辑 RetryCallback,如果成功则直接返回
  • 业务执行失败,上下文 RetryContext 补充相应的异常数据
  • 同时拦截器 启动失败时拦截回调. RetryListener, 此时的失败回调是void,即仅起到通知作用
  • 在需要进行下一次重试的基础之上(即还会进行重试), 启动 RetryPolicy 重试策略,即准备相应的间隔sleep处理
  • 在所有重试均结束时,直接Throw最后一次执行异常.
  • 完成整个重试逻辑,拦截器 RetryListener 进行onClose回调,即在整个重试体系结束前进行通知回调.

实际业务场景举例

@Service
public class UserService {

private static final Logger logger = LoggerFactory.getLogger(UserService.class);
private int count = 0;

// 默认重试3次 可通过maxAttempts配置(including the first failure)
@Retryable(backoff = @Backoff(2000))
public void insert(User user) {
logger.info("UserService.insert user = [{}] count: {}", user, ++count);
if (user != null && user.getId() > 2L && logger.isInfoEnabled()) {
logger.info(user.toString());
return;
}
throw new RuntimeException("插入失败");
}

@Recover
public void cover(User user){
logger.info("user: {}, e: {}", user, "插入失败");
}

// @Recover
// public void cover(){º
// logger.info("no params method execute!");
// }
}


@Configuration
@EnableRetry
@ComponentScan
public class Application {

public static void main(String[] args) {
ApplicationContext context =
new AnnotationConfigApplicationContext(Application.class);
UserService userService = context.getBean(UserService.class);
User user = new User().setId(1L).setName("sirpeng");
userService.insert(user);
user.setId(3L).setName("user3");
userService.insert(user);
user.setId(4L).setName("user4");
userService.insert(user);
}
}

依赖配置

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
</dependencies>
坚持原创技术分享,您的支持将鼓励我继续创作!