使用 Spring-Retry 指定特定于异常的退避策略

Specifying an exception-specific backoff policy with Spring-Retry

我正在使用 Spring-Retry 进行一些数据库操作。在 SQLRecoverableException 上我重试三次(这假设导致异常的任何原因如果失败三次是非瞬态的),在 SQLTransientException 上我无限期地重试(程序不能做任何事情没有访问数据库,所以它可能会一直重试,直到用户决定重新启动服务器),并且在任何其他异常情况下我不会重试。我使用指数退避策略,基本重试为 100 毫秒,最大重试为 30,000 毫秒。

private static final int MAX_RECOVERABLE_RETRIES = 3;
private static final long INITIAL_INTERVAL = 100;
private static final long MAX_INTERVAL = 30 * 1000;
private static final double MULTIPLIER = 2.0;

public static RetryTemplate databaseTemplate() {
    RetryTemplate template = new RetryTemplate();
    ExceptionClassifierRetryPolicy retryPolicy = new ExceptionClassifierRetryPolicy();
    Map<Class<? extends Throwable>, RetryPolicy> policyMap = new HashMap<>();
    NeverRetryPolicy baseException = new NeverRetryPolicy();
    SimpleRetryPolicy recoverablePolicy = new SimpleRetryPolicy();
    recoverablePolicy.setMaxAttempts(MAX_RECOVERABLE_RETRIES);
    AlwaysRetryPolicy transientPolicy = new AlwaysRetryPolicy();
    policyMap.put(Exception.class, baseException);
    policyMap.put(SQLRecoverableException.class, recoverablePolicy);
    policyMap.put(SQLTransientException.class, transientPolicy);
    retryPolicy.setPolicyMap(policyMap);
    template.setRetryPolicy(retryPolicy);
    ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
    backOffPolicy.setInitialInterval(INITIAL_INTERVAL);
    backOffPolicy.setMaxInterval(MAX_INTERVAL);
    backOffPolicy.setMultiplier(MULTIPLIER);
    template.setBackOffPolicy(backOffPolicy);
    return template;
}

理想情况下,我想对所有 SQLRecoverableExceptions 使用 100 毫秒的固定退避,并且只对 SQLTransientExceptions 应用指数退避策略。我可以通过嵌套重试来完成此操作,但这会大大增加代码的复杂性 - 如果没有其他选择,我更愿意简单地将指数退避应用于 SQLRecoverableExceptionSQLTransientException 异常。

有没有办法让我使用单个重试模板将不同的退避策略应用于不同的异常?

确实,ExceptionClassifierRetryPolicy 是必经之路。不过,我没能让它与 policyMap 一起使用。

以下是我的使用方法:

@Component("yourRetryPolicy")
public class YourRetryPolicy extends ExceptionClassifierRetryPolicy
{
    @PostConstruct
    public void init()
    {
        final SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy();
        simpleRetryPolicy.setMaxAttempts( 3 );

        this.setExceptionClassifier( new Classifier<Throwable, RetryPolicy>()
        {
            @Override
            public RetryPolicy classify( Throwable classifiable )
            {
                    if ( classifiable instanceof YourException )
                    {
                            return new NeverRetryPolicy();
                    }
                    // etc...
                    return simpleRetryPolicy;
            }
        });
    }
}

然后,您只需在重试模板上进行设置即可:

@Autowired
@Qualifier("yourRetryPolicy")
private YourRetryPolicy yourRetryPolicy;

//...

RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy( yourRetryPolicy );

已接受的答案仅处理特定于异常的 RetryPolicy 实例。 Spring 不为异常特定的 BackOffPolicy 实例提供任何开箱即用的功能。 幸运的是它很容易实现。

import org.springframework.classify.Classifier
import org.springframework.classify.ClassifierSupport
import org.springframework.classify.SubclassClassifier
import org.springframework.retry.RetryContext
import org.springframework.retry.backoff.BackOffContext
import org.springframework.retry.backoff.BackOffInterruptedException
import org.springframework.retry.backoff.BackOffPolicy
import org.springframework.retry.backoff.NoBackOffPolicy

