SpringBoot系列(21):基于Guava_Retrying机制实现重试功能

作者: 修罗debug
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。


摘要:

对于“接口/方法 重试”,相信很多小伙伴都听说过,但是在实际项目中却很少真正去实践过,在本篇文章中,Debug将给各位小伙伴介绍一种“重试”机制的实现,即Guava_Retrying,相对于传统的Spring_Retrying或者动态代理实现的重试功能而言,本文要介绍的Guava_Retrying机制使用起来将更加容易、灵活性更强!

内容:

老赵:“这个 接口/方法 调用又失败了,老李啊,你去写个重试功能吧!”。

老李:“他娘的,这接口调用咋又不行了。。。行吧,老子立马给你撸一个重试功能” 。

这样的对话,相信有些小伙伴会感觉似曾相识!特别是当自己在工位上安安静静的写代码时,会突然性的接到技术老大分配给自己的这种需求。。。没啥好说的,只能潜下心,去研究研究了!

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

而诸如“远程调用超时”、“网络突然中断”等业务场景则可以进行重试,在微服务 治理框架中,通常都有自己的重试与超时配置,比如Dubbo可以设置retries=1,timeout=500调用失败只重试1次,超过500ms调用仍未返回则调用失败(详情可以观看学习Debug录制的“分布式服务调度Dubbo实战教程 https://www.fightjava.com/web/index/course/detail/2 ”)

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

下面我们基于前面搭建的SpringBoot多模块企业级项目,基于Guava_Retrying初步实现所谓的“重试功能”。工欲善其事必先利其器,首先当然是需要加入Guava_Retrying的依赖Jar,如下所示:

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

之后,我们来写个简单的入门案例,先来 过一把“接口调用重试”的瘾!  

/**
* Guava_retrying机制实现重试
* @Author:debug (SteadyJack)
* @Link: weixin-> debug0868 qq-> 1948831260
* @Date: 2019/12/1 16:17
**/
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的叠加逻辑----");
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();
}
}

}

运行该main方法,可以得到如下的结果:



从该上述代码中,我们得知“重试机制”功能实现的核心在于定义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();
}
}


其中,我们加入了“监听器Listener”“定义了重试次数”“定义了每次重试的时间间隔”,这三个才是Guava_Retrying提供给开发者重量级的玩意,如下代码所示!  

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


其中,“重试次数策略StopStrategies”、“重试的时间间隔设置策略WaitStrategies”中Guava_Retrying提供了许多种选择,比如“重试次数可以是一个随机数”、“重试的时间间隔也可以设置为某个区间范围内的随机数”等等,下图为运行结果截图:



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

下图为 系统字典配置表SysConfig存储的字典记录,其中,没有id=11的记录,我们将拿着这个 id=11 来进行测试:



如下代码为正常项目开发过程中我们自定义的Service及其方法:  

/**
* Guava_Retrying重试机制的 小型真实案例
* @Author:debug (SteadyJack)
* @Link: weixin-> debug0868 qq-> 1948831260
* @Date: 2019/12/1 17:51
**/
@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();
}
}
}


最后写个Java Unit Test,即Java单元测试案例,如下所示:  

    @Autowired
private RetryService retryService;

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

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


点击运行该单元测试案例,啥事都不要做,等待运行结果,你会发现“重试”的效果我们已经实现了!如下所示:



我们再点击运行该单元测试案例,然后在它运行了第1次重试机会之后,我们赶紧手动到数据库将 id=12 的那条系统配置记录,调整为 id=11 !然后再来看运行的结果,如下图所示:


如下图为“补偿性措施”中的“发送邮件”:


好了,本篇文章我们就介绍到这里了,建议各位小伙伴一定要照着文章提供的样例代码撸一撸,只有撸过才能知道这玩意是咋用的,否则就成了“空谈者”(我他娘就最讨厌空谈之人!)。其他相关的技术,感兴趣的小伙伴可以关注底部Debug的技术公众号,或者加Debug的微信,拉你进“微信版”的真正技术交流群!一起学习、共同成长!  


补充:

1、本文涉及到的相关的源代码可以到此地址,check出来进行查看学习:

https://gitee.com/steadyjack/SpringBootTechnology

2、最近Debug发布了几门重量级的课程,感兴趣的小伙伴可以前往观看学习:
(1) 缓存中间件Redis技术入门与应用场景实战(SpringBoot2.x + 抢红包系统设计与实战) 
https://www.fightjava.com/web/index/course/detail/12

(2)  企业权限管理平台(SpringBoot2.0+Shiro+Vue+Mybatis) 
https://www.fightjava.com/web/index/course/detail/8

3、关注一下Debug的技术微信公众号,最新的技术文章、课程以及技术专栏将会第一时间在公众号发布哦