這可能是Feign呼叫可重試的最佳方案了

JAVA旭陽發表於2023-01-11

前言

在我們公司裡,不同的服務之間透過Feign進行遠端呼叫,但是,我們在嘗試使呼叫可重試時遇到了一個小問題,Feign框架本身可以配置的自己的重試機制,但是它是一刀切的方式,所有的呼叫都是同樣的機制,沒有辦法像我們希望的那樣在每個方法的基礎上配置。不過我在專案中探索除了一種新的寫法,透過spring-retry框架集合Feign去實現重試機制,可以為每個呼叫實現不同的重試機制,那究竟是如何做到的呢,繼續往下看呀。

歡迎關注個人公眾號『JAVA旭陽』交流溝通

自定義註解@FeignRetry

為了解決上面提到的問題,讓Feign呼叫的每個介面單獨配置不同的重試機制。我們使用了面向切面程式設計並編寫了一個自定義註解:@FeignRetry。此註釋的工作方式類似於@Retryable的包裝器,並與其共享相同的規範以避免混淆。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface FeignRetry {

    Backoff backoff() default @Backoff();
    int maxAttempt() default 3;
    Class<? extends Throwable>[] include() default {};
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Backoff {
    long delay() default 1000L;;
    long maxDelay() default 0L;
    double multiplier() default 0.0D;;
}

FeignRetryAspect切面處理@FeignRetry註解。

Slf4j
@Aspect
@Component
public class FeignRetryAspect {

    @Around("@annotation(FeignRetry)")
    public Object retry(ProceedingJoinPoint joinPoint) throws Throwable {
        Method method = getCurrentMethod(joinPoint);
        FeignRetry feignRetry = method.getAnnotation(FeignRetry.class);

        RetryTemplate retryTemplate = new RetryTemplate();
        retryTemplate.setBackOffPolicy(prepareBackOffPolicy(feignRetry));
        retryTemplate.setRetryPolicy(prepareSimpleRetryPolicy(feignRetry));

        // 重試
        return retryTemplate.execute(arg0 -> {
            int retryCount = arg0.getRetryCount();
            log.info("Sending request method: {}, max attempt: {}, delay: {}, retryCount: {}",
                    method.getName(),
                    feignRetry.maxAttempt(),
                    feignRetry.backoff().delay(),
                    retryCount
            );
            return joinPoint.proceed(joinPoint.getArgs());
        });
    }

    private BackOffPolicy prepareBackOffPolicy(FeignRetry feignRetry) {
        if (feignRetry.backoff().multiplier() != 0) {
            ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
            backOffPolicy.setInitialInterval(feignRetry.backoff().delay());
            backOffPolicy.setMaxInterval(feignRetry.backoff().maxDelay());
            backOffPolicy.setMultiplier(feignRetry.backoff().multiplier());
            return backOffPolicy;
        } else {
            FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
            fixedBackOffPolicy.setBackOffPeriod(feignRetry.backoff().delay());
            return fixedBackOffPolicy;
        }
    }


    private SimpleRetryPolicy prepareSimpleRetryPolicy(FeignRetry feignRetry) {
        Map<Class<? extends Throwable>, Boolean> policyMap = new HashMap<>();
        policyMap.put(RetryableException.class, true);  // Connection refused or time out
        policyMap.put(ClientException.class, true);     // Load balance does not available (cause of RunTimeException)
        if (feignRetry.include().length != 0) {
            for (Class<? extends Throwable> t : feignRetry.include()) {
                policyMap.put(t, true);
            }
        }
        return new SimpleRetryPolicy(feignRetry.maxAttempt(), policyMap, true);
    }

    private Method getCurrentMethod(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        return signature.getMethod();
    }
}

捕獲FeignRetry註解的方法,將配置傳遞給Spring RetryTemplate,根據配置呼叫服務。

@FeignRetry 的使用

用法很簡單,只需將註釋放在我們希望重試機制處於活動狀態的 Feign Client 方法上即可。自定義切面的用法類似於Spring自帶的@Retryable註解。

@GetMapping
@FeignRetry(maxAttempt = 3, backoff = @Backoff(delay = 500L))
ResponseEntity<String> retrieve1();

@GetMapping
@FeignRetry(maxAttempt = 6, backoff = @Backoff(delay = 500L, maxDelay = 20000L, multiplier = 4))
ResponseEntity<String> retrieve2();

另外還需要在應用程式類中使用 @EnableRetry 註釋來啟動重試,比如可以載入SpringBoot的啟動類中。

總結

Feign重試其實是一個很常見的場景,我們本文透過了自定義了一個@FeignRetry註解來實現可重試的機制,針對不同的Feign介面還可以使用不同的重試策略,是不是很方便,快在你的專案中用起來吧。

歡迎關注個人公眾號『JAVA旭陽』交流溝通

相關文章