經常使用的重試技術—如何優雅的重試

背景

分佈式環境下,重試是高可用技術中的一個部分,你們在調用RPC接口或者發送MQ時,針對可能會出現網絡抖動請求超時狀況採起一下重試操做,本身簡單的編寫重試大多不夠優雅,而重試目前已有不少技術實現和框架支持,但也是有個有缺點,本文主要對其中進行整理,以求找到比較優雅的實現方案;java

重試在功能設計上須要根據應用場景進行設計,讀數據的接口比較適合重試的場景,寫數據的接口就須要注意接口的冪等性了,還有就是重試次數若是太多的話會致使請求量加倍,給後端形成更大的壓力,設置合理的重試機制是關鍵;git

重試技術實現

本文整理比較常見的重試技術實現:
一、Spring Retry重試框架;
二、Guava Retry重試框架;
三、Spring Cloud 重試配置;github

具體使用面進行整理:算法

一、 Spring Retry重試框架

SpringRetry使用有兩種方式:spring

  • 註解方式
    最簡單的一種方式數據庫

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

設置重試捕獲條件,重試策略,熔斷機制便可實現重試到熔斷整個機制,這種標準方式查閱網文便可;
這裏介紹一個本身處理熔斷的狀況,及不用 @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. 數據轉換
      ....
        //三、存儲
        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;
        
    }

注意:
一、 @Recover中不能再拋出Exception,不然會報沒法識別該異常的錯誤;
二、以註解的方式對方法進行重試,重試邏輯是同步執行的,重試的「失敗」針對的是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 {

    @Autowired
    RestTemplateFactory 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
             */
            @Override
            public 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:隨機指數退避策略,引入隨機乘數能夠實現隨機乘數回退

二、Guava retry重試框架

guava retryer工具與spring-retry相似,都是經過定義重試者角色來包裝正常邏輯重試,可是Guava retryer有更優的策略定義,在支持重試次數和重試頻度控制基礎上,可以兼容支持多個異常或者自定義實體對象的重試源定義,讓重試功能有更多的靈活性。

三、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: 6000
    ConnectTimeout: 6000
    MaxAutoRetries: 1
    MaxAutoRetriesNextServer: 2
       OkToRetryOnAllOperations: true

指定服務service1配置

service1:
  ribbon:
    MaxAutoRetries: 1
    MaxAutoRetriesNextServer: 2
    ConnectTimeout: 5000
    ReadTimeout: 2000
    OkToRetryOnAllOperations: 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-service

feign:
  hystrix:
    enabled: true


client1:
  ribbon:
    #配置首臺服務器重試1次
    MaxAutoRetries: 1
    #配置其餘服務器重試兩次
    MaxAutoRetriesNextServer: 2
    #連接超時時間
    ConnectTimeout: 500
    #請求處理時間
    ReadTimeout: 2000
    #每一個操做都開啓重試機制
    OkToRetryOnAllOperations: true

#配置斷路器超時時間,默認是1000(1秒)
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 2001

參考

一、https://www.jianshu.com/p/96a5003c470c
二、https://www.imooc.com/article/259204
三、http://www.javashuo.com/article/p-fwwgyhno-cu.html
四、https://houbb.github.io/2018/08/07/guava-retry

相關文章
相關標籤/搜索