class ExceptionClassifierBackoffPolicy implements BackOffPolicy {

    private static class ExceptionClassifierBackoffContext implements BackOffContext, BackOffPolicy {
        Classifier<Throwable, BackOffPolicy> exceptionClassifier
        RetryContext retryContext
        Map<BackOffPolicy, BackOffContext> policyContextMap = [:]

        ExceptionClassifierBackoffContext(Classifier<Throwable, BackOffPolicy> exceptionClassifier, RetryContext retryContext) {
            this.exceptionClassifier = exceptionClassifier
            this.retryContext = retryContext
        }

        @Override
        BackOffContext start(RetryContext context) {
            return null
        }

        @Override
        void backOff(BackOffContext backOffContext) throws BackOffInterruptedException {
            def policy = exceptionClassifier.classify(retryContext.lastThrowable)
            def policyContext = policyContextMap.get(policy)
            if (!policyContext) {
                policyContext = policy.start(retryContext)
                policyContextMap.put(policy, policyContext)
            }
            policy.backOff(policyContext)
        }
    }
    private Classifier<Throwable, BackOffPolicy> exceptionClassifier = new ClassifierSupport<Throwable, BackOffPolicy>(new NoBackOffPolicy());

    void setPolicyMap(Map<Class<? extends Throwable>, BackOffPolicy> policyMap) {
        exceptionClassifier = new SubclassClassifier<Throwable, BackOffPolicy>(policyMap, new NoBackOffPolicy());
    }

    @Override
    BackOffContext start(RetryContext context) {
        return new ExceptionClassifierBackoffContext(exceptionClassifier, context)
    }

    @Override
    void backOff(BackOffContext backOffContext) throws BackOffInterruptedException {
        def classifierBackOffContext = (ExceptionClassifierBackoffContext) backOffContext
        classifierBackOffContext.backOff(backOffContext)
    }
}

然后:

BackOffPolicy backOffPolicy = new ExceptionClassifierBackoffPolicy()
def policyMap = [
        (RuntimeException): new FixedBackOffPolicy(backOffPeriod: 1000),
        (IOException)     : new ExponentialRandomBackOffPolicy(initialInterval: 500, maxInterval: 360000, multiplier: 2)
] as Map<Class<? extends Throwable>, BackOffPolicy>
backOffPolicy.policyMap = backoffPolicyMap

晚会有点晚了,但是当你需要的只是根据不同的情况有不同的退避期 异常的类型,扩展 FixedBackoffPolicy 应该可以解决问题并且非常简单。 沿着这些线的东西:

首先你创建你的退避策略 class 来接收一个地图 每种异常类型的不同退避期:

public class MultipleExceptionsBackoffPolicy extends FixedBackoffPolicy 
{
    private Classifier<Throwable, Long> classifier;

    public MultipleExceptionsBackoffPolicy (final Map<Class<? extends Throwable>, Long> throwableBackoffMap) {
        classifier = new SubclassClassifier<>(throwableBackoffMap, 5_000L) // default is 5s
    }

    @Override
    protected void doBackOff() exception BackOffInterruptedException {
        final backoff = classifier.classify(RetrySynchronizationManager.getContext().getLastThrowable());
        setBackOffPeriod(backoff);
        super.doBackOff();
    }
}

然后你必须创建一个自定义拦截器,例如:

@Bean
public Object myCustomInterceptor (){
    var exBackoffMap = Map.of(
        ExceptionTypeOne.class, 2_000L, // If ExceptionTypeOne happens, backoff period is 2s
        ExceptionTypeTwo.class, 7_000L // and if ExceptionTypeTwo happens, the backoff period is 7s
    )
    return RetryInterceptorBuilder
                .stateless()
                .retryPolicy(new SimpleRetryPolicy(3)) // always 3 attempts no matter what
                .backOffPolicy(new MultipleExceptionsBackoffPolicy(exBackoffMap))
                .build();
}

最后,您只需在 retryable 中配置自定义拦截器:

@Retryable(interceptor="myCustomInterceptor")
public @interface MyRetryable {}