本文参考https://zhuanlan.zhihu.com/p/648556608,在小徐的基础上做了个人的笔记。
在聊分布式事务之前,我们先理清楚有关于 “事务” 的定义.
事务 Transaction,是一段特殊的执行程序,其需要具备如下四项核心性质:
当涉及到事务处理时,有四个核心要素,它们被称为事务的ACID四大特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。这些特性在关系型数据库范围内通常较容易实现,因为数据和操作都在同一个数据库内。然而,当一个事务涉及到跨越不同的数据库、服务或存储组件时,这个问题就变得更加复杂和有趣,这正是我们今天要重点讨论的“分布式事务”领域所涉及的问题。
由于数据库的拆分或分布式架构(微服务)不可避免的带来了分布式事务的问题。如下为当前针对分布式事务的工程实践和处理方式。
下面我们通过一个常见的场景问题引出有关于分布式事务的话题.
假设我们在维护一个电商后台系统,每当在处理一笔来自用户创建订单的请求时,需要执行两步操作:
从业务流程上来说,这个流程需要保证具备事务的原子性,即两个操作需要能够一气呵成地完成执行,要么同时成功,要么同时失败,不能够出现数据状态不一致的问题,比如发生从用户账户扣除了金额但商品库存却扣减失败的问题。
然而从技术流程上来讲,两个步骤是相对独立的两个操作,底层涉及到的存储介质也是相互独立的,因此无法基于本地事务的实现方式。
分布式事务的实现确实面临着很高的难度,但在业界已经提出了一套被广泛认可并应用的解决方案。这些解决方案将在后续的章节中介绍。在此之前,我们需要明确在分布式事务的实现中,所谓的数据状态一致性需要做出妥协:
数据状态一致性:在分布式事务中,我们所谈论的数据状态一致性指的是数据的最终一致性,而不是即时一致性。即时一致性通常在分布式系统中难以实现,因为网络延迟和不同组件之间的通信可能导致即时一致性变得不切实际。因此,在分布式环境中,我们更关注确保数据最终达到一致的状态,即经过一段时间后,系统的各个节点都会收敛到相同的数据状态。
百分之百的一致性无法保证:分布式事务中的一个根本挑战是无法百分之百地保证数据状态的一致性。这是因为分布式系统的稳定性和一致性受到网络环境的影响,以及与第三方系统的交互等多种因素的影响。即使采用了复杂的分布式事务协议和机制,也难以消除所有可能的故障和不一致性。
因此,分布式事务的实现需要在数据一致性和系统性能之间寻找平衡。通常情况下,分布式系统会采用某种程度的最终一致性,同时尽力减小数据不一致性的发生概率。这可能涉及到使用分布式事务协议、分布式锁、版本控制等技术手段,以确保在大多数情况下数据状态是一致的。但在特殊情况下,仍然需要处理可能的不一致性问题,并设计恢复机制来纠正这些问题。因此,分布式事务的实现需要权衡各种因素,以满足系统的要求和可用性目标。
首先,一类偏狭义的分布式事务解决方案是基于消息队列 MessageQueue(后续简称 MQ)实现的事务消息 Transaction Message.
RocketMQ 是阿里基于 java 实现并托管于 apache 基金会的顶级开源消息队列组件,其中事务消息 TX Msg 也是 RocketMQ 现有的一项能力. 本章将主要基于 RocketMQ 针对事务消息的实现思路展开介绍.
RocketMQ github 地址:https://github.com/apache/rocketmq
Kafka(Apache Kafka)是一种高吞吐量、分布式、持久性的消息传递系统,最初由LinkedIn开发,并且后来成为了Apache软件基金会的一个顶级项目。Kafka旨在处理大量数据流,并支持实时数据流处理应用程序。
Kafka的典型用例包括日志聚合、事件溯源、监控和度量、实时数据分析、日志流式处理、电子商务订单处理等。它在大规模数据处理、实时数据流和事件驱动架构中广泛使用。
我们知道在 MQ 组件中,通常能够为我们保证的一项能力是:投递到 MQ 中的消息能至少被下游消费者 consumer 消费到一次,即所谓的 at least once 语义.
基于此,MQ 组件能够保证消息不会在消费环节丢失,但是无法解决消息的重复性问题. 因此,倘若我们需要追求精确消费一次的目标,则下游的 consumer 还需要基于消息的唯一键执行幂等去重操作,在 at least once 的基础上过滤掉重复消息,最终达到 exactly once 的语义.
依赖于 MQ 中 at least once 的性质,我们简单认为,只要把一条消息成功投递到 MQ 组件中,它就一定被下游 consumer 端消费端,至少不会发生消息丢失的问题.
倘若我们需要执行一个分布式事务,事务流程中包含需要在服务 A 中执行的动作 I 以及需要在服务 B 中执行的动作 II,此时我们可以基于如下思路串联流程:
对上述流程进行总结,其具备如下优势:
与之相对的,上述流程也具备如下几项局限性:
上面的小节中,聊到的服务 A 所要执行的操作分为两步:本地事务+消息投递. 这里我们需要如何保证这两个步骤的执行能够步调统一呢,下面不妨一起来推演一下我们的流程设计思路:
首先,这两个步骤在流程中一定会存在一个执行的先后顺序,我们首先来思考看看不同的组织顺序可能会分别衍生出怎样的问题:
组合 I 的劣势:
组合 II 的劣势:
对上面对流程进行梳理总结实现思路是:基于本地事务包裹消息投递操作的实现方式,对应执行步骤如下:
这个流程乍一看没啥毛病,重复利用了本地事务回滚的能力,解决了本地修改操作成功、消息投递失败后本地数据修正成本高的问题.
然而,这仅仅是表现. 上述流程实际上是经不住推敲的,其中存在三个致命问题:
我们以 RocketMQ 中 TX Msg 的实现方案为例展开介绍。首先抛出结论,TX Msg 能保证我们做到在本地事务执行成功的情况下,后置的投递消息操作能以接近百分之百的概率被发出. 其实现的核心流程为:
在 TX Msg 的实现流程中,能够保证 前面小节中谈及的各种 bad case 都能被很好地消化:
总结一下:
保证本地事务成功后消息投递接近百分之百的概率:RocketMQ的TX Msg机制确保了在本地事务执行成功的情况下,消息会以接近百分之百的概率被成功发出。这是因为只有在本地事务成功后,才会向RocketMQ发送确认消息,从而触发消息的真正发送。
事务性保障:RocketMQ的TX Msg允许生产者执行本地事务,确保了消息发送与事务的一致性。如果本地事务失败,消息不会被发送,从而维护了数据的一致性。
回滚机制:如果本地事务执行失败,RocketMQ会接收到回滚指令,然后删除对应的半事务消息,而不会执行实际的消息发送操作。这意味着即使本地事务失败,不会导致消息被误发送,保持了数据的一致性。
轮询任务:RocketMQ会定期轮询半事务消息的状态,如果长时间未收到本地事务的二次确认,RocketMQ会主动询问生产者本地事务的执行状态,确保半事务消息能够最终达到终态。
RocketMQ 中半事务消息轮询流程示意如下:
最后,我们再回过头把 RocketMQ TX Msg 的使用交互流程总结梳理如下:
现在我们就来总结梳理一下,TX Msg 中存在的几项局限性:
关于上面第二点,我们再展开谈几句. 我们知道,并非所有动作都能通过简单的重试机制加以解决.
打个比方,倘若下游是一个库存管理系统,而对应商品的库存在事实上已经被扣减为 0,此时无论重试多少次请求都是徒然之举,这就是一个客观意义上的失败动作.
而遵循正常的事务流程,后置操作失败时,我们应该连带前置操作一起执行回滚,然而这部分能力在 TX Msg 的主流程中并没有予以体现.
要实现这种事务的逆向回滚能力,就必然需要构筑打通一条由下游逆流而上回调上游的通道,这一点并不属于 TX Msg 探讨的范畴.