前言

我正在参加「启航计划」

我们好,我是 Skow

在我们事务开发的进程中,难免会碰到对外进行调用的情况,比方在金融场景中,我们需求去推送还款计划的信息、查询还款成果等

三方的接口,对于我们来说其实相似一个“黑盒”,我们不知道这个盒子里面究竟做了什么事,可是为了不影响盒子出现的失常影响我们事务正常的进行,所以我们需求针对不同的失常进行一个重试或许其他战略的进行

类比到我们结构中,比方 RocketMQ 针对于发送失利的消息,也会将消息放到指定的队列,然后进行重试发送,其实简而言之重试,就是为了去确保我们事务的可用性、容错性、一致性

那么,针对于失常重试,或许新来的小杨同学会觉得,失常重试不是很简略,我判别一下三方回来的失常,进行 for 循环固定次数的从头调用就完事了

比方这样

一行注解搞定失常重试,这么牛?

坏代码的味道

稍微解释一下这一部分代码(摘自实在事务体系,去除了一些活络的事务逻辑)

开发这一部分理赔查验的同学,抽取了一个 payQuery 方法,其间一个参数为 重试次数

然后代码里假设重试次数小于等于 0,则认为重试结束了,此次查验失利

然后 运用 try catch 去进行外部调用,针对外部 回来的不同失常进行 catch 然后进行事务逻辑处理、睡觉、重试次数减一,继续进行外部调用

看到这个代码的时分,其实你说能跑吧也能跑,用吧,也可以凑合用,就是有点 “臭”,接下来我们凭借 Spring-Retry 结构去优雅完成我们的重试功用

把玩 Spring-Retry

介绍

根据 Baeldung 网站介绍

Spring Retry provides an ability to automatically re-invoke a failed operation. This is helpful where the errors may be transient (like a momentary network glitch).

这些都是四级词汇,想必我们应该都知道,我在给我们翻译一下

Spring-Retry 结构供给了主动重试失利操作的才干,这个才干对瞬时的过错(比方网络故障)对错常有帮助的

经过上述介绍,其实我们知道 Spring-Retry 是底子可以满意我们的需求,那么接下来跟我,我们一起把玩一下这个结构

入门

引入依靠

        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
            <version>1.2.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
        </dependency>

发动类增加注解

@SpringBootApplication
// 增加这个注解
@EnableRetry
public class RetryApplication {
    public static void main(String[] args) {
        SpringApplication.run(RetryApplication.class, args);
    }
}

指定失常重试

将 @Retryable 注解加在对应的方法上即可

针对 NPE、IAE 失常会进行重试

@Retryable(value = {NullPointerException.class, IllegalArgumentException.class})

经过 SPEL 写法指定失常

@Retryable(exceptionExpression = "#{#root instanceof T(java.lang.IllegalArgumentException)}")

经过 SPEL 写法指定失常

// 格局#{@bean.methodName(#root)}。methodName的返回值为boolean类型。#root是失常类,即用户可以在代码中判别是否进行重试
    @Retryable(exceptionExpression = "#{@retryDemoImpl.isRetry(#root)}")
    public void testNullException() throws MyException {
        log.info("重试一下吧!");
        throw new MyException("失常");
    }
    public Boolean isRetry(Exception e) {
        return e instanceof MyException && (((MyException) e).getMyMessage()).contains("失常");
    }
    @Getter
    @Setter
    public class MyException extends Exception {
        private String myMessage;
        public MyException(String myMessage) {
            this.myMessage = myMessage;
        }
        @Override
        public String toString() {
            return "MyException{" + "myMessage='" + myMessage + '\'' + '}';
        }
    }

打扫某些失常不进行重试

@Retryable(exclude = {IllegalArgumentException.class} )

指定重试次数

    /**
     * maxAttempts 直接指定重试次数
     * maxAttemptsExpression = "${max.attempts:}" 从配备文件中获取失常重试次数
     * 假设贪心的你都进行配备了,那么以 maxAttemptsExpression 为主
     */
    @Retryable(value = {IllegalArgumentException.class}, maxAttempts = 5, maxAttemptsExpression = "${max.attempts:}" )
    public void testNullException() {
        log.info("重试一下吧!");
        throw new IllegalArgumentException();
    }

