springboot系列——重试机制原理和应用,还有比这个讲的更好的吗(附完整源码)
如果有,请转给我!
“重试是为了提高成功的可能性“
反过来理解,任何可能失败且允许重试操作的场景,就适合使用重试机制。但有了重试机制就一定能成功吗?显然不是。如果不成功就一直重试,这种处理方式会使得业务线程一直被重试占用,这样会导致服务的负载线程暴增直至服务宕机,因此需要限制重试次数。失败情况下,我们需要做后续的操作,如果是数据库操作的重试,需要回滚事物;如果是服务调用的重试,需要邮件报警通知运维开发人员,恢复服务。
对于服务接口调用,可能是因为网络波动导致超时失败,这时候所有重试次数是在很短时间内发起的话,就很容易全部超时失败,因此超时机制还需要引入重试动作之间时间间隔以及第一次失败后延迟多长时间再开始重试等机制。
重试机制要素
- 限制重试次数
- 每次重试的时间间隔
- 最终失败结果的报警或事物回滚
- 在特定失败异常事件情况下选择重试
任何可能失败且允许重试操作的场景,就适合使用重试机制。那么在分布式系统开发环境中,哪些场景需要是使用重试机制呢。
- 乐观锁机制保证数据安全的数据更新场景,如账户信息的金额数据更新。
- 微服务的分布式架构下,服务的调用因超时而失败。
spring-retry核心:配置重试元数据,失败恢复或报警通知。
pom文件依赖
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency>
配置重试元数据
@Override @Retryable(value = Exception.class,maxAttempts = 3 , backoff = @Backoff(delay = 2000,multiplier = 1.5)) public int retryServiceOne(int code) throws Exception { // TODO Auto-generated method stub System.out.println("retryServiceOne被调用,时间:"+LocalTime.now()); System.out.println("执行当前业务逻辑的线程名:"+Thread.currentThread().getName()); if (code==0){ throw new Exception("业务执行失败情况!"); } System.out.println("retryServiceOne执行成功!"); return 200; }
配置元数据情况:
- 重试次数为3
- 第一次重试延迟2s
- 每次重试时间间隔是前一次1.5倍
- Exception类异常情况下重试
测试:
启动应用,浏览器输入:http://localhost:8080/springRetry。
后台结果:
执行业务发起逻辑的线程名:http-nio-8080-exec-6 retryServiceOne被调用,时间:17:55:48.235 执行当前业务逻辑的线程名:http-nio-8080-exec-6 retryServiceOne被调用,时间:17:55:50.235 执行当前业务逻辑的线程名:http-nio-8080-exec-6 retryServiceOne被调用,时间:17:55:53.236 执行当前业务逻辑的线程名:http-nio-8080-exec-6 回调方法执行!!!!
注解类:
/** * 重试注解 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface JdkRetry{ //默认 int maxAttempts() default 3; //默认每次间隔等待3000毫秒 long waitTime() default 3000; //捕捉到的异常类型 再进行重发 Class<?> exception () default Exception.class ; String recoverServiceName () default "DefaultRecoverImpl"; }
注解类包含的元数据有:
- 尝试次数
- 重试间隔时间
- 抛出哪种异常会重试
- 重试完后还是失败的恢复类
使用spring AOP技术,实现重试注解的切面逻辑类RetryAspect。
@Transactional(rollbackFor = Exception.class) @Around("@annotation(jdkRetry)") //开发自定义注解的时候,定要注意 @annotation(jdkRetry)和下面方法的参数,按规定是固定的形式的,否则报错 public Object doConcurrentOperation(ProceedingJoinPoint pjp , JdkRetry jdkRetry) throws Throwable { //获取注解的属性 // pjp.getClass().getMethod(, parameterTypes) System.out.println("切面作用:"+jdkRetry.maxAttempts()+ " 恢复策略类:"+ jdkRetry.recoverServiceName()); Object service = JdkApplicationContext.jdkApplicationContext.getBean(jdkRetry.recoverServiceName()); Recover recover = null; if(service == null) return new Exception("recover处理服务实例不存在"); recover = (Recover)service; long waitTime = jdkRetry.waitTime(); maxRetries = jdkRetry.maxAttempts(); Class<?> exceptionClass = jdkRetry.exception(); int numAttempts = 0; do { numAttempts++; try { //再次执行业务代码 return pjp.proceed(); } catch (Exception ex) { //必须只是乐观锁更新才能进行重试逻辑 System.out.println(ex.getClass().getName()); if(!ex.getClass().getName().equals(exceptionClass.getName())) throw ex; if (numAttempts > maxRetries) { recover.recover(null); //log failure information, and throw exception // 如果大于 默认的重试机制 次数,我们这回就真正的抛出去了 // throw new Exception("重试逻辑执行完成,业务还是失败!"); }else{ //如果 没达到最大的重试次数,将再次执行 System.out.println("=====正在重试====="+numAttempts+"次"); TimeUnit.MILLISECONDS.sleep(waitTime); } } } while (numAttempts <= this.maxRetries); return 500; }
切面类获取到重试注解元信息后,切面逻辑会做以下相应的处理:
- 捕捉异常,对比该异常是否应该重试
- 统计重试次数,判断是否超限
- 重试多次后失败,执行失败恢复逻辑或报警通知
测试:
启动应用,浏览器输入:http://localhost:8080/testAnnotationRetry
结果:
切面作用:3 恢复策略类:DefaultRecoverImpl AnnotationServiceImpl被调用,时间:18:11:25.748 org.jackdking.retry.jdkdkingannotation.retryException.UpdateRetryException =====正在重试=====1次 AnnotationServiceImpl被调用,时间:18:11:28.748 org.jackdking.retry.jdkdkingannotation.retryException.UpdateRetryException =====正在重试=====2次 AnnotationServiceImpl被调用,时间:18:11:31.749 org.jackdking.retry.jdkdkingannotation.retryException.UpdateRetryException =====正在重试=====3次 AnnotationServiceImpl被调用,时间:18:11:34.749 org.jackdking.retry.jdkdkingannotation.retryException.UpdateRetryException 2020-05-26 18:11:34.749 ERROR 14892 --- [io-8080-exec-10] o.j.r.j.recover.impl.DefaultRecoverImpl : 重试失败,未进行任何补全,此为默认补全:打出错误日志
幂等性问题:
在分布式架构下,服务之间调用会因为网络原因出现超时失败情况,而重试机制会重复多次调用服务,但是对于被调用放,就可能收到了多次调用。如果被调用方不具有天生的幂等性,那就需要增加服务调用的判重模块,并对每次调用都添加一个唯一的id。
大量请求超时堆积:
超高并发下,大量的请求如果都进行超时重试的话,如果你的重试时间设置不安全的话,会导致大量的请求占用服务器线程进行重试,这时候服务器线程负载就会暴增,导致服务器宕机。对于这种超高并发下的重试设计,我们不能让重试放在业务线程,而是统一由异步任务来执行。
模板方法设计模式来实现异步重试机制
所有业务类继承重试模板类RetryTemplate
@Service("serviceone") public class RetryTemplateImpl extends RetryTemplate{ public RetryTemplateImpl() { // TODO Auto-generated constructor stub this.setRecover(new RecoverImpl()); } @Override protected Object doBiz() throws Exception { // TODO Auto-generated method stub int code = 0; System.out.println("RetryTemplateImpl被调用,时间:"+LocalTime.now()); if (code==0){ throw new Exception("业务执行失败情况!"); } System.out.println("RetryTemplateImpl执行成功!"); return 200; } class RecoverImpl implements Recover{ @Override public String recover() { // TODO Auto-generated method stub System.out.println("重试失败 恢复逻辑,记录日志等操作"); return null; } } }
- 业务实现类在doBiz方法内实现业务过程
- 所有业务实现一个恢复类,实现Recover接口,重试多次失败后执行恢复逻辑
测试:
启动应用,浏览器输入:http://localhost:8080/testRetryTemplate
结果:
2020-05-26 22:53:41.935 INFO 25208 --- [nio-8080-exec-4] o.j.r.r.c.RetryTemplateController : 开始执行业务 RetryTemplateImpl被调用,时间:22:53:41.936 RetryTemplateImpl被调用,时间:22:53:41.938 RetryTemplateImpl被调用,时间:22:53:44.939 RetryTemplateImpl被调用,时间:22:53:47.939 2020-05-26 22:53:50.940 INFO 25208 --- [pool-1-thread-1] o.j.r.r.service.RetryTemplate : 业务逻辑失败,重试结束 重试失败 恢复逻辑,记录日志等操作
完整的demo项目,请关注公众号“前沿科技bot“并发送”重试机制”获取。