目录
背景:
在业务执行失败之后,重试一种常见的容错策略。保证数据最终的一致性。
场景:
第三方平台api调用(支付模块,邮件等等),第三方服务api调用(其他业务服务数据同步,步等),
问题:
失败场景以及造成结果:
- 网络抖动失败:面临问题->推送或者调用失败。
- 接收方没有成功,调用方失败
- 面临问题:推送失败,数据可能丢,业务数据不一致。
- 接收方没有成功,调用方失败
- 网络超时失败:面临问题->推送或者调用成功。
- 接收方处理成功,调用方失败
- 面临问题:调用方以为失败了,但接收方却接收到了,造成幂等问题。
- 接收方处理成功,调用方失败
- 业务处理失败:接收方正常,返回业务错误码,导致失败。
- 面临问题:失败就是失败了,需要人工干预。那么怎么通知开发人员,所以采用邮件,短信等等机制。
方案:
思考:
面临上面失败问题,怎么去保证提高推送成功的可能性,因此想到重试,所以重试是为了提高成功的可能性。
重试引发问题:
那么我们重试要
- 重试少次?
- 怎么实现重试策略?
因为一直重试可能会造成业务线程一直被重试占用,这样会导致服务的负载线程暴增直至服务宕机,因此需要限制重试次数。
那么如果是因为网络抖动,服务断线,我们一直重试,会造成重试浪费,造成资源浪费。所以怎么样去采用重试的时机。
思考:
那么既然有重试次数,如果我们重试也都失败了怎么办?不还是失败了嘛。
不也是可能会失败嘛。
所以我们知道重试只是提高成功的可能性。由此看,所有的方案都是提高成功的概率,如果重试都成功,服务挂了,全部断网,等等,一样会失败。所以我们实现都是近似值。到达一定程度,只能通过人为干预。
实现重试:
重试有哪些方案?怎么选择?
通过上面背景与问题分析,可以总结重试机制要素
- 限制重试次数
- 每次重试的时间间隔
- 最终失败结果的报警或事物回滚
- 在特定失败异常事件情况下选择重试
有了这些要素,就知道了我们的目标
我们的目标是实现一个优雅的重试机制,那么先来看下怎么样才算是优雅。
- 无侵入:这个好理解,不改动当前的业务逻辑,对于需要重试的地方,可以很简单的实现
- 可配置:包括重试次数,重试的间隔时间,是否使用异步方式等
- 通用性:最好是无改动(或者很小改动)的支持绝大部分的场景,拿过来直接可用
所以我们猜测:
切面方式
这个思路比较清晰,在需要添加重试的方法上添加一个用于重试的自定义注解,然后在切面中实现重试的逻辑,主要的配置参数则根据注解中的选项来初始化
优点:
- 真正的无侵入
缺点:
- 某些方法无法被切面拦截的场景无法覆盖(如spring-aop无法切私有方法,final方法)
- 直接使用aspecj则有些小复杂;如果用spring-aop,则只能切被spring容器管理的bean
消息总线方式
这个也比较容易理解,在需要重试的方法中,发送一个消息,并将业务逻辑作为回调方法传入;由一个订阅了重试消息的consumer来执行重试的业务逻辑
优点:
- 重试机制不受任何限制,即在任何地方你都可以使用
- 利用
EventBus
框架,可以非常容易把框架搭起来
缺点:
- 业务侵入,需要在重试的业务处,主动发起一条重试消息
- 调试理解复杂(消息总线方式的最大优点和缺点,就是过于灵活了,你可能都不知道什么地方处理这个消息,特别是新的童鞋来维护这段代码时)
- 如果要获取返回结果,不太好处理, 上下文参数不好处理
模板方式
把这个单独捞出来,主要是某些时候我就一两个地方要用到重试,简单的实现下就好了,也没有必用用到上面这么重的方式;而且我希望可以针对代码快进行重试
优点:
- 简单(依赖简单:引入一个类就可以了; 使用简单:实现抽象类,讲业务逻辑填充即可;)
- 灵活(这个是真正的灵活了,你想怎么干都可以,完全由你控制)
缺点:
- 强侵入
- 代码臃肿
第三方组件
guava-retrying
和 spring-retry
实际上是更好的选择,设计与实现都非常优雅,实际的项目中完全可以直接使用
框架中应用
- rocketMQ 消息可靠性
- spring cloud 注册中心
思考
既然我们有了实现的方式,那么思考,上面的问题是不是都解决了呢?
- 好像重试结束后还是失败没解决
我们可以采用死信队列DLQ。因为
重试就会导致后续的消费无法被消费,就会导致消息的堆积。所以他就把是失败的消息,给另外一个队列,然后去处理就行 了。
还有一种处理方式:
我们不希望消息,重试这么多,
那么我们会有一个消息重试记录表,
就是判断多少次,之后,我们持久化到数据库,
- 好像幂等问题没有解决
他会造成重复消费问题。
怎么解决呢?
第三方服务比较好解决:通过状态机幂等 或者访问标识,每一个请求携带一个请求。
第三方平台怎么解决?
当时支付请求发起成功,突然断网,发起方判断不了是否支付成功,此时如何处理?
状态判断