前面我们讲了分布式事务的2PC、3PCTCC 的原理。这些事务其实都在尽力的模拟数据库的事务,我们可以简单的认为他们是一个同步行的事务。特别是 2PC,3PC 他们完全利用数据库的事务能力,在一阶段开始事务后不进提交会严重影响应用程序的并发性能。TCC 一阶段虽然不会阻塞数据库,但是它同样是在尽力追求同时成功同时失败的一致性要求。但是在很多时候,我们的应用程序的核心业务为了追求更高的性能、更高的可用性,可以允许在一段时间内的数据不一致性,只需要在最终时刻数据是一致就可以了。基于以上场景我们可以采用基于可靠消息服务的最终一致性分布式事务处理方案。
总体架构
可靠消息最终一致性的架构上分3个部分:
主动方:主动发起事务的一方,一般是指分布式事务中最先开始执行的那个服务,也是核心业务服务
可靠消息服务:可靠消息服务由关系型数据库、消息队列MQ等组件组成,利用关系型数据库的强一致性特性来持久化消息的状态,利用MQ来保证消息的可靠投递及消费
被动方:被动方订阅MQ的消息,当收到MQ的消息后执行对应的业务 以上是比较粗狂的结构图,下面我们来详细分析一下这个事务的执行过程。
流程
该方案总体流程上可分为以下步骤:
主动方在真正的业务开始前先向可靠消息服务发送一个“待确认”的消息
可靠消息服务收到待确认消息后持久化消息到数据库
如果以上操作成功则主动方开始真正的业务,如果失败则直接放弃执行业务
如果业务执行成功则发送“确认”消息给可靠消息服务,如果执行失败则发送“取消”给可靠消息服务。
如果可靠消息服务收到“确认”消息则更新数据库里的消息记录的状态为“待发送”,如果收到的消息为“取消”则更新消息状态为“已取消”
如果上一步更新的数据库为“待发送”,那么会开始往MQ投递消息,并且更改数据库里的消息记录的状态为“已发送”
上一步往MQ投递消息成功后,MQ会给被动方推送消息。
被动方收到消息后开始处理业务
如果业务处理成功,则被动方对MQ进行ACK回复,则这条消息会从MQ内移除掉
如果业务处理成功,则发送“已完成”消息给可靠消息服务
可靠消息服务收到“已完成”消息后更新数据库消息记录未“已完成”
异常处理
以上我们描述的是一套在理想情况下执行的逻辑。但是分布式系统由于网络的存在,网络的不可靠性会导致我们消息的传递没办法100%成功。我们的可靠消息服务跟主动方、被动方之间的交互也是分布式的,这就需要我们在流程上有很多补偿的机制。以下我们来讨论一些异常情况:
如果步骤1发送“待确认”消息失败,主动方业务不会执行,直接放弃事务,不会有影响
如果步骤1发送“待确认”消息成功,并且可靠消息已经更新“待确认”成功,但是由于网络问题,比如超时,主动方得到的结果是失败,主动方会放弃执行事务,标记为“已取消”。这个时候就会出现可靠消息服务跟主动方的状态出现不一致的情况。
为解决这个问题,我需要主动方提供一个事务状态查询接口,可靠消息服务这边则启动一个定时任务,定时去查这些长时间处于待确认的事务,然后通过主动方的接口确认这些事务是已执行,还是已取消。如果步骤4,主动方发送“确认”消息失败,可靠消息服务会通过定时任务通过主动方的查询接口去确认状态。
步骤6,投递消息给MQ跟更新状态为“已发送”,是这个流程中至关重要的一步。理想的流程是整个2个操作同时成功同时失败,保持强一致性。网上很多文章都会说“为了保证发送MQ消息跟更新消息状态同时成功同时失败,需要把这个2个步骤写同一个本地事务中”。这是完全不靠谱的,数据库跟MQ本就是2个独立的服务,如果通过一个本地的事务就能保证一致性,那么我们现在讨论的分布式事务毫无意义,直接写在一个本地事务里不就完了么。
写在本地事务内只能尽可能的保证数据库更新跟MQ投递消息同时成功,但是并不能保证100%一致。
以下我们来分2种情况分析失败的情况:
事务开始
1. send to mq
2. database update
事务结束
先发送 MQ 消息,可能 MQ 消息发送成功,但是database由于某些原因更新失败了。数据库可以回滚,但是通常的MQ没有回滚能力。
事务开始
1. database update
2. send to mq
事务结束
先更新数据库,再发送 MQ 消息,同样会有问题。就算1,2都执行成功了,但是事务是需要提交的,数据库有可能在提交阶段失败,数据库是可以回滚,但是 MQ 的消息已经发出去了,它并没有回滚的能力。
为了解决这个问题,我们同样需要补偿机制。在可靠消息服务一侧开启定时任务,定时去查询那些长期处于“待发送\已发送”的事务,再次对 MQ 进行投递消息。这个机制有可能造成被动方重复收到 MQ 的消息,这就需要被动方处理业务的时候要进行幂等处理。
总结
通过以上我们详细介绍了可靠消息最终一致性事务解决方案的总体结构跟执行的流程,以及对异常情况的一些补偿方法,总体流程上还是比较清晰简单的。但是可靠消息最终一致性方案在使用上也是具有比较强的局限性,因为它的异步特性跟有可能出现的高延时性不适合处理一些敏感业务。比如它适合处理消费新增积分场景,但是不合适处理积分兑换礼品的场景。因为如果积分扣减延迟了,那么用户就可能兑换超出本身积分多的多的礼品。所以我们选择分布式事务的时候还需根据场景来进行选择。
好了讲了这么多分布式事务的原理,下一期我们使用 .NET 真正的实现一个分布式事务,敬请期待。
.Net Core with 微服务 - 什么是微服务
.Net Core with 微服务 - 架构图
.Net Core with 微服务 - Ocelot 网关
.Net Core with 微服务 - Consul 注册中心
.Net Core with 微服务 - Seq 日志聚合
.Net Core with 微服务 - Elastic APM
.Net Core with 微服务 - Consul 配置中心
.Net Core with 微服务 - Polly 熔断降级
.Net Core with 微服务 - 分布式事务 - 2PC、3PC
.Net Core with 微服务 - 分布式事务 - TCC