背景

分布式环境下,重试是高可用技术中的一个部分,大家在调用RPC接口或者发送MQ时,针对可能会出现网络抖动请求超时情况采取一下重试操作,自己简单的编写重试大多不够优雅,而重试目前已有很多技术实现和框架支持,但也是有个有缺点,本文主要对其中进行整理,以求找到比较优雅的实现方案;

重试在功能设计上需要根据应用场景进行设计,读数据的接口比较适合重试的场景,写数据的接口就需要注意接口的幂等性了,还有就是重试次数如果太多的话会导致请求量加倍,给后端造成更大的压力,设置合理的重试机制是关键;

重试技术实现

本文整理比较常见的重试技术实现:
1、Spring Retry重试框架;
2、Guava Retry重试框架;
3、Spring Cloud 重试配置;

具体使用面进行整理:

1、 Spring Retry重试框架

SpringRetry使用有两种方式:

  • 注解方式
    最简单的一种方式

@Retryable(value = RuntimeException.class,maxAttempts = 3, backoff = @Backoff(delay = 5000L, multiplier = 2))

设置重试捕获条件,重试策略,熔断机制即可实现重试到熔断整个机制,这种标准方式查阅网文即可;
这里介绍一个自己处理熔断的情况,及不用 @Recover 来做兜底处理,继续往外抛出异常,代码大致如下:
Service中对方法进行重试:

