1. 概览
在分布式场景中,Retry 和 Fallback 是最常见的容灾方案。
-
Retry 就是在调用远程接口失败时,Client 主动发起重试请求,以期待获得最终结果,从而完成整个流程 -
Fallback 是在调用远程接口失败时,Client 不进行重试而是调用一个特殊的 fallback 方法,从这个方法中获取结果,使流程能够继续下去
那 Retry 和 Fallback 该怎么抉择呢?
1.1. 背景
首先,先看下 Retry 和 Fallback 都是怎么帮助流程进行自我恢复的。
1.1.1. Retry
现在有一个生单流程:

核心流程如下:
-
从商品服务中获取商品信息 -
根据商品信息创建订单 -
将订单保存到数据库
如果发生网络抖动,将导致生单失败。

-
在调用商品服务获取商品时,由于网络异常,接口调用失败 -
由于无法获取商品信息,生单流程被异常中断
由于生单流程太过重要,系统需尽最大努力保障用户能够完成下单操作,那针对网络抖动这个问题,可以通过 Retry 进行修复。

-
在第一次获取商品信息时,由于网络问题导致获取失败 -
系统不会直接抛出异常,而是在等待一段时间后,重新发起第二次请求,也就是 Retry 操作 -
网络恢复,第二次请求成功获取商品信息 -
流程继续运行,最终完成用户生单
Retry 机制非常适合服务短时间不可用,或某个服务节点异常 这类场景。
1.1.2. Fallback
一个生单验证接口,主流程如下:

-
调用商品服务的接口获取商品信息 -
根据商品和用户信息判断用户是否能够购买该商品
同样,假设在访问商品服务时出现网络异常:

由于无法获取商品信息,从而导致整个验证流程被异常中断,用户操作被迫终止。
聪明的你估计会说那就使用 Retry 呀,是的:

如果是短时不可用,通过 Retry 机制便可以恢复流程。
但,如果是商品服务压力过大,响应时间过长呢?比如,商品服务流量激增,导致 DB CPU 飙升,出现大量的慢 SQL,这时触发了系统的 Retry 会是怎样?

-
在获取商品失败后,系统自动触发 Retry 机制 -
由于是商品服务本身出了问题,第二次请求仍旧失败 -
服务又触发了第三次请求,仍未获取结果 -
达到最大重试次数,仍旧无法获取商品,只能通过异常中断用户请求
通过 Retry 机制未能将流程从异常中恢复过来,也给下游的 商品服务 造成了巨大伤害。
-
商品服务压力大,响应时间长 -
上游系统由于超时触发自动重试 -
自动重试增大了对商品服务的调用 -
商品服务请求量更大,更难以从故障中恢复
这就是常说的“读放大”,假设用户验证是否能够购买请求的请求量为 n,那极端情况下 商品服务的请求量为 3n (其中 2n 是由 Retry 机制造成)
此时,Retry 就不是一个好的方案。我们先退回业务场景进行思考,如果无法获取商品,验证接口是否可以直接放行,先让用户完成购买?
如果,这个业务假设能够接受的话,那就到了 Fallback 上场的时候了。

