• 重试机制思考与实现


    目录

    背景:

    场景:

    问题:

    失败场景以及造成结果:

    方案:

    思考:

    重试引发问题:

    思考:

    实现重试:

    切面方式

    消息总线方式

    模板方式

    第三方组件

    思考


    背景:

    在业务执行失败之后,重试一种常见的容错策略。保证数据最终的一致性。

    场景:

    第三方平台api调用(支付模块,邮件等等),第三方服务api调用(其他业务服务数据同步,步等),

    问题:

    失败场景以及造成结果:

    1. 网络抖动失败:面临问题->推送或者调用失败。
      1. 接收方没有成功,调用方失败
        1. 面临问题:推送失败,数据可能丢,业务数据不一致。
    2. 网络超时失败:面临问题->推送或者调用成功。
      1. 接收方处理成功,调用方失败
        1. 面临问题:调用方以为失败了,但接收方却接收到了,造成幂等问题。
    3. 业务处理失败:接收方正常,返回业务错误码,导致失败。
      1. 面临问题:失败就是失败了,需要人工干预。那么怎么通知开发人员,所以采用邮件,短信等等机制。

    方案:

    思考:

    面临上面失败问题,怎么去保证提高推送成功的可能性,因此想到重试,所以重试是为了提高成功的可能性。

    重试引发问题:

    那么我们重试要

    1. 重试少次?
    2. 怎么实现重试策略?

    因为一直重试可能会造成业务线程一直被重试占用,这样会导致服务的负载线程暴增直至服务宕机,因此需要限制重试次数。

    那么如果是因为网络抖动,服务断线,我们一直重试,会造成重试浪费,造成资源浪费。所以怎么样去采用重试的时机。

    思考:

    那么既然有重试次数,如果我们重试也都失败了怎么办?不还是失败了嘛。

    不也是可能会失败嘛。

    所以我们知道重试只是提高成功的可能性。由此看,所有的方案都是提高成功的概率,如果重试都成功,服务挂了,全部断网,等等,一样会失败。所以我们实现都是近似值。到达一定程度,只能通过人为干预。

    实现重试:

    重试有哪些方案?怎么选择?

    通过上面背景与问题分析,可以总结重试机制要素

    • 限制重试次数
    • 每次重试的时间间隔
    • 最终失败结果的报警或事物回滚
    • 在特定失败异常事件情况下选择重试

    有了这些要素,就知道了我们的目标

    我们的目标是实现一个优雅的重试机制,那么先来看下怎么样才算是优雅。

    • 无侵入:这个好理解,不改动当前的业务逻辑,对于需要重试的地方,可以很简单的实现
    • 可配置:包括重试次数,重试的间隔时间,是否使用异步方式等
    • 通用性:最好是无改动(或者很小改动)的支持绝大部分的场景,拿过来直接可用

    所以我们猜测:

    切面方式

    这个思路比较清晰,在需要添加重试的方法上添加一个用于重试的自定义注解,然后在切面中实现重试的逻辑,主要的配置参数则根据注解中的选项来初始化

    优点:

    • 真正的无侵入

    缺点:

    • 某些方法无法被切面拦截的场景无法覆盖(如spring-aop无法切私有方法,final方法)
    • 直接使用aspecj则有些小复杂;如果用spring-aop,则只能切被spring容器管理的bean

    消息总线方式

    这个也比较容易理解,在需要重试的方法中,发送一个消息,并将业务逻辑作为回调方法传入;由一个订阅了重试消息的consumer来执行重试的业务逻辑

    优点:

    • 重试机制不受任何限制,即在任何地方你都可以使用
    • 利用EventBus框架,可以非常容易把框架搭起来

    缺点:

    • 业务侵入,需要在重试的业务处,主动发起一条重试消息
    • 调试理解复杂(消息总线方式的最大优点和缺点,就是过于灵活了,你可能都不知道什么地方处理这个消息,特别是新的童鞋来维护这段代码时)
    • 如果要获取返回结果,不太好处理, 上下文参数不好处理

    模板方式

    把这个单独捞出来,主要是某些时候我就一两个地方要用到重试,简单的实现下就好了,也没有必用用到上面这么重的方式;而且我希望可以针对代码快进行重试

    优点:

    • 简单(依赖简单:引入一个类就可以了; 使用简单:实现抽象类,讲业务逻辑填充即可;)
    • 灵活(这个是真正的灵活了,你想怎么干都可以,完全由你控制)

    缺点:

    • 强侵入
    • 代码臃肿

    第三方组件

    guava-retryingspring-retry 实际上是更好的选择,设计与实现都非常优雅,实际的项目中完全可以直接使用

    框架中应用

    1. rocketMQ 消息可靠性
    2. spring cloud 注册中心

    思考

    既然我们有了实现的方式,那么思考,上面的问题是不是都解决了呢?

    1. 好像重试结束后还是失败没解决

    我们可以采用死信队列DLQ。因为

    重试就会导致后续的消费无法被消费,就会导致消息的堆积。所以他就把是失败的消息,给另外一个队列,然后去处理就行 了。

    还有一种处理方式:

    我们不希望消息,重试这么多,

    那么我们会有一个消息重试记录表,

    就是判断多少次,之后,我们持久化到数据库,

    1. 好像幂等问题没有解决

    他会造成重复消费问题。

    怎么解决呢?

    第三方服务比较好解决:通过状态机幂等 或者访问标识,每一个请求携带一个请求。

    第三方平台怎么解决?

    当时支付请求发起成功,突然断网,发起方判断不了是否支付成功,此时如何处理?

    状态判断

  • 相关阅读:
    小白转行入门STM32----手机蓝牙控制STM32单片机点亮LED
    校园安防智能视频行为分析预警系统解决方案
    [buuctf.reverse] 126-130
    本科论文数据匹配不上有没有朝阳区群众zhidian
    在线副业教程之 01 如何通过编码赚钱的 6 种方法
    [Leetcode]用队列实现栈
    Redis 消息队列:构建消息代理的 4 个简单步骤
    我的创作纪念日
    面试经典题型:调用HashMap的put方法的具体执行流程
    【微信小程序篇】- 多环境(版本)配置
  • 原文地址:https://blog.csdn.net/gududedabai/article/details/127940492