一文详解分布式事务之基于消息投递与消费的最终一致性
1 前言
微服务架构中,服务的细分,导致原来在单一服务中的模块拆分为多个服务,同时数据存储也从单一的数据源,变为按服务划分的多数据源。单一服务单数据源时,可以通过数据库本身的ACID机制(Atomicity 原子性、Consistency 一致性、Isolation 隔离性、Durability 持久性)保证业务数据的一致性(通常称作本地事务),微服务架构下通常采用BASE机制(Basically Available 基本可用、**Soft State 软状态 、Eventual Consistency 最终一致性 **)来保证系统间数据的最终一致性。
分布式事务通常可采用2PC、3PC、TCC、SAGA、最大努力通知与本文将要介绍的基于消息投递与消费的最终一致性实现。2PC 与 3PC 依赖于数据库的事务能力,在一阶段开始事务后不进提交会严重影响应用程序的并发性能 ,实际业务中基本不会采用。TCC 相于2PC与3PC而言并发性能更高,是一种柔性事务,但要求业务侵入性较大且实现复杂,要求业务侧实现Try、Cancel、Confirm方法,同时为了解决网络通信失败或超时带来的异常情况,要求业务方在设计实现要允许空回滚、操作幂等性、防止资源悬挂,其比较适合数据一致性要求较高的业务场景,如组合支付,订单减库存。SAGA 其支持的并发性相对TCC而言更高,业务侵入性较低,适合长事务的业务场景。
2 基于消息投递与消费的最终一致性
基于消息投递与消费的最终一致性 这类实现方案可以细分为本地消息表、可靠消息服务、事务消息(可靠消息服务的特例),由于这些方案本质上都是将跨系统的业务操作变成可靠的消息投递与消费,达到将分布式事务拆分为多个本地事务的目地,实现系统间数据的最终一致性,就将这类方案统称为基于消息投递与消费的最终一致性。
2.1 本地消息表
本地消息表该方案,由 eBay 的系统架构师 Dan Pritchett 在 2008 年在 ACM 发表的论文《Base: An Acid Alternative》中提出的,BASE机制也是他在该论文中提出的。先看一下直接用本地事务能否保证系统间数据的最终一致性。
以常见的电商平台,用户购买商品创建订单为例,订单系统在创建订单时,通常会采用预扣减库存的方式来避免超售,用户支付成功后再真正扣减库存,对于配置时间内超时未支付的订单,会将预扣减的库存归还。为了提高订单创建的效率,将订单系统中订单生成与预扣减库存二者异步解耦,即订单系统内生成订单再向MQ投递预扣减库存的消息。
订单系统内部分为4步,分别是
用户ID加锁防止用户重复下单
通常会指定锁的自动释放时间,如1秒正常情况用户1秒内不可能真正下多单,这种情况更多是由于重复下单导致的,出现这种情况直接返回请误重复下单。
Lua脚本校验库存预扣库存
这一步主要是在缓存中校验与预扣库存
创建订单
真正创建业务订单
投递订单创建消息
订单系统向MQ投递订单创建消息
讨论分布式事务时,我们将用户ID加锁防止用户重复下单 与 Lua脚本校验库存预扣库存这两步直接跳过,只关心与事务相关的 创建订单 与 投递订单创建消息 。
常见的错误作法有下面两种:
- 在本地事务中先创建订单,后投递订单创建消息
本地事务开始
1. 创建订单
2. 投递订单创建消息
本地事务结束
这种作法存在的问题如下,第二步投递订单创建消息由于网络抖动出现超时,整个本地事务回滚,此时业务订单创建回滚,但订单创建的消息可能已投递到MQ中,最终导致商品库存被错误的预扣。
- 在本地事务中先投递订单创建消息,后创建订单
本地事务开始
1. 投递订单创建消息
2. 创建订单
本地事务结束
这种作法同样存在,投递订单创建消息由于网络抖动出现超时,整个本地事务回滚,此时业务订单未创建,但订单创建的消息可能已投递到MQ中,最终导致商品库存被错误的预扣。
那如何保证创建订单与投递订单创建消息要么一起成功,要么一起失败? 本地消息就是解决该问题,其做法如下:
1、上游系统在本地事务中新增或更新业务记录,同时新增本地消息记录,消息状态为待发送
2、上游系统中定时任务扫描本地消息表中状态为待发送的记录,将该记录投递到MQ中,MQ返回成功再更新本地消息表的状态为已发送
3、MQ推送消息供下游系统消费
4、下游系统消费MQ推着的消息,执行业务逻辑,返回ACK给MQ
注意:第2步可能出现定时任务向MQ投递重复消息的情况,第3步可能出现MQ重复推送消息的情况,所以下游在做业务处理时一定要通过一定机制保证操作的幂等性,像上面的例子不能出现重复消费创建订单消息而导致多次预扣减库存的情况,业务操作的幂等性可以参考之前的文章 分布式系统下面业务操作幂等的必要性。
本地消息表方案增加了业务系统维护消息表的成本使得事务处理部分与业务系统耦合,不能成为通用的解决方案,高并发时本地消息表的读写操作会成为系统的瓶颈,同时定时任务扫描本地消息表会增加系统之间的延时。
2.2 可靠消息最终一致性
本地消息表方案对业务的侵入性很大,不合适作为通用的解决方案。通过将本地消息的处理,转由单独的服务去完成,可以得到通用的可靠消息最终一致性解决方案。
可靠消息最终一致性方案由上游服务、可靠消息服务、下游服务组成。
可靠消息服务
可靠消息服务专门负责存储消息、投递消息、更新消息状态的独立服务。消息一般由待确认、已发送、已取消、已完成组成。
待确认
上游服务发送给可靠消息服务的待确认消息,上游服务在执行完本地事务的业务逻辑,后会对该消息进行确认或取消
已发送
上游服务本地事务执行成功,后会向可靠消息服务发送确认消息,可靠消息服务收到消息后,将消息状态由待确认更新为已发送
已取消
上游服务本地事务执行失败,后会向可靠消息服务发送取消消息,可靠消息服务收到消息后,将消息状态由待确认更新为已取消
已完成
下游服务消费完MQ中的消息完后,会向MQ投递已完成消费消息,可靠消息服务消费该消息,将消息状态由已发送更新为已完成
上游服务
上游服务,主动发起事务的一方,一般是指分布式事务中最先开始执行的那个服务。其在需要调用下游服务时,不直接通过RPC之类的方式调用,而是先生成一条消息,具体步骤如下:
- 上游服务执行业务逻辑前,先发送一条待确认消息(一般称为half-msg,包含接口调用信息)给可靠消息服务,可靠消息服务会将这条记录存储到自己的数据库(或本地磁盘)状态为【待确认】(下图中第1步与第2步)。
- 上游服务在本地事务中执行业务逻辑,如果本地事务执行成功,侧可靠消息服务发送一条确认消息;如果本地执行失败,则向消息服务发送一条取消消息 (下图中第3步与第4步)。
- 可靠消息服务,根据收到消息为确认消息或取消消息,修改本地数据库对应消息记录的状态为【已发送】或【已取消】。如果是确认消息,同时还要将消息投递到MQ消息队列中,修改消息状态和投递MQ必须在一个事务里,保证要么都成功要么都失败(下图中第 5.1 步与第5.2步)。
注意:为了防止出现生产者的本地事务执行成功,但是发送确认/取消消息超时的情况。可靠消息服务,内部一般会提供一个后台定时任务,不停的扫描消息表中消息状态为【待确认】的消息,然后回调上游服务的一个接口,由上游服务决定该消息应该确认还是取消,并发送对应消息。
下游服务
下游服务,订阅MQ消息,收到MQ的消息后执行本地事务。执行成功后会对消息进行ACK确认,同时会向MQ投递已完成消费的消息,可靠消息服务收到该消息后会更新消息表中的消息状态为【已完成】,然后再对消息进行ACK确认。
下游服务,订阅MQ消息,收到MQ的消息后执行对应的业务逻辑。
注意: 为了防止重复消息消费,下游服务业务逻辑处理要保证幂等。同时第8、第9步、每10步都可能由于系统或网络原因出现异常。第8失败,消息会再次推送(主流的MQ都会保障消息至少投递一次),下游服务业务逻辑处理要保证幂等。第9步失败,则要通过可靠消息服务中的定时任务对扫描已超时的【已发送】消息,并重新向MQ投递消息。第10步失败,同样消息会再次推送,若消息表中消息状态为【已完成】则直接再次ACK消息就好。业务操作的幂等性可以参考之前的文章 分布式系统下面业务操作幂等的必要性。
2.3 事务消息
事务消息,该方案也叫可靠消息最终一致性,很多开源的消息中间件都支持分布式事务,比如RocketMQ、Kafka。其核心其思想和本地消息表、可靠消息服务是一样的,只不过是将可靠消息服务和MQ功能封装在一起,屏蔽了底层细节,从而更方便用户的使用。
RocketMQ在4.3.0已经支持分布式事务消息,这里RocketMQ采用了2PC的思想来实现了提交事务消息,同时增加一个补偿逻辑来处理二阶段超时或者失败的消息,如下图所示。
上图说明了事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。
- 事务消息发送及提交
(1) 上游服务向MQ Server 发送half消息。
(2) MQ Server 服务端响应消息写入结果 (此时half消息对下游消息订阅者不可见)。
(3) 上游服务根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)。
(4) 上游服务根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)
- 补偿流程
(5) 对没有Commit/Rollback的事务消息(pending状态的消息),MQ Server向上游服务发起一次“回查”
(6) 上游服务收到回查消息,检查回查消息对应的本地事务的状态
(7) 上游服务根据本地事务状态,重新Commit或者Rollback
其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。
总结
本文总结了基于消息投递与消费的最终一致性实现分布式事务的主要方案本地消息表、可靠消息服务、事务消息,这些方案本质上都是将跨系统的业务操作变成可靠的消息投递与消费,达到将分布式事务拆分为多个本地事务的目地,实现系统间数据的最终一致性。
参考文档
结尾
原创不易,点赞、在看、转发是对我莫大的鼓励,关注公众号洞悉源码是对我最大支持。同时相信我会分享更多干货,我同你一起成长,我同你一起进步。