在一些业务场景中,对于数据的强一致性并不是严格要求。比如用户购买商品时增加用户的积分,对于用户来说,重要的是下单成功,至于增加积分相对不是非常重要,在一段时间内保证用户的积分增加成功即可。对于这类场景,可以使用最终一致性的方案,带来的好处是可以增加应用的可用性以及qps。
本文介绍实现最终一致性的俩种方案,本地事务表和事务消息,俩种方案的核心思想都是将一个大事务拆分长一系列的小事务,通过重试机制保证一致性。
本地事务表
核心思想来自于本地事务表 (An Acid Alternative),具体流程如下图
- 利用本地事务能力,将业务操作和存储消息放在一个事务中,保证业务操作成功的同时消息也被存储。
- 发送消息,如果发送成功则更新本地数据库中的消息是已发送状态。
- 定时任务重新发送为发送成功的消息,保证消息一定能够发送成功。
- 消息服务器转发消息到下游,同时利用消息服务器的本地存储以及重试机制,保证消息能够被下游消费。
- 下游服务监听并消费消息,为解决消息的重复投递,下游服务需要具备幂等的能力。
解释下,上面为什么没有把发送消息和业务操作放在同一个事务中。假设放在一个事务中,如果发送消息失败,事务进行回滚,这种情况没有问题。但是如果是发送消息超时导致的失败,同时消息又发送出去,回滚事务,会出现不一致的情况。因此可以得到的是发送消息和业务操作放在一个事务中不合适。而通过定时任务监听未发送成功的消息,补偿进行重新发送,可以弥补消息未发送成功的情况。
通过上面的介绍可以看出,本地事务表适用于可异步执行的业务,且后续操作无需回滚的业务。
事务消息
基于本地消息的最终一致性方案通过引入本地消息表和轮询与重试机制的形式保障最终一致性,但是对业务侵入很大,因为每个上游业务都需要新增一张消息表,针对这个问题,产生了独立消息服务的最终一致性方案。这种方案需要消息服务器具备事务消息的能力。下图是RocketMQ事务消息的基本流程
- 事务发起方首先发送半事务消息(half 消息)到MQ。
- 在发送半事务消息成功后执行本地事务。
- 根据本地事务执行结果返回commit或者是rollback。
- 如果消息是rollback,MQ将删除半事务消息不进行下发,如果是commit消息,MQ 将会把这个消息发送给 consumer 端。
- 如果执行本地事务过程中,执行端挂掉,或者超时,MQ将会不停的询问其同组的其它producer来获取状态。
- Consumer端的消费成功机制有MQ保证。
具体的实现方案可以参考 基于RocketMQ分布式事务 - 完整示例 和 收发事务消息
总结
本地事务表和事务消息俩种方案的核心思想基本一致,都是利用消息的重试机制,来达到最终一致性。只是如何确保本地事务执行成功后,发送消息的方式不同。
上面一直说的是下游业务方能够执行成功的情形,对于执行不成功的情况,可以通过发送通知到对应的人员,进行手动处理,相对来说这种情况出现的几率比较小,如果大量出现,则很大情况是业务逻辑出了问题,需要检查下业务代码。对于Rocketmq,如果达到最大重试次数都没有消费成功,则会把消息放置到死信队列,具体可以参考死信队列,可以监控死信队列来发现异常情形。