Redian新闻
>
容灾方案:Retry 和 Fallback 该怎么抉择?

容灾方案:Retry 和 Fallback 该怎么抉择?

公众号新闻

点击上方“芋道源码”,选择“设为星标

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 10:33 更新文章,每天掉亿点点头发...

源码精品专栏

 
来源:geekhalo

1. 概览

在分布式场景中,Retry 和 Fallback 是最常见的容灾方案。

  1. Retry 就是在调用远程接口失败时,Client 主动发起重试请求,以期待获得最终结果,从而完成整个流程
  2. Fallback 是在调用远程接口失败时,Client 不进行重试而是调用一个特殊的 fallback 方法,从这个方法中获取结果,使流程能够继续下去

那 Retry 和 Fallback 该怎么抉择呢?

1.1. 背景

首先,先看下 Retry 和 Fallback 都是怎么帮助流程进行自我恢复的。

1.1.1. Retry

现在有一个生单流程:

核心流程如下:

  1. 从商品服务中获取商品信息
  2. 根据商品信息创建订单
  3. 将订单保存到数据库

如果发生网络抖动,将导致生单失败。

  1. 在调用商品服务获取商品时,由于网络异常,接口调用失败
  2. 由于无法获取商品信息,生单流程被异常中断

由于生单流程太过重要,系统需尽最大努力保障用户能够完成下单操作,那针对网络抖动这个问题,可以通过 Retry 进行修复。

  1. 在第一次获取商品信息时,由于网络问题导致获取失败
  2. 系统不会直接抛出异常,而是在等待一段时间后,重新发起第二次请求,也就是 Retry 操作
  3. 网络恢复,第二次请求成功获取商品信息
  4. 流程继续运行,最终完成用户生单

Retry 机制非常适合服务短时间不可用,或某个服务节点异常 这类场景。

1.1.2. Fallback

一个生单验证接口,主流程如下:

  1. 调用商品服务的接口获取商品信息
  2. 根据商品和用户信息判断用户是否能够购买该商品

同样,假设在访问商品服务时出现网络异常:

由于无法获取商品信息,从而导致整个验证流程被异常中断,用户操作被迫终止。

聪明的你估计会说那就使用 Retry 呀,是的:

如果是短时不可用,通过 Retry 机制便可以恢复流程。

但,如果是商品服务压力过大,响应时间过长呢?比如,商品服务流量激增,导致 DB CPU 飙升,出现大量的慢 SQL,这时触发了系统的 Retry 会是怎样?

  1. 在获取商品失败后,系统自动触发 Retry 机制
  2. 由于是商品服务本身出了问题,第二次请求仍旧失败
  3. 服务又触发了第三次请求,仍未获取结果
  4. 达到最大重试次数,仍旧无法获取商品,只能通过异常中断用户请求

通过 Retry 机制未能将流程从异常中恢复过来,也给下游的 商品服务 造成了巨大伤害。

  1. 商品服务压力大,响应时间长
  2. 上游系统由于超时触发自动重试
  3. 自动重试增大了对商品服务的调用
  4. 商品服务请求量更大,更难以从故障中恢复

这就是常说的“读放大”,假设用户验证是否能够购买请求的请求量为 n,那极端情况下 商品服务的请求量为 3n (其中 2n 是由 Retry 机制造成)

此时,Retry 就不是一个好的方案。我们先退回业务场景进行思考,如果无法获取商品,验证接口是否可以直接放行,先让用户完成购买?

如果,这个业务假设能够接受的话,那就到了 Fallback 上场的时候了。

  1. 调用商品服务获取商品信息失败
  2. 系统不会进行重试,而是触发 fallback 机制
  3. fallback 会调用指定的一个方法,并将返回值作为远程接口的返回值
  4. 接下来的流程使用 fallback 方法的返回值完成业务逻辑

1.1.3. 场景思考

同样是对商品服务接口(同一个接口)的调用,在不同的场景需要使用不同的策略用以恢复业务流程,通常情况下:

  1. Command 场景优先使用 Retry


    • 这种流量即为重要,最好能保障流程的完整性
  • 通常写流量比较小,小范围 Retry 不会对下游系统造成巨大影响
  1. Query 场景优选使用 Fallabck


    • 大多数展示场景,哪怕部分信息没有获取到对整体的影响也比较小
  • 通常读场景流量较高,Retry 对下游系统的伤害不容忽视

那面对一个远程接口被多个场景使用,我们该怎么处理呢?

  1. 提供两组接口,一个具有 Retry 能力,一个具有 Fallback 能力,由使用方根据业务场景进行选择?
  2. 还是…

1.2. 目标

  1. 远程接口具备 Retry 和 Fallback 能力
  2. 能够根据上下文不同场景,在发生调用异常时动态选择 Retry 或 Fallback 进行流程恢复

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

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 又是怎么进行管理的呢,包括信息绑定和信息清理? 最常用的方式便是:

  1. 提供一个注解,在方法上添加注解用于对 ActionType 的配置;
  2. 提供一个拦截器,对方法调用进行拦截。方法调用前,从注解中获取配置信息并绑定到上下文;方法调用后,主动清理上下文信息;