指定间隔时间重试

    /**
     * delay为 2000 ms 进行重试,multiplier设置为2,则表明榜首次重试间隔为2s,第2次为4秒,第三次为8s,
     * maxDelay 设置最大的重试间隔,当超过这个最大的重试间隔的时分,重试的间隔就等于maxDelay的值
     */
    @Retryable(value = IllegalArgumentException.class, backoff = @Backoff(delay = 2000, multiplier = 2, maxDelay = 5000))
    public void testNullException() {
        log.info("重试一下吧!");
        throw new IllegalArgumentException();
    }

重试失利兜底战略


    @Retryable(value = IllegalArgumentException.class, backoff = @Backoff(delay = 2000, multiplier = 2, maxDelay = 5000))
    public int testNullException(String message) {
        log.info("重试一下吧!");
        throw new IllegalArgumentException();
    }
    /**
     * 作为 @Retryable 方法重试失利之后的兜底计划
     * @Recover 的失常 @Retryable 注解的方法保持一致,榜首入参为要重试的失常,其他参数与 @Retryable 保持一致,返回值也要相同,否则无法实行
     */
    @Recover()
    public int recover1(IllegalArgumentException e, String message) {
        log.info("进入失常1");
        return 1;
    }
    @Recover
    public int recover2(NullPointerException e, String message) {
        log.info("进入失常2");
        return 2;
    }

运用 RetryTemplate 进行失常重试

我们知道事务一般可以分为声明式事务办理和编程式事务办理,关于这二者的差异,我前期文章中有进行分析,感兴趣的小伙伴可以去翻一下

我们的失常重试也有“声明式重试”和“编程式重试”

相同的我认为,再有必要的时分可以运用“编程式重试”,去提示未来接手这段代码的小伙伴,这段代码具有重试逻辑~~,你丫的看认真点~~

我们凭借 RetryTemplate 进行 “编程式重试”

首要我们需求自定义我们自己的 RetryTemplate,比方以下所示

@Configuration
public class RetryTemplateConfig {
    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();
        // 设置重试战略
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(5);
        retryTemplate.setRetryPolicy(retryPolicy);
        // 设置退避战略
        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setSleeper(new Sleeper() {
            @Override
            public void sleep(long backOffPeriod) throws InterruptedException {
                // 等候的时分做点什么事呢,看个电影吧?
                System.out.println("其时等候时间" + backOffPeriod);
            }
        });
        fixedBackOffPolicy.setBackOffPeriod(5000L);
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
        retryTemplate.setListeners();
        return retryTemplate;
    }
}

其实这一段代码,无非就是把注解的东西搬到了我们的代码上

关键注重 RetryPolicy、FixedBackOffPolicy 、RetryListener这三个类是啥玩意

RetryPolicy 其实就是去设置我们的重试战略,默许的话现已给我们供给了多种重试战略

一行注解搞定失常重试,这么牛?

从图中我们可以看 RetryPolicy 有许多的完成类,我们说两个比较常用的,其他的小伙伴可以自己去探究一下如何运用,

  • SimpleRetryPolicy 默许最多重试3次
  • CompositeRetryPolicy 可以组合多个重试战略
  • NeverRetryPolicy 从不重试
  • AlwaysRetryPolicy 总是重试

FixedBackOffPolicy 是我们的退避战略,退避战略,其实就是去指定我们一旦产生失常多久的时间可以重试、怎样去做下一次重试

相同的我们看一下 BackOffPolicy 的完成类有哪些,我们也简略举一些

一行注解搞定失常重试,这么牛?

  • FixedBackOffPolicy 默许固定推延 1 秒后实行下一次重试
  • ExponentialBackOffPolicy 指数递加推延实行重试,默许初始 0.1 秒,系数是 2,那么下次推延 0.2 秒,再下次就是推延 0.4 秒,如此类推,最大推延 30 秒
  • ExponentialRandomBackOffPolicy 在上面那个战略上增加随机性,我们设置该战略参数为initialInterval = 50 multiplier = 2.0 maxInterval = 3000 numRetries = 5,依照正常重试时间为 [50, 100, 200, 400, 800],可是在该战略下或许为 [76, 151, 304, 580, 901]
  • UniformRandomBackOffPolicy 这个跟上面的差异就是,上面的推延会不断递加,这个只会在你设置的最小的重试时间和最大的重试之间随机

