使用重试机制保证应用的稳定性
前言
没有人能保证自己的系统不出错,同样,在调用第三方系统时,也不能保证能够100%的成功。
往往会因为程序逻辑、网络中断、边界值等各种各样的问题导致服务失败。
在不同的业务领域对于服务的错误率有着不同的要求,一些金融领域的系统一般要求服务的错误率为0.01%。
那么为了保证很低的错误率,则需要通过一些专门的机制来完成。而最常见的方式就是在出现错误时通过重试来解决。
场景
比如,在购买保险的场景中,用户购买保险之前,需要根据用户的个人信息来查询产品的报价。
在产品报价接口中,它调用了另一个服务查询客户信息,假设这个客户信息需要从另一台独立的微服务中去查询。
如果客户信息查询接口返回500错误,那么产品报价服务会怎么给用户返回报价信息呢?一种简单的方法是直接返回给用户查询失败。
当然,更好的方法是通过重试客户信息查询接口,来恢复报价服务查询的功能。在我们依赖于另一个外部服务时,我们在某种程度上无法控制外部服务的稳定性,通过这种重试的自我恢复机制,可以有效地保证自己服务的稳定性,改善用户体验。
可能有些朋友要问了,如果客户信息查询的接口一直失败呢?难道要一直重试吗?
当然不是。我们可以按照一定的重试机制,比如只重试3次,如果3次都失败,则重试结束。
那么该如何实现这种重试呢?最简单的方法就是在调用外部服务时,使用最原始的for循环。
但是这是一个很普遍、通用的场景,Spring框架的开发人员早就想到了。在Spring中已经封装好了相应的API,可以让我们简单、灵活地来实现接口重试。
Spring重试机制spring-retry
最开始,spring的重试功能是和spring-batch
放在一个模块中的。从2.2.0版本开始,将spring-retry
单独成立了一个模块。
如果我们要在Springboot项目中启用这个功能,则需要在maven的pom.xml
文件中添加如下依赖:
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.3.1.RELEASE</version>
</dependency>
这个库并没有自动配置功能,它的artifactId
也不是xxx-starter,我们需要在SpringBoot启动类或者带@Configuration
注解的配置配置类中添加@EnableRetry
注解,启动重试功能。
如何使用
完成上面的依赖添加和@EnableRetry
注解配置之后,接下来我们要对调用外部接口的服务增加重试功能,则很简单。
注解声明方式
使用@Retryable
注解是最简单快捷的方式。
@Service
public class RetryableCustomerClient {
@Autowired
private CustomerSrvClient customerSrvClient;
@Retryable(value = RuntimeException.class, maxAttempts = 3, backoff = @Backoff(delay = 500L, maxDelay = 3000L, multiplier = 2, random = true))
public Optional<Customer> getCustomer(Long id){
return customerSrvClient.getCustomer(id);
}
}
上述代码中在getCustomer
方法上的@Retryable
注解表示该方法具备重试功能。其中的参数配置了重试的具体机制。
- value指定只有该方法抛出RuntimeException时进行重试;
- maxAttempts 指定该方法最多重试3次
- backoff 指定每次重试的间隔在500ms-3000ms之间随机。
在添加@Retryable
注解之后,Spring会在运行时创建一个代理对象,在这个代理中根据指定的重试参数执行重试逻辑,调用客户信息查询的API。而这个代理对象是在系统启动时才创建的,对产品报价服务透明,因此产品报价服务中不需要额外修改代码。
使用注解方式基本可以满足我们常规的一些重试机制的使用。
但是,如果你想再做一些更灵活、更自定义的重试策略,则使用注解则不太适合。
比如,在我们的这个案例中,我们想按照产品报价查询时的不同产品,使用不同的重试策略。在这种情况下,我们可以使用写代码的方式来实现,与声明式相对比也称之为命令式。
命令式实现-支持动态重试策略
针对命令式实现,在spring-retry中专门提供了一个RetryTemplate
类。这种方式需要在我们的业务逻辑中使用这个类。
在以下代码示例中,实现了根据产品类型的不同,使用不同的重试策略。可以看出,使用RetryTemplate的确实要比注解灵活许多。
private Optional<Product> retrieveProduct(String productCode){
// 比如交通意外险重试5次,其他保险产品重试2次
int maxAttempts = productCode.startsWith(TRAVEL_INSURANCE_PREFIX)? 5 : 2;
RetryTemplate retryTemplate = RetryTemplate.builder()
.maxAttempts(maxAttempts)
.retryOn(RuntimeException.class)
.exponentialBackoff(300L, 2, 5000L, true)
.build();
return retryTemplate.execute(arg -> productSrvClient.getProduct(productCode));
}
写操作重试
在前面的案例中我们都是针对查询接口进行重试,当然重试不仅在查询时可用,在进行一些数据写入、更新的操作中同样可以使用。比如当我们在一个保存订单的操作中,可能会因为数据库锁超时、连接超时之类的一些原因导致处理失败,这种情况如果再进行一次重试基本都会成功。
但是这里要特别注意,对于写操作服务的重试,一定要保证服务的幂等性。 也就是说,当你进行多次执行时,结果应该和执行一次一样。比如对于同一个订单的保存操作在重复执行多次后应该还是只保存一条记录,而不是保存多条。
如果订单数据的物理主键是自增的,则必须使用其他的业务主键字段,保证其唯一性。
如何测试重试
对于我们的重试功能,虽然通过API我们能知道其功能,但是我们还是要针对不同的场景进行测试的。
但是对于重试场景的测试往往不太容易,在我团队中的小伙伴很多次没有对重试功能进行自测,其原因基本都是因为在测试环境不方便模拟失败的情况,比如我想让它前两次成功,第三次失败之类。
对于这种情况的测试,我们需要依赖于一些mock测试工具,比如使用Mockito模拟测试。
关于Mockito的使用不是本期内容的重点,大家可以查询相关文档学习。
最后
使用重试机制往往会对提高系统的稳定性带来很大的帮助,如果你只是想用一些简单的重试机制,那么使用@Retryable注解即可,但是要更定制化,则可以使用RetryTemplate来灵活地完成。
当然了,在进行重试改造之前,一定要评估是否真的需要,以及对于写操作的服务,是否保证了操作的幂等性。
以上就是本期的全部内容,希望对你有所帮助。
我是小黑,一名在互联网“苟且”的程序员。
原创不易,需要一点正反馈,点赞+转发+在看,三连走一波~ ❤