百味皆苦 java后端开发攻城狮

接口重试技术

2022-04-10
百味皆苦

简介

对于“接口/方法 重试”,介绍一种“重试”机制的实现,即Guava_Retrying,相对于传统的Spring_Retrying或者动态代理实现的重试功能而言,Guava_Retrying机制使用起来将更加容易、灵活性更强!

对于“重试”,那可是有场景限制的,不是什么场景都适合重试,比如参数校验不合法、写操作等(因为要考虑到写是否幂等)都不适合重试。

而诸如“远程调用超时”、“网络突然中断”等业务场景则可以进行重试,在微服务 治理框架中,通常都有自己的重试与超时配置,比如Dubbo可以设置retries=1,timeout=500调用失败只重试1次,超过500ms调用仍未返回则调用失败

对于“外部 RPC 调用”,或者“数据入库”等操作,如果一次操作失败,则可以进行多次重试,从而提高调用成功的可能性。

guava-retrying

小案例

demo

jar

<!--guava-retrying-->
<dependency>
   <groupId>com.github.rholder</groupId>
   <artifactId>guava-retrying</artifactId>
   <version>2.0.0</version>
</dependency>

util

public class RetryUtil {
    private static final Logger log= LoggerFactory.getLogger(RetryUtil.class);

    private static Integer i=1;

    public static Integer execute() throws Exception{
        log.info("----重试时 变量i的叠加逻辑,i={}----",i);
        return i++;
    }

    public static void main(String[] args) {
        //TODO:定义任务实例
        Callable<String> callable= () -> {
            Integer res=execute();
            //当重试达到3 + 1次之后 我们就不玩了
            if (res>3){
                return res.toString();
            }
            return null;
        };

        //TODO:定义重试器
        Retryer<String> retryer=RetryerBuilder.<String>newBuilder()
                //TODO:当返回结果为Null时 - 执行重试
                .retryIfResult(Predicates.isNull())
                //TODO:当执行核心业务逻辑抛出RuntimeException - 执行重试
                .retryIfRuntimeException()
                //TODO:还可以自定义抛出何种异常时 - 执行重试
                .retryIfExceptionOfType(IOException.class)
                .build();
        try {
            retryer.call(callable);
        } catch (ExecutionException | RetryException e) {
            e.printStackTrace();
        }
    }

}

“重试机制”功能实现的核心在于定义Retryer实例以及Callable任务运行实例 ,特别是Retryer实例,可以设置“什么时机重试”。

除此之外,对于 Retryer实例 我们还可以设置“重试的次数”、“重试的时间间隔”、“每次重试时,定义Listener监听一些操作逻辑”等等。

public static void main(String[] args) {
        //TODO:定义任务实例
        Callable<String> callable= () -> {
            return null;
        };

        //TODO:每次重试时 监听器执行的逻辑
        RetryListener retryListener=new RetryListener() {
            @Override
            public <V> void onRetry(Attempt<V> attempt) {
                Long curr=attempt.getAttemptNumber();
                log.info("----每次重试时 监听器执行的逻辑,当前已经是第 {} 次重试了----",curr);

                if (curr == 3){
                    log.error("--重试次数已到,是不是得该执行一些补偿逻辑,如发送短信、发送邮件...");
                }
            }
        };

        //TODO:定义重试器
        Retryer<String> retryer=RetryerBuilder.<String>newBuilder()
                //TODO:当返回结果为Null时 - 执行重试
                .retryIfResult(Predicates.isNull())
                //TODO:当执行核心业务逻辑抛出RuntimeException - 执行重试
                .retryIfRuntimeException()
                //TODO:还可以自定义抛出何种异常时 - 执行重试
                .retryIfExceptionOfType(IOException.class)

                //TODO:每次重试时的时间间隔为5s
                .withWaitStrategy(WaitStrategies.fixedWait(5L, TimeUnit.SECONDS))
                //TODO:重试次数为3次,3次之后就不重试了
                .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                //TODO:每次重试时定义一个监听器listener,监听器的逻辑可以是 "日志记录"、"做一些补偿操作"...
                .withRetryListener(retryListener)
                .build();

        try {
            retryer.call(callable);
        } catch (ExecutionException | RetryException e) {
            e.printStackTrace();
        }
    }

业务场景

调用某个接口的方法,用于获取SysConfig配置表中某个字典配置记录,如果该字典配置记录不存在(即返回Null),那我们就重试3次,如果期间获取到了,那么就返回结果;3次过后,依旧为Null时,则执行一些补偿性的措施:即发送邮件通知给到指定的人员,让他们上去检查检查相应的数据状况!

@Service
public class RetryService {
    private static final Logger log= LoggerFactory.getLogger(RetryService.class);

    @Autowired
    private SysConfigMapper sysConfigMapper;

    @Autowired
    private EmailSendService emailSendService;

    //TODO:获取某个字典配置详情
    public SysConfig getConfigInfo(final Integer id){
        SysConfig config=sysConfigMapper.selectByPrimaryKey(id);

        if (config==null){
            //TODO:当没有查询到该数据记录时,执行重试逻辑
            doRetry(id);
            config=sysConfigMapper.selectByPrimaryKey(id);
        }
        return config;
    }


    //TODO:执行重试逻辑
    private void doRetry(final Integer id){

        //TODO:定义任务实例
        Callable<SysConfig> callable= () -> {
            return sysConfigMapper.selectByPrimaryKey(id);
        };

        //TODO:每次重试时 监听器执行的逻辑
        RetryListener retryListener=new RetryListener() {
            @Override
            public <V> void onRetry(Attempt<V> attempt) {
                Long curr=attempt.getAttemptNumber();
                log.info("----每次重试时 监听器执行的逻辑,当前已经是第 {} 次重试了----",curr);
				
                //当达到3次时 就执行一些补偿性的措施,如发送邮件通知某些大佬….
                if (curr == 3){
                    log.error("--重试次数已到,是不是得该执行一些补偿逻辑,如发送短信、发送邮件...");

                    emailSendService.sendSimpleEmail("重试次数已到","请各位大佬上去检查一下sysConfig是否存在","1948831260@qq.com");
                }
            }
        };

        //TODO:定义重试器
        Retryer<SysConfig> retryer= RetryerBuilder.<SysConfig>newBuilder()
                //TODO:当返回结果为 false 时 - 执行重试(即sysCofig为null)
                .retryIfResult(Objects::isNull)
                //TODO:当执行核心业务逻辑抛出RuntimeException - 执行重试
                .retryIfRuntimeException()
                //TODO:还可以自定义抛出何种异常时 - 执行重试
                .retryIfExceptionOfType(IOException.class)

                //TODO:每次重试时的时间间隔为10s (当然啦,实际项目中一般是不超过1s的,如500ms,这里是为了方便模拟演示)
                .withWaitStrategy(WaitStrategies.fixedWait(10L, TimeUnit.SECONDS))
                //TODO:重试次数为3次,3次之后就不重试了
                .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                //TODO:每次重试时定义一个监听器listener,监听器的逻辑可以是 "日志记录"、"做一些补偿操作"...
                .withRetryListener(retryListener)

                .build();
        try {
            retryer.call(callable);
        } catch (ExecutionException | RetryException e) {
            e.printStackTrace();
        }
    }
}

test

    @Autowired
    private RetryService retryService;

    @Test
    public void method8() throws Exception{
        final Integer id=11;

        SysConfig entity=retryService.getConfigInfo(id);
        log.info("---结果:{}",entity);
    }

Comments

Content