@Override@Transactional@Retryable(value = ZcSupplyAccessException.class,maxAttempts = 3,backoff = @Backoff(delay = 2000,multiplier = 1.5))public OutputParamsDto doZcSupplyAccess(InputParamsDto inputDto) throws ZcSupplyAccessException {//1. 校验....//2. 数据转换....//3、存储try {doSaveDB(ioBusIcsRtnDatList);log.info("3.XXX-数据接入存储完成");} catch (Exception e) {log.info("3.XXX-数据接入存储失败{}", e);throw new ZcSupplyAccessException("XXX数据接入存储失败");}return new OutputParamsDto(true, "XXX处理成功");}

Controller中捕获异常进行处理,注意这里不用异常我们需要进行不同的处理,不能在***@Recover ***中进行处理,以免无法在外层拿到不同的异常;

@PostMapping("/accessInfo")public OutputParamsDto accessInfo( @RequestBody InputParamsDto inputDto ){log.info("接入报文为:"+JSONUtil.serialize(inputDto));OutputParamsDto output = validIdentity(inputDto);if(output==null || output.getSuccess()==false){return output;}log.info("Pre.1.安全认证通过");IAccessService accessService = null;try {....accessService = (IAccessService) ApplicationContextBeansHolder.getBean(param.getParmVal());//先转发(异常需处理)output = accessService.doZcSupplyTranfer(inputDto);//后存储(异常不处理)accessService.doZcSupplyAccess(inputDto);} catch (ZcSupplyTransferException e){log.error("转发下游MQ重试3次均失败,请确认是否MQ服务不可用");return new OutputParamsDto(false,"转发下游MQ重试3次均失败,请确认是否MQ服务不可用");} catch (ZcSupplyAccessException e){log.error("接入存储重试3次均失败,请确认是否数据库不可用");} catch (Exception e) {log.error("通过bean名调用方法和处理发生异常:"+e);return new OutputParamsDto(false,"通过bean名调用方法和处理发生异常");}...return output;}

注意:
1、 @Recover中不能再抛出Exception,否则会报无法识别该异常的错误;
2、以注解的方式对方法进行重试,重试逻辑是同步执行的,重试的“失败”针对的是Throwable,如果你要以返回值的某个状态来判定是否需要重试,可能只能通过自己判断返回值然后显式抛出异常了。

  • 方法式
    注解式只是让我们使用更加便捷,但是有一定限制,比如要求抛异常才能重试,不能基于实体,Recover方法如果定义多个比较难指定具体哪个,尤其是在结构化的程序设计中,父子类中的覆盖等需要比较小心,SpringRetry提供编码方式可以提高灵活性,返回你自定义的实体进行后续处理,也更加友好。

下面代码中RecoveryCallback部分进行了异常的抛出,这里也可以返回实体对象,这样就比注解式更友好了。

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.retry.RecoveryCallback;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.CircuitBreakerRetryPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.stereotype.Component;import java.time.LocalTime;
import java.util.Collections;
import java.util.Map;/*** <p>* 系统 <br>* <br>* Created by    on 2019/9/1016:12  <br>* Revised by [修改人] on [修改日期] for [修改说明]<br>* </p>*/
@Slf4j
@Component
@RefreshScope
public class ZcSupplySynRemoteRetryHandler {@AutowiredRestTemplateFactory restTemplateFactory;final RetryTemplate retryTemplate = new RetryTemplate();//简单重试策略final SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(3, Collections.<Class<? extends Throwable>, Boolean>singletonMap(ZcSupplySynRemoteException.class, true));@Value("${retry.initialInterval}")private String initialInterval;@Value("${retry.multiplier}")private String multiplier;/*** 重试处理** @param reqMap* @return* @throws ZcSupplySynRemoteException*/public  Map<String, Object> doSyncWithRetry(Map<String, Object> reqMap, String url) throws ZcSupplySynRemoteException {//熔断重试策略CircuitBreakerRetryPolicy cbRetryPolicy = new CircuitBreakerRetryPolicy(new SimpleRetryPolicy(3));cbRetryPolicy.setOpenTimeout(3000);cbRetryPolicy.setResetTimeout(10000);//固定值退避策略FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();fixedBackOffPolicy.setBackOffPeriod(100);//指数退避策略ExponentialBackOffPolicy exponentialBackOffPolicy = new ExponentialBackOffPolicy();exponentialBackOffPolicy.setInitialInterval(Long.parseLong(initialInterval));exponentialBackOffPolicy.setMultiplier(Double.parseDouble(multiplier));//设置策略retryTemplate.setRetryPolicy(retryPolicy);retryTemplate.setBackOffPolicy(exponentialBackOffPolicy);//重试回调RetryCallback<Map<String, Object>, ZcSupplySynRemoteException> retryCallback = new RetryCallback<Map<String, Object>, ZcSupplySynRemoteException>() {/*** Execute an operation with retry semantics. Operations should generally be* idempotent, but implementations may choose to implement compensation* semantics when an operation is retried.** @param context the current retry context.* @return the result of the successful operation.* @throws ZcSupplySynRemoteException of type E if processing fails*/@Overridepublic Map<String, Object> doWithRetry(RetryContext context) throws ZcSupplySynRemoteException {try {log.info(String.valueOf(LocalTime.now()));Map<String, Object> rtnMap = (Map<String, Object>) restTemplateFactory.callRestService(url,JSONObject.toJSONString(reqMap, SerializerFeature.WriteMapNullValue));context.setAttribute("rtnMap",rtnMap);return rtnMap;}catch (Exception e){throw new ZcSupplySynRemoteException("调用资采同步接口发生错误,准备重试");}}};//兜底回调RecoveryCallback<Map<String, Object>> recoveryCallback = new RecoveryCallback<Map<String, Object>>() {/*** @param context the current retry context* @return an Object that can be used to replace the callback result that failed* @throws ZcSupplySynRemoteException when something goes wrong*/public Map<String, Object> recover(RetryContext context) throws ZcSupplySynRemoteException{Map<String, Object> rtnMap = (Map<String, Object>)context.getAttribute("rtnMap");log.info("xxx重试3次均错误,请确认是否对方服务可用,调用结果{}", JSONObject.toJSONString(rtnMap, SerializerFeature.WriteMapNullValue));//注意:这里可以抛出异常,注解方式不可以,需要外层处理的需要使用这种方式throw new ZcSupplySynRemoteException("xxx重试3次均错误,请确认是否对方服务可用。");}};return retryTemplate.execute(retryCallback, recoveryCallback);}
}

核心类
RetryCallback: 封装你需要重试的业务逻辑;

RecoverCallback:封装在多次重试都失败后你需要执行的业务逻辑;

RetryContext: 重试语境下的上下文,可用于在多次Retry或者Retry 和Recover之间传递参数或状态;

RetryOperations : 定义了“重试”的基本框架(模板),要求传入RetryCallback,可选传入RecoveryCallback;

RetryListener:典型的“监听者”,在重试的不同阶段通知“监听者”;

RetryPolicy : 重试的策略或条件,可以简单的进行多次重试,可以是指定超时时间进行重试;

BackOffPolicy: 重试的回退策略,在业务逻辑执行发生异常时。如果需要重试,我们可能需要等一段时间(可能服务器过于繁忙,如果一直不间隔重试可能拖垮服务器),当然这段时间可以是 0,也可以是固定的,可以是随机的(参见tcp的拥塞控制算法中的回退策略)。回退策略在上文中体现为wait();

RetryTemplate: RetryOperations的具体实现,组合了RetryListener[],BackOffPolicy,RetryPolicy。

重试策略
NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试

AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环

SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略

TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试

ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试

CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate

CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许重试即可以,
悲观组合重试策略是指只要有一个策略不允许重试即可以,但不管哪种组合方式,组合中的每一个策略都会执行

重试回退策略
重试回退策略,指的是每次重试是立即重试还是等待一段时间后重试。

默认情况下是立即重试,如果需要配置等待一段时间后重试则需要指定回退策略BackoffRetryPolicy。

NoBackOffPolicy:无退避算法策略,每次重试时立即重试

FixedBackOffPolicy:固定时间的退避策略,需设置参数sleeper和backOffPeriod,sleeper指定等待策略,默认是Thread.sleep,即线程休眠,backOffPeriod指定休眠时间,默认1秒

UniformRandomBackOffPolicy:随机时间退避策略,需设置sleeper、minBackOffPeriod和maxBackOffPeriod,该策略在[minBackOffPeriod,maxBackOffPeriod之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒

ExponentialBackOffPolicy:指数退避策略,需设置参数sleeper、initialInterval、maxInterval和multiplier,initialInterval指定初始休眠时间,默认100毫秒,maxInterval指定最大休眠时间,默认30秒,multiplier指定乘数,即下一次休眠时间为当前休眠时间*multiplier

ExponentialRandomBackOffPolicy:随机指数退避策略,引入随机乘数可以实现随机乘数回退

2、Guava retry重试框架

guava retryer工具与spring-retry类似,都是通过定义重试者角色来包装正常逻辑重试,但是Guava retryer有更优的策略定义,在支持重试次数和重试频度控制基础上,能够兼容支持多个异常或者自定义实体对象的重试源定义,让重试功能有更多的灵活性。

3、Spring Cloud 重试配置

Spring Cloud Netflix 提供了各种HTTP请求的方式。
你可以使用负载均衡的RestTemplate, Ribbon, 或者 Feign。
无论你选择如何创建HTTP 请求,都存在请求失败的可能性。
当一个请求失败时,你可能想它自动地去重试。
当使用Sping Cloud Netflix这么做,你需要在应用的classpath引入Spring Retry。
当存在Spring Retry,负载均衡的RestTemplates, Feign, 和 Zuul,会自动地重试失败的请求

RestTemplate+Ribbon全局设置:

spring:cloud:loadbalancer:retry:enabled: true
ribbon:ReadTimeout: 6000ConnectTimeout: 6000MaxAutoRetries: 1MaxAutoRetriesNextServer: 2OkToRetryOnAllOperations: true

指定服务service1配置

service1:ribbon:MaxAutoRetries: 1MaxAutoRetriesNextServer: 2ConnectTimeout: 5000ReadTimeout: 2000OkToRetryOnAllOperations: true
配置 说明
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds 断路器的超时时间需要大于ribbon的超时时间,不然不会触发重试。
hello-service.ribbon.ConnectTimeout 请求连接的超时时间
hello-service.ribbon.ReadTimeout 请求处理的超时时间
hello-service.ribbon.OkToRetryOnAllOperations 是否对所有操作请求都进行重试
hello-service.ribbon.MaxAutoRetriesNextServer 重试负载均衡其他的实例最大重试次数,不包括首次server
hello-service.ribbon.MaxAutoRetries 同一台实例最大重试次数,不包括首次调用

feign重试完整配置yml

eureka:client:serviceUrl:defaultZone: http://localhost:8761/eureka/
server:port: 7001
spring:application:name: feign-servicefeign:hystrix:enabled: trueclient1:ribbon:#配置首台服务器重试1次MaxAutoRetries: 1#配置其他服务器重试两次MaxAutoRetriesNextServer: 2#链接超时时间ConnectTimeout: 500#请求处理时间ReadTimeout: 2000#每个操作都开启重试机制OkToRetryOnAllOperations: true#配置断路器超时时间,默认是1000(1秒)
hystrix:command:default:execution:isolation:thread:timeoutInMilliseconds: 2001

参考

1、https://www.jianshu.com/p/96a5003c470c
2、https://www.imooc.com/article/259204
3、https://blog.csdn.net/kisscatforever/article/details/80048395
4、https://houbb.github.io/2018/08/07/guava-retry

常用的重试技术—如何优雅的重试(Spring-Retry)等相关推荐

  1. 稳定性之重试,如何优雅地重试,防止系统雪崩

    背景 在微服务架构中,一个大系统被拆分成多个小服务,小服务之间大量 RPC 调用,经常可能因为网络抖动等原因导致 RPC 调用失败,这时候使用重试机制可以提高请求的最终成功率,减少故障影响,让系统运行 ...

  2. 优雅实现重试功能选型分析

    业务场景 应用中需要实现一个功能: 需要将数据上传到远程存储服务,同时在返回处理成功情况下做其他操作.这个功能不复杂,分为两个步骤:第一步调用远程的Rest服务逻辑包装给处理方法返回处理结果:第二步拿 ...

  3. Google Guava Retry 优雅的重试方案

    Google Guava Retry 优雅的重试方案 前言 使用场景 什么场景不适合重试 了解幂等性 一.Guava Retry是什么? 与Spring retry比较 二.使用步骤 1.引入库 2. ...

  4. foxmail邮件加载失败重试_java retry(重试) spring retry, guava retrying 详解

    系列说明 java retry 的一步步实现机制. java-retry 源码地址 情景导入 简单的需求 产品经理:实现一个按条件,查询用户信息的服务. 小明:好的.没问题. 代码 UserServi ...

  5. java retry(重试) spring retry, guava retrying 详解

    转载 自 http://blog.51cto.com/9250070/2156431 系列说明 java retry 的一步步实现机制. java-retry 源码地址 情景导入 简单的需求 产品经理 ...

  6. Spring异常重试机制 - Spring Retry

    目录 一 . 引入依赖 二 . 在启用类或业务类上添加@EnableRetry注解启用重试机制(在启用类上添加全局有效 , 在业务类上添加仅当前有效) 三 . 使用@Retryable实现重试 四 . ...

  7. HTTP服务器端常用推送技术

    服务器端推送技术描述 不论是传统的HTTP请求-响应式的通信模式, 还是异步的AJAX式请求, 服务器端始终处于被动的应答状态, 只有在客户端发出请求的情况下, 服务器端才会返回响应. 这种通信模式被 ...

  8. java ee有哪些工具_JavaEE开发中最常用到的技术和工具汇总

    原标题:JavaEE开发中最常用到的技术和工具汇总 今天千锋广州小编给大家来介绍一下关于目前JavaEE开发中最常用到的技术和工具的介绍,下面我们一起来看一下吧. 项目管理:Ant,项目管理事实上的标 ...

  9. Python常用6个技术网站汇总分享!

    Python是一门面向对象的编程语言,它具有丰富和强大的库,能够把用其他语言编写的各种模块轻松地联结在一起,因此也常被称为"胶水语言".Python技术会随着互联网的不断发展一直迭 ...

最新文章

  1. [转]MySQL实现分页查询
  2. hihocoder 网络流二·最大流最小割定理
  3. Android PC投屏简单尝试—最终章1
  4. .NET Core容器化@Docker
  5. python的字符串内建函数
  6. Tips--Solidworks 2016绘制工程图时显示gtol.sym文件缺失的解决方法
  7. 个人Blog小程序开发完毕
  8. python驱动级模拟按键大师_AB叔_C#驱动级模拟按键操作
  9. flex measure
  10. 组态王与三菱PLC编程软件GXWorks2通过OPC数据库进行动态仿真,只要写三菱程序即可实现组态王动态画面
  11. 抖音上热门的六大规律
  12. C# 打开CMD窗口并执行CMD 指令
  13. matlab直接解超越方程函数,用matlab 解超越方程~
  14. 【学懂数据结构】二叉树之舞之二叉树
  15. 以创业思维和产品思维复盘《商业简史》
  16. 通过java获取抖音用户主页信息(2020年9月)
  17. 自动弹图片html,HTML图片预览弹出层实现
  18. git学习记录/菜鸟教程(基于Gitcode)
  19. 随机过程(2)__马尔可夫链的主要性质__查普曼-科莫高洛夫方程
  20. 朝阳医院2018年销售数据分析

热门文章

  1. 黑马程序员 交通灯管理系统
  2. 全文检索框架Lucene——原理
  3. 基于行的帧内编码快速算法
  4. 加拿大11年级计算机课程代码,加拿大读11年级,我经历的选课、学英语、拿高分...
  5. 分布式系统—最后一致性
  6. Java笔记(3)_黑马刘意_Java入门_2019_idea版
  7. 在PowerBI中导入JSON文件
  8. C语言基础知识:C语言函数调用怎么返回两个值
  9. Github国内加速克隆及下载
  10. matlab 正则化表达式_Matlab-------regexp正则表达式