核心实现为:

@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 来完成。 其核心配置项包括:

配置项含义默认配置
recoverfallback 方法名称
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 时:

  1. 第一次调用时,触发异常,打印: Error-0
  2. 此时 SmartFaultExecutor 主动进行重试,打印: Retry method xxxx
  3. 方法重试成功,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]
[mainc.g.l.faultrecovery.smart.RetryService3  : recover-0

可见,当 action type 为 QUERY 时:

  1. 第一次调用时,触发异常,打印: Error-0
  2. SmartFaultExecutor 执行 Fallback 策略,打印:recover From ERROR for method xxxx
  3. 调用RetryService3的 recover 方法,获取最终返回值。RetryService3 打印:recover-0

异常后自动执行 fallback,将流程从异常中恢复过来,处理过程和效果符合预期。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

3. 设计&扩展

3.1 核心设计

整体流程如下:

  1. ActionAspect 从 @Action 中读取配置信息,将请求类型绑定到线程上下文

  2. 然后执行正常业务逻辑

  3. 当调用 @SmartFault 注解的方法时,会被 SmartFaultMethodInterceptor 拦截器拦截


    1. 拦截器通过 ActionTypeProvider 获取当前的 ActionType
  4. 根据 ActionType 对请求进行路由

  5. 如果是 COMMAND 操作,将使用 RetryTemplate 执行请求,在发生异常时,通过重试配置进行请求重发,从而最大限度的获得远程结果

  6. 如果是 QUERY 操作,将使用 FallbackTemplate(重试次数为0的 RetryTemplate)执行请求,当发生异常时,调用 fallback 方法,执行配置的 recover 方法,直接使用返回结果

  7. 获取远程结果后,执行后续的业务逻辑

  8. 最后,ActionAspect 将 ActionType 从线程上下文中移除

4. 项目信息

项目仓库地址:https://gitee.com/litao851025/lego

项目文档地址:https://gitee.com/litao851025/lego/wikis/support/smart-fault



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

已在知识星球更新源码解析如下:

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*)

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
Friends, Real and Imagined: The Poetry of Wang Yin道德补偿理论compensatory ethics theoryUNReal 每周一场主题电音节|本周六𝘿𝙧𝙪𝙢 𝙣' 𝘽𝙖𝙨𝙨 炸裂舞池!Treg的功能鉴定:构建Treg和Responder T共培养体系React官方网站更新,并启用新域名:react.dev直播:后疫情时代,新移民在美国置业如何抉择?Times Square Billboard Attracts Chinese Attention Seekers恭喜Emory学员斩获BlackRock (US) 2023 Full-time Offer!关于容灾及备份的关键指标【Career Forum|4.1】Fight the Career Winter in the Tech Industry!2022南美南极行(3)巴西 里约热内卢【4.7折扣】资生堂7折!AMI/Hollister大促6折起!Charlotte Tilbury 8折!闲聊两部电影,三位“Ryan”(下)【外所】美国伟凯,北京,实习,LLM/LLB,英语优秀!英国咨询| 日常实习推荐: blackrock/state street/Natixis已开启,23/24届可投!最高院发布《关于渝金融法院案件管辖的规定》;ALB、GDR、LEGALBAND公布多项奖项榜单|律所动态渡十娘|怎么看懂《瞬息全宇宙》(《Everything Everywhere All At Once》)王羲之“遮蔽”了哪些笔法?是真的吗?毕业后收入最高的10个专业有8个属于工程学!面对这种“诱惑”,该怎么抉择?肯德基Triple Stacker Burger三层汉堡,$12.95!在secret menu “隐藏菜单”里哦【外所】孖士打,实习,北京,5000元/月,LLB/LLM/JD优先英国人气本土品牌3折起!罕见低价收Mulberry!疫情众生相With More People Getting Sick, China’s Restaurants Are Strugglin【波士顿最顶级公寓|紧邻Chinatown和Newbury Street|奢华大气|私人隐私|近绿线地铁】LG 55LB7500 55" Smart WebOS Full HD LED LCD 3D TV no controller三阴性乳腺癌如何抉择治疗方案?治疗有进展吗?香港春招 | Jane Street开放Sales and Trading InternshipEverythingToolbar 1.0发布,支持Windows 11邀请函 | 3/29 Braintree议长梅雷迪思·博里克(Meredith Boericke) 筹款见面会Crackdown Continues on Illegal ‘Competitions’ Targeting StudentsShanghai Vows Greater Market Access for Economic Recovery没有帝王之争,也没有帝国之争,只有文明之争[评测]AMD Ryzen 9 7900/Ryzen 7 7700/Ryzen 5 7600 评测
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。