RetryListener 其实可以了解为我们的监听者,它可以去监听我们重试进程然后实行我们对应的回调方法

接下来,我们自定义 Listener 玩一下

@Slf4j
public class DefaultListenerSupport extends RetryListenerSupport {
    @Override
    public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback,
        Throwable throwable) {
        log.info("在最后一次重试后调用");
        super.close(context, callback, throwable);
    }
    @Override
    public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback,
        Throwable throwable) {
        log.info("在每次重试后都会被调用");
        super.onError(context, callback, throwable);
    }
    @Override
    public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
        log.info("在榜首次重试前调用");
        return super.open(context, callback);
    }
}

然后 set 到我们的 RetryTemplate 中即可

完好的运用比方如下

    @Autowired
    private RetryTemplate retryTemplate;
    public int testNullException(String message) throws IllegalAccessException {
        retryTemplate.execute(new RetryCallback<Object, IllegalAccessException>() {
            @Override
            public Object doWithRetry(RetryContext context) throws IllegalAccessException {
                log.info("重试一下吧");
                throw new IllegalAccessException();
            }
        });
        return 1;
    }

源码赏识

上面仅仅带我们入门了一下 Spring-Retry 的底子运用方法,小伙伴们可以以此为抓手,然后自己深化研究,闭环 Spring-Retry 全体玩法,然后到达年末 375 的好评

我们深化评论下 Spring-Retry 是如何完成重试的,在翻看源码前,其实我们可以猜测,假设是我们自己去规划这样一个重试注解我们会怎样规划?

考虑 5 分钟

5 分钟结束

接下来跟着 Skow 扒开 Spring-Retry 的外衣

我们开始 debug,根据仓库其实我们可以发现这个结构的入口处其实在

一行注解搞定失常重试,这么牛?

org.springframework.retry.support.RetryTemplate#execute(org.springframework.retry.RetryCallback<T,E>)

	public final <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback)
			throws E {
		return doExecute(retryCallback, null, null);
	}

这个 execute 仅接纳一个函数,这个函数其实就是涵盖了我们需求重试的方法以及类的一些底子信息

我们继续 debug 往下走,下一步是 org.springframework.retry.support.RetryTemplate#doExecute

一般在 Spring 源码中,带有 do 的方法,就是实在的实行逻辑,这个小 tip 送给我们,拿小本本记起来

这个 doExecute 也是我们 Spring-Retry 真实的实行逻辑, 在这个方法中它主要做了几件事

  • 判别是否需求进行回调
  • 依照配备实行重试战略(有情况、无情况)
  • 依照配备实行退避战略

那我们就根据这几点进行关键代码的分析

判别是否需求进行回调

判别方法集中在 RetryTemplate.java:278

canRetry(retryPolicy, context) && !context.isExhaustedOnly()

这个方法名起的就非常的简略易懂,是否可以重试 && 暂时不知道什么的 context 是否现已结束

我们先看一下 canRetry 里面做了什么事

	@Override
	public boolean canRetry(RetryContext context) {
		Throwable t = context.getLastThrowable();
		return (t == null || retryForException(t)) && context.getRetryCount() < maxAttempts;
	}

这个 t 我们可以了解为其时抛出来的失常类型,即引起我们重试的究竟是那个憎恶的失常

retryForException 做的工作就是判别你其时这个失常是否符合我们配备的失常类型

context 可以了解为重试配备的上下文方针,从中获取的 retryCount 就是我们现在现已重试多少次了

maxAttempts 读了上文的小伙伴必定一眼就知道了,这个就是我们自主配备的最大的重试次数

所以这个 canRetry 这个方法做的工作就是判别失常类型是否符合我们的需求重试的失常而且其时的重试次数要小于我们的重试次数

接着我们来看 !context.isExhaustedOnly() 做了什么事

在前文我们现已剧透了 context 事 重试配备的上下文方针,那我 isExhaustedOnly 这个字段是什么时分被设置的,什么时分为 true 什么时分为 false,则需求引起我们的注重