-
调用商品服务获取商品信息失败 -
系统不会进行重试,而是触发 fallback 机制 -
fallback 会调用指定的一个方法,并将返回值作为远程接口的返回值 -
接下来的流程使用 fallback 方法的返回值完成业务逻辑
1.1.3. 场景思考
同样是对商品服务接口(同一个接口)的调用,在不同的场景需要使用不同的策略用以恢复业务流程,通常情况下:
-
Command 场景优先使用 Retry -
这种流量即为重要,最好能保障流程的完整性 -
通常写流量比较小,小范围 Retry 不会对下游系统造成巨大影响 -
Query 场景优选使用 Fallabck -
大多数展示场景,哪怕部分信息没有获取到对整体的影响也比较小 -
通常读场景流量较高,Retry 对下游系统的伤害不容忽视
那面对一个远程接口被多个场景使用,我们该怎么处理呢?
-
提供两组接口,一个具有 Retry 能力,一个具有 Fallback 能力,由使用方根据业务场景进行选择? -
还是…
1.2. 目标
-
远程接口具备 Retry 和 Fallback 能力 -
能够根据上下文不同场景,在发生调用异常时动态选择 Retry 或 Fallback 进行流程恢复
2. 快速入门
2.1. 准备环境
项目主要依赖 spring retry
和 lego starter
首先,引入 spring-retry 依赖
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
此次,引入 lego-starter 依赖
<dependency>
<groupId>com.geekhalo.lego</groupId>
<artifactId>lego-starter</artifactId>
<version>0.1.17</version>
</dependency>
最后新建 RetryConfiguration
以开启 Retry 能力
@EnableRetry
@Configuration
public class RetryConfiguration {
}
2.2. 构建 ActionTypeProvider
在完成基本配置后,需要准备一个 ActionTypeProvider
用以提供上下文信息。
ActionTypeProvider
接口定义如下:
public interface ActionTypeProvider {
ActionType get();
}
public enum ActionType {
COMMAND, QUERY
}
通常情况下,我们会使用 ThreadLocal
组件将 ActionType
存储于线程上下文,在使用时从上下中获取相关信息。
public class ActionContext {
private static final ThreadLocal<ActionType> ACTION_TYPE_THREAD_LOCAL = new ThreadLocal<>();
public static void set(ActionType actionType){
ACTION_TYPE_THREAD_LOCAL.set(actionType);
}
public static ActionType get(){
return ACTION_TYPE_THREAD_LOCAL.get();
}
public static void clear(){
ACTION_TYPE_THREAD_LOCAL.remove();
}
}
有了上下文之后,ActionBasedActionTypeProvider
直接从 Context 中获取 ActionType 具体如下
@Component
public class ActionBasedActionTypeProvider implements ActionTypeProvider {
@Override
public ActionType get() {
return ActionContext.get();
}
}
上下文中的 ActionType
又是怎么进行管理的呢,包括信息绑定和信息清理?
最常用的方式便是:
-
提供一个注解,在方法上添加注解用于对 ActionType 的配置; -
提供一个拦截器,对方法调用进行拦截。方法调用前,从注解中获取配置信息并绑定到上下文;方法调用后,主动清理上下文信息;
核心实现为:
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Action {
ActionType type();
}
@Aspect
@Component
@Order(Integer.MIN_VALUE)
public class ActionAspect {
@Pointcut("@annotation(com.geekhalo.lego.faultrecovery.smart.Action)")
public void pointcut() {
}
@Around(value = "pointcut()")
public Object action(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Action annotation = methodSignature.getMethod().getAnnotation(Action.class);
ActionContext.set(annotation.type());
try {
return joinPoint.proceed();
}finally {
ActionContext.clear();
}
}
}
在这些组件的帮助下,我们只需在方法上基于 @Action
注解进行标记,便能够将 ActionType
绑定到上下文。
2.3. 使用 @SmartFault
在将 ActionType
绑定到上下文之后,接下来要做的便是对 远程接口 进行配置。远程接口的配置工作主要由 @SmartFault
来完成。
其核心配置项包括:
配置项 | 含义 | 默认配置 |
---|---|---|
recover | fallback 方法名称 | |
maxRetry | 最大重试次数 | 3 |
include | 触发重试的异常类型 | |
exclude | 不需要重新的异常类型 |
接下来,看一个 demo
@Service
@Slf4j
@Getter
public class RetryService3 {
private int count = 0;
private int retryCount = 0;
private int fallbackCount = 0;
private int recoverCount = 0;
public void clean(){
this.retryCount = 0;
this.fallbackCount = 0;
this.recoverCount = 0;
}
/**
* Command 请求,启动重试机制
*/
@Action(type = ActionType.COMMAND)
@SmartFault(recover = "recover")
public Long retry(Long input) throws Throwable{
this.retryCount ++;
return doSomething(input);
}
/**
* Query 请求,启动Fallback机制
*/
@Action(type = ActionType.QUERY)
@SmartFault(recover = "recover")
public Long fallback(Long input) throws Throwable{
this.fallbackCount ++;
return doSomething(input);
}
@Recover
public Long recover(Throwable e, Long input){
this.recoverCount ++;
log.info("recover-{}", input);
return input;
}
private Long doSomething(Long input) {
// 偶数抛出异常
if (count ++ % 2 == 0){
log.info("Error-{}", input);
throw new RuntimeException();
}
log.info("Success-{}", input);
return input;
}
}
测试代码如下:
@SpringBootTest(classes = DemoApplication.class)
public class RetryService3Test {
@Autowired
private RetryService3 retryService;
@BeforeEach
public void setup(){
retryService.clean();
}
@Test
public void retry() throws Throwable{
for (int i = 0; i < 100; i++){
retryService.retry(i + 0L);
}
Assertions.assertTrue(retryService.getRetryCount() > 0);
Assertions.assertTrue(retryService.getRecoverCount() == 0);
Assertions.assertTrue(retryService.getFallbackCount() == 0);
}
@Test
public void fallback() throws Throwable{
for (int i = 0; i < 100; i++){
retryService.fallback(i + 0L);
}
Assertions.assertTrue(retryService.getRetryCount() == 0);
Assertions.assertTrue(retryService.getRecoverCount() > 0);
Assertions.assertTrue(retryService.getFallbackCount() > 0);
}
}
运行 retry 测试,日志如下:
[main] c.g.l.c.f.smart.SmartFaultExecutor : action type is COMMAND
[main] c.g.l.faultrecovery.smart.RetryService3 : Error-0
[main] c.g.l.c.f.smart.SmartFaultExecutor : Retry method public java.lang.Long com.geekhalo.lego.faultrecovery.smart.RetryService3.retry(java.lang.Long) throws java.lang.Throwable use [0]
[main] c.g.l.faultrecovery.smart.RetryService3 : Success-0
可见,当 action type
为 COMMAND
时:
-
第一次调用时,触发异常,打印: Error-0 -
此时 SmartFaultExecutor
主动进行重试,打印:Retry method xxxx
-
方法重试成功, RetryService3
打印:Success-0
方法主动进行重试,流程从异常中恢复,处理过程和效果符合预期。
运行 fallback 测试,日志如下:
[main] c.g.l.c.f.smart.SmartFaultExecutor : action type is QUERY
[main] c.g.l.faultrecovery.smart.RetryService3 : Error-0
[main] c.g.l.c.f.smart.SmartFaultExecutor : recover From ERROR for method ReflectiveMethodInvocation: public java.lang.Long com.geekhalo.lego.faultrecovery.smart.RetryService3.fallback(java.lang.Long) throws java.lang.Throwable; target is of class [com.geekhalo.lego.faultrecovery.smart.RetryService3]
[main] c.g.l.faultrecovery.smart.RetryService3 : recover-0
可见,当 action type
为 QUERY
时:
-
第一次调用时,触发异常,打印: Error-0 -
SmartFaultExecutor
执行Fallback
策略,打印:recover From ERROR for method xxxx
-
调用 RetryService3
的 recover 方法,获取最终返回值。RetryService3
打印:recover-0
异常后自动执行 fallback,将流程从异常中恢复过来,处理过程和效果符合预期。
3. 设计&扩展
3.1 核心设计

整体流程如下:
-
ActionAspect
从@Action
中读取配置信息,将请求类型绑定到线程上下文 -
然后执行正常业务逻辑 -
当调用 @SmartFault
注解的方法时,会被SmartFaultMethodInterceptor
拦截器拦截 -
拦截器通过 ActionTypeProvider
获取当前的 ActionType -
根据 ActionType
对请求进行路由 -
如果是 COMMAND
操作,将使用RetryTemplate
执行请求,在发生异常时,通过重试配置进行请求重发,从而最大限度的获得远程结果 -
如果是 QUERY
操作,将使用FallbackTemplate
(重试次数为0的RetryTemplate
)执行请求,当发生异常时,调用fallback
方法,执行配置的recover
方法,直接使用返回结果 -
获取远程结果后,执行后续的业务逻辑 -
最后, ActionAspect
将ActionType
从线程上下文中移除
4. 项目信息
项目仓库地址:https://gitee.com/litao851025/lego
推荐
PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!
原文始发于微信公众号(Java知音):容灾方案:Retry 和 Fallback 该怎么抉择?
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/93159.html