问题背景
- 前台(浏览器或app等)提交一个请求到A系统,A系统调B系统创建订单,同时A系统需要扣除金币(数据库操作)。这是一个跨进程事务,需要保持两个系统的数据一致性。
- 如果数据都保存在B系统,则没有系统一致性问题,但通常由于业务需要,尤其是系统拆分之后,经常需要处理分布式一致事务问题。
问题分析和方案
系统调用的结果
- 调B系统可能出现以下三种结果:
- 成功
- 失败
- 超时(未响应)
方案一
- A系统把调B系统创建订单(调用成功)和扣除金币(数据库操作成功)绑定成进程内的一个事务。
造成数据不一致的原因有:
- 调B系统超时(上述第3种情况)。发生的原因通常是网络问题。 这种情况一般A系统当失败处理,导致A系统未扣除金币但B系统已经有订单。
- 调B系统成功,但A系统刚好宕机(或重启,断电等原因)。导致跟结果跟1一致。
解决方案
- B系统提供接口供B系统获取订单结果。(确认机制)
- 定时同步B系统所有订单数据(这些记录在A系统都是读操作),同步的机制可以是增量的(按照时间段同步),同步结果对比金币的扣除情况,若不一致,则需要进行扣除。
方案二
- 解耦,异步任务处理。由原来同步调B系统,变成异步调B系统。将A系统扣除金币和调B系统创建订单,这两个操作分开执行。
- A系统接收前台请求之后,先扣除金币,然后生成一个调B系统创建订单的任务(任务落地,状态为待执行)。这两个动作都是数据库操作,同一个数据库事务。
- 后台定时取待执行的任务调B系统创建订单,包含重试机制和策略。
注意点
- 异步调B系统有可能出现失败的情况,因为B系统提供的接口可能是具有时效性的,或者进行了严格的参数校验。所以需要了解B系统接口的校验规则,在前台提交请求时做好校验,提高后续异步调B系统的成功率。
- 在异步调B系统失败的情况下,需要对金币进行回退,并把用户创建订单的状态由执行中置为失败。
总结
- 以上两种方案对B系统的要求有,创建订单接口需保证幂等性,可以返回相应的错误码。实现幂等性可以使用业务中的唯一标志,或者UUID来进行。
- 两种方案都可以保证数据的最终一致性,但如果业务上无太硬性要求,且不一致的发生概率较低的情况下,可以使用调接口报错以及数据监控告警,双方线下确认,通过相应补偿机制来解决。
- 由前台提交创建订单防止重提交 (UUID,数据库索引,集中式cache)
引入第三方组件
notify
- 引入异步消息队列解耦,相当于上述方案二的升级版。组件具有相关的确认机制,如淘宝的notify。
- notify使用两阶段提交的机制(预提交和确认提交)。若notify在一定的时间内未收到确认提交请求(如A系统执行完扣除金币动作后,在发出确认请求到notify之前挂掉),则会回调A系统提供的check接口进行确认(A系统需要实现check接口,决策消息commit还是rollback)
- 淘宝的消息中间件:https://segmentfault.com/a/1190000003059871
其他
除了notify,另一种消息队列的实现:
http://mp.weixin.qq.com/s/x9IRp4-1N4otIVBEEIE-og
http://mp.weixin.qq.com/s/h74d6LtGB5M8VF0oLrXdCA
http://mp.weixin.qq.com/s/Brd-j3IcljcY7BV01r712Q
总结
为了尽量保证消息必达,架构设计方向为:
(1)消息收到先落地
(2)消息超时、重传、确认保证消息必达
一种提高微服务架构的稳定性与数据一致性的方法:http://mp.weixin.qq.com/s/ROVuCPr2Rg3G1m_daYu-Vg
同步转异步,解决稳定性问题
在平时的时候,都是 RPC 同步调用。如果调用失败了,则自动把同步调用降级为异步的。消息此时进入队列,然后异步被重试。所以处理下游依赖就变成了三种可能性:
- 完全强依赖,下游不能挂。
- 因为我的返回值依赖了某个下游的处理结果,我必须同步调用它。但是不是强依赖,可降级。降级时不返回这部分的数据。同步调用降级时转为异步的。
- 完全异步化。下游服务只是消费我写入的队列,我不与之直接RPC通信。