仔细的小伙伴或许会观察到我们在自定义 DefaultListenerSupport 的时分,其实在方法中是可以拿到 contetx 这个作为入参的,bingo,就是在这个时分我们可以设置重试结束,直接 context.setExhaustedOnly(); 即可阻遏我们的重试

无情况重试战略

经过了前一轮方法的判别,我们可以顺利实行我们的重试了

一行注解搞定失常重试,这么牛?

一旦重试产生了任何失常,回被我们再次 catch 住

然后进行 registerThrowable,这个方法其实就是为了计算我们的重试次数、将失常塞入我们的上下文方针、设置我们的 retryState(这是一个重试情况我们后边再说)

紧接着会走到我们的 doOnErrorInterceptors 方法

这个方法其实就是实行我们的监听者的 onError (在每次重试后都会被调用)方法

	private <T, E extends Throwable> void doOnErrorInterceptors(
			RetryCallback<T, E> callback, RetryContext context, Throwable throwable) {
		for (int i = this.listeners.length; i-- > 0;) {
			this.listeners[i].onError(context, callback, throwable);
		}
	}

因为经过监听者的过滤,我们的 context 或许会被改动,所以这儿还会进行一次是否可以继续重试的判别

依照配备实行退避战略

假设还可以继续重试的话,则调用 backOffPolicy.backOff(backOffContext);

这个方法其实就是去实行我们的退避战略,比方暂停一段时间后再进行重试

有情况重试战略

或许有小伙伴会对这个有疑问,什么是有情况,什么是无情况

无情况重试比较好了解,最简略的场景就是我们直接对外调用,这个时分这个重试上下文在一个循环中完结一切的事,我们直接无脑进行重试就好了

可是有情况的重试,其实就需求依靠到我们上文说到的 retryState,一般在 Spring-Retry 中我们有两种情况需求用到有情况重试 事务失常需求回滚、以及熔断器形式下的重试 则需求进行有情况的重试

具体运用方法如 demo 所示

    public int testNullException() throws IllegalAccessException {
        // 其时情况的称号,当把情况放入缓存时,经过该key查询获取
        Object key = "mykey";
        // 是否每次都从头生成上下文仍是从缓存中查询,即全局形式(如熔断器战略时从缓存中查询)
        boolean isForceRefresh = true;
        // 对 DuplicateKeyException 进行回滚
        BinaryExceptionClassifier rf = new BinaryExceptionClassifier(
            Collections.<Class<? extends Throwable>>singleton(DuplicateKeyException.class));
        RetryState state = new DefaultRetryState(key, isForceRefresh, rf);
        try {
            retryTemplate.execute(new RetryCallback<Object, IllegalAccessException>() {
                @Override
                public Object doWithRetry(RetryContext context) throws IllegalAccessException {
                    log.info("重试一下吧");
                    throw new IllegalAccessException();
                }
            }, new RecoveryCallback() {
                @Override
                public Object recover(RetryContext context) throws Exception {
                    log.info("重试一下吧2");
                    return null;
                }
            }, state);
        } catch (DuplicateKeyException e) {
            // 实行回滚操作
        }
        return 1;
    }

继续有情况的重试,仍是失利的话,会继续实行

handleRetryExhausted(recoveryCallback, context, state);

这个方法就是 假设你存在 RecoveryCallback,就会实行此回调,否则直接抛出失常

最后 的话会进行相关的环境变量清理

至此我们的 RetryTemplate 现已分析结束,当然还有一些非常细节的点我没有展开说明,感兴趣的小伙伴可以自己跟着 debug 分析一波,需求文中的 demo 可以与我联络,我可以发给你,开箱即 debug

闲言碎语

这是上一年就想写的一篇文章,一直拖到了现在,国庆期间写了一下,然后又拖到了现在才发

假期期间朋友推荐了一部电影给我《去世诗社》

抱负和实际,自我寻求与被规划着前行的压力,我觉得应该很多人都有过相似的体会

《去世诗社》好像给了仍对未来抱有神往的人一个答案

“我步入森林,因为我希望日子的有意义,我希望活的深化。罗致生命中一切的精华,把非生命的一切都击溃,防止让我在生命完结时,发现自己从来没有活过!

seize the day 珍惜现在 把握今天