一、分布式事务概念理论
分布式系统中面临的挑战是复杂的,而分布式事务又是其中的难点,要理解什么是分布式事务,首先我们从单体系统中的事务说起。
1.1、什么是事务?
事务(Transaction)
指的是一组操作,该操作具有 原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)
,也就是常说的 ACID 原则
。
原子性(Atomicity)
:事务只能全部发生或者全部不发生。一致性(Consistency)
:事务需要得到逻辑上正确的结果,这由 AID 保证。隔离性(Isolation)
:指多个事务并发执行的时候 不会互相干扰,保证事务操作过程中 不被干扰 。即一个事务内部的数据对于其他事务来说是隔离的。事务被干扰后会出现脏读、不可重复读、幻读。持久性(Durability)
:指一个事务一旦被提交,它对数据库中数据的改变就是永久性的(数据被永远保存下来),接下来即使数据库发生故障或者其他操作也不应该对其结果有任何影响。
(1)原子性(Atomicity)举例,你给你朋友转 100
块钱,首先你 -100
,然后你朋友 +100
。这两个步骤都要发生才能够完成转账这就是只能全部。当你 -100
之后银行服务器宕机了,你朋友还没+100
,此时银行需要保证你的 -100
不成立,这就是全部不发生。银行让 -100
操作不发生的过程就是 回滚(rollback)。
(2)隔离性(Isolation)举例,接下来以去理发店理发为例来讲述干扰。
脏读
:在事务未完成的情况下,另一事务访问了统同一数据。 举个例子,我在二层烫完头 tony 老师拿着卡机让我刷卡,发现钱不够,我跟 tony 老师说我去一层交一下钱,我下了楼此时还没交钱,tony 等不急又刷了一次卡余额还是不够。当我下楼+充钱(这个事务还没完成)的时候,tony 又读了一次余额这个过程就是脏读。不可重复读
:一个事务内两次读到的数据是不一样的。 接着刚才的例子,tony 老师在送走我之后看到员工卡里的积分刚好可以换个发胶,于是登上系统去换, 但是在他换之前店里的女朋友 Amy 先一步用他的账号换了发胶,于是 tony 就发现积分不够。tony 看到积分然后系统根据积分去兑换物品,在这一事务中间 Amy 对积分进行了操作,导致了 tony 没有兑换成功,这个过程就是不可重复读。它发生于update
过程中。幻读
:一个事务内读取到了别的事务插入的数据,导致前后读取不一致。 tony 没客人的时候翻手机,打开淘宝看了一眼自己的订单列表觉得应该买点什么,乱翻一通之后发现没有想买的,于是想看看自己原来买了什么,点开订单列表发现列表最上面是一套女性内衣,不用说又是 Amy 的操作。在 tony 两次看订单期间 Amy 买了套内衣,导致 tony 前后看到的订单列表不一样这个过程就是幻读。它发生于inster
过程中。
为了解决这些问题,我们为事务设置隔离,隔离的级别有 4 种,由低到高分别为:
Read uncommitted (读未提交)
,啥也不干,脏读、不可重复读、幻读都会发生。Read committed (读提交)
,当事务提交后才能读取。下楼+充钱都做完了 tony 再读取就不是余额不足了。解决脏读。Repeatable read (可重复读)
,读取记录之后阻止对该记录的操作。tony 看到积分之后看住Amy不让她操作。解决不可重复读。Serializable(序列化)
,事务串行。tony 把淘宝 PC 端下线,避免一个账号同时被两个人用。解决幻读。
1.2、什么是分布式?
该领域需要解决的问题极多,在不同的技术层面上,又包括:分布式缓存、分布式数据库、分布式计算、分布式文件系统
等,一些技术如 MQ、Redis、Zookeeper、ETCD、TDSQL、 HDFS
等都跟分布式有关。
分布式(distributed)
是指将一个业务拆分成不同的子业务,分布在不同的机器上执行。服务之间通过远程调用协同工作,对外提供服务。目的是为了解决单个物理服务器容量和性能瓶颈问题而采用的优化手段。分布式系统(Distributed system)
简单来说,分布式就是将一个大问题拆分成多个小问题或者一个大单体系统拆分成多个可独立部署的小服务模块,对这些问题逐一解决,拆分后的各个服务之间相互通信、协同合作,最终作完成一个特定任务,支持分布式处理的软件系统,我们就称之为分布式系统。分布式计算(Distributed computing)
就是在两个或多个软件系统互相共享信息,这些软件既可以在同一台计算机上运行,也可以在通过网络连接起来的多台计算机上运行。
1.3、什么是分布式事务?
分布式事务(Distributed transaction)
:是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。一个大的操作由 N 多的小的操作共同完成,而这些小的操作又分布在不同的服务上。针对于这些操作,要么全部成功执行,要么全部不执行。
1.4、典型分布式应用场景
分布式事务应用场景经典举例:跨行转账(或者跨账户)转账。
假设用户 A 发起一笔跨行转账给用户 B,银行系统首先扣掉用户 A 的钱,然后增加用户 B 账户中的余额。如果其中某个步骤失败,此时就有可能会出现两种异常情况:
- 用户 A 的账户扣款成功,用户 B 账户余额增加失败。
- 用户 A 的账户扣款失败,用户 B 账户余额增加成功。
实际上,两种情况都是不允许发生的,此时就需要事务来保证转账操作的成功(双方操作行为同时成功
)。
在单体应用中,只需要 @Transactional
就可以开启 事务
来保证整个操作的 原子性(即遵循 ACID
原则)。
1.4、分布式理论
通过上面基本概念的认识,接下来我们来看一下分布式系统中的通用理论依据。
1.4.1、CAP 定理
在一个分布式系统中,以下三点特性无法同时满足(号称 CAP “不可能三角”
):
C:Consistency,一致性
。 在分布式系统中的所有数据备份,「在同一时刻具有同样的值」,所有节点在同一时刻读取的数据都是最新的数据副本(等同于所有节点访问同一份最新的数据副本)。A:Availability,可用性,好的响应性能
。完全的可用性指的是在「任何故障模型(集群系统中部分节点故障)下,服务都会在有限的时间内处理完成并进行响应」(对数据更新具备高可用性)。P:Partition tolerance,分区容忍性
。尽管网络上有部分消息丢失或者服务不可用,但系统仍然可继续工作、继续操作。
具体地讲在分布式系统中,在任何数据库设计中,一个 Web 应用 「至多只能同时支持上面的两个属性」
。显然,任何横向扩展策略都要依赖于 数据分区
。因此,设计人员必须在 一致性
与 可用性
之间做出选择:
- 任何分布式系统,必须依赖
P(Partition tolerance,分区容忍性)
。 - 分布式系统可选性,要么
CP(强一致性)
,要么AP(弱一致性,也叫最终一致性)
,三者不可兼具。 - 单体系统中的事务一致性,即为
AC
组合。表现为不拆分数据系统,在一个数据库的一个事务中完成所有的操作。
1.4.2、BASE 理论
简单地来说 BASE 理论的思想是:系统短期内可能处于数据不一致的状态,但最终态数据会变得一致。
在分布式系统中,更多追求的是可用性
,它的权重比一致性要高,如何实现高可用性?那就是 BASE
理论,它是用来对 CAP
定理进行进一步扩充的。BASE 理论解决 CAP 理论提出了分布式系统的一致性和可用性不能兼得的问题。
BASE
在英文中有 “(base)碱”
的意思,对应本篇开头的 ACID
在英文中 “(acid)酸”
的意思,基于这两个名词提出了 酸碱平衡
的结论。简单来说是 在不同的场景下,可以分别利用 ACID
和 BASE
来解决分布式服务化系统的一致性问题。
BASE
模型与 ACID
模型截然不同,满足 CAP
理论,通过牺牲强一致性,获得可用性,一般应用在服务化系统的应用层或者大数据处理系统,通过达到 最终一致性
来尽量满足业务的绝大部分需求。
(1)BASE 理论三要素
BASE
理论模型包含个三个元素:
BA:Basically Available(基本可用)
,系统出现了不可预知的故障,但还是能用,相比较正常的系统而言会有响应时间上的损失和功能上的损失。S:Soft state(软状态)
,状态可以有一段时间不同步。E:Eventually consistent(最终一致性)
,最终态数据是一致的就可以了,而不是时时保持强一致。
BASE
理论是对 CAP
中的 一致性(CP)
和 可用性(AP)
进行一个权衡的结果,理论的核心思想就是:无法做到 强一致性(CP)
,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到 最终一致性(Eventual consistency)
。
(2)什么是软状态呢?
相对于 原子性(ACID)
而言,要求多个节点的数据副本都是一致的,这种也称为 “硬状态”
。
“软状态”
指的是:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时同步。当然不可能一直是软状态,必须有个时间期限。在期限过后,应当保证所有副本保持数据一致性,从而达到数据的 最终一致性
(最终态)。这个 时间期限取决于网络延时、系统负载、数据复制方案设计等等因素
。
(3)最终一致性分类
在实际工程实践中,最终一致性分为 5 种:
因果一致性(Causal consistency)
,如果节点A在更新完某个数据后通知了节点B,那么节点B之后对该数据的访问和修改都是基于A更新后的值。于此同时,和节点A无因果关系的节点C的数据访问则没有这样的限制。读己之所写一致性(Read-your-writes consistency)
,节点A更新一个数据后,它自身总是能访问到自身更新过的最新值,而不会看到旧值。其实也算一种因果一致性。会话一致性(Session consistency)
,会话一致性将对系统数据的访问过程框定在了一个会话当中:系统能保证在同一个有效的会话中实现 “读己之所写” 的一致性,也就是说,执行更新操作之后,客户端能够在同一个会话中始终读取到该数据项的最新值。单调读一致性(Monotonic read consistency)
,如果一个节点从系统中读取出一个数据项的某个值后,那么系统对于该节点后续的任何数据访问都不应该返回更旧的值。单调写一致性(Monotonic write consistency)
,一个系统要能够保证来自同一个节点的写操作被顺序的执行。
在实际的实践中,这 5 种类型往往会结合使用,以构建一个具有最终一致性的分布式系统。
BASE 模型的 软状态(Soft state)
是实现 BASE 理论的方法,基本可用(BA)
和 最终一致(E)
是目标。按照 BASE
模型实现的系统,由于不保证强一致性,系统在处理请求的过程中,可以存在短暂的不一致,在短暂的不一致窗口请求处理处在临时状态中,系统在做每步操作的时候,通过记录每一个临时状态,在系统出现故障的时候,可以从这些中间状态继续未完成的请求处理或者退回到原始状态,最后达到一致的状态。
二、分布式事务解决方案
上面部分我们讲解了分布式系统中相关的概念和理论思想,接下来我们来认识下分布式事务对应的解决方案。
2.1、两阶段提交(2PC)
两阶段提交(2PC,two-phase commit
),分为两个阶段,准备阶段(PreCommit)
和 提交阶段(DoCommit)
。
2.1.1、2PC 概念简介
简单理解就是,所有的事务先准备成功后,然后再一起提交,如果有一个事务准备失败,则所有事务都会回滚。
- 如果在提交阶段(
DoCommit
)出现了问题,则会造成事务的不一致性,需要人工介入。 - 在两阶段提交过程中,在所有事务没有完全提交完成前,数据是锁死状态,其他线程访问会被阻塞。
由于两阶段提交需要等待所有的事务提交完成,因此效率低下,性能与本地事务相差较大,用户体验不是很好。在生产环境(追求性能)不建议使用。
2PC 的应用
- 两阶段协议可以用于
单机集中式系统
,由事务管理器协调多个资源管理器; - 也可以用于
分布式系统
,由一个全局的事务管理器协调各个子系统的局部事务管理器完成两阶段提交。
该协议有两个角色:A 节点是 事务的协调者
,B/C 是 事务的参与者
。
(1)第一阶段:投票阶段
- 协调者首先将命令写入日志。
- 发一个
prepare
命令给B/C
节点这两个参与者。 B/C
收到消息后,根据自己的实际情况,判断自己的实际情况是否可以提交。- 将处理结果记录到日志系统。
- 将结果返回给协调者。
(2)第二阶段:决定阶段
- 当
A
节点收到B/C
参与者所有的确认消息后; - 判断所有协调者是否都可以提交。
- 如果可以则写入日志并发起
commit
命令;有一个不可以则写入日志并发起abort
命令。 - 参与者收到协调者发起的命令,执行命令。
- 将执行命令及结果写入日志。
- 返回结果给协调者。
2.1.2、2PC 可能存在的问题
单点故障
:一旦事务管理器(事务协调者)出现故障,将导致整个系统不可用。数据不一致
:在阶段二,如果事务管理器只发送了部分commit
消息,此时网络发生异常,那么只有部分参与者接收到commit
消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。响应时间较长
:整个消息链路是串行的,要等待响应结果,不适合高并发的场景。不确定性
:当事务管理器发送commit
之后,并且此时只有一个参与者收到了commit
,那么当该参与者与事务管理器同时宕机之后,重新选举的事务管理器无法确定该条消息是否提交成功。
2.2、三阶段提交(3PC)
3PC(three-phase commit)解决了 2PC 中事务管理器(事务协调者)存在的单点问题,但性能问题和数据不一致问题仍然没有根本解决。
相对于 2PC
,3PC
基于 2PC
的基础上进行了改良,增加了 CanCommit
阶段和 timeout
超时机制。如果一段时间内没有收到协调者的 commit
请求,那么就会自动进行 commit
。 3PC 解决了 2PC 单点故障的问题,但性能问题和数据不一致问题仍然没有根本解决。
第一阶段:CanCommit 阶段
,协调者询问事务参与者,是否有能力完成此次事务。如果都返回yes
,则进入第二阶段;其中只要有一个返回no
或等待响应超时,则中断事务,并向所有参与者发送abort
请求。第二阶段:PreCommit 阶段
,协调者会向所有的参与者发送PreCommit
请求,参与者收到信息后开始执行事务操作,并将Undo
和Redo
信息记录到事务日志中。参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈“Ack”
(消息确认)表示已经准备好提交了,并等待协调者的下一步指令。第三阶段:DoCommit 阶段
,在阶段二中如果所有的参与者节点都可以进行PreCommit
提交,那么协调者就会从“预提交状态”
转变为“提交状态”
。然后向所有的参与者节点发送doCommit
请求,参与者节点在收到提交请求后就会各自执行事务提交操作,并向协调者节点反馈“Ack”
消息,协调者收到所有参与者的“Ack”
消息后完成事务。相反,如果有一个参与者节点未完成PreCommit
的反馈或者反馈timeout
超时,那么协调者都会向所有的参与者节点发送abort
终止请求,从而中断事务执行。
2.3、补偿事务(TCC)
TCC
模式也分为三个阶段,分别是:Try、Confirm、Cancel
。本质上是采用的补偿机制,其核心思想是:针对每个操作都要注册一个与之对应的取消(cancel)方法,如果执行失败,则调用取消(cancel)方法撤销之前的操作
。如果取消(cancel
)方法执行时发生了错误,则需要定时任务去轮询补偿直至成功为止,如果失败则需人工介入。
2.3.1、TCC 概念简介
Try 阶段
,主要是对业务系统做检测及资源预留,其主要分为两个阶段。Confirm 阶段
,主要是对业务系统做确认提交,Try
阶段执行成功并开始执行Confirm
阶段时,默认Confirm
阶段是不会出错的。即:只要Try
成功,Confirm
一定成功。Cancel 阶段
,主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
2.3.2、TCC 典型应用举例
执行流程:
Try 阶段
,订单系统将当前订单状态设置为支付中,库存系统校验当前剩余库存数量是否大于1
,然后将可用库存数量设置为库存剩余数量-1
。如果 Try 阶段执行成功,执行 Confirm 阶段
,将订单状态修改为支付成功,库存剩余数量修改为可用库存数量。如果 Try 阶段执行失败,执行 Cancel 阶段
,将订单状态修改为支付失败,可用库存数量修改为库存剩余数量。
2.3.3、TCC 模式的优缺点?
- 优点:逻辑比较简单。例如串行事务执行,先执行 A 事务,再执行 B 事务,B 事务执行失败了,则调用 A 事务的取消(
cancel
)方法撤销 A 事务之前的操作。 - 缺点:需要程序员自己编写的代码较多,无形中增加了很多工作量,且容易出错。
2.3.4、TCC 相比于 2PC,解决的问题?
单点问题
:解决了协调者单点问题,由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。同步阻塞问题
:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。数据一致性问题
:有了补偿机制之后,由业务活动管理器控制一致性。
总之,TCC
就是通过代码人为实现了 2PC
两阶段提交,不同的业务场景所写的代码都不一样,并且很大程度的增加了业务代码的复杂度。因此,这种模式并不能很好地被复用。
2.4、本地消息表 & MQ 异步化
本地消息表简单理解,项目服务产生的消息存入本地数据库表,该表就称之为 本地消息表。
执行流程:
消息生产方
,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,即需在同一个数据库里。然后消息会经过MQ
发送到消息的消费方。如果消息发送失败,定时任务会进行重试发送,直到消息发送成功为止。消息消费方
,需要处理这个消息,并完成自己的业务逻辑。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。- 此时如果本地事务处理成功,表明已经处理成功了。如果处理失败,那么定时任务就会重试执行。
- 最后
生产方
和消费方
定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。
该方案依赖 MQ 和 数据库的事务
,实现了数据 最终一致性
。增加 MQ
异步化流程,提升了系统接入数据的并发能力,但分散到各自独立服务环境后,并发性能主要依赖于 DB 环节(可能会产生性能瓶颈,如果追求极致高并发性能体验的系统,此处需要考虑 DB
高并发方案),推荐生产环境使用该方案。
2.5、消息事务
消息事务的原理 是 将两个事务通过消息中间件进行异步解耦
,和上述的本地消息表有点类似,可以看作是本地消息表的变种,但是通过消息中间件的机制去做的,其本质就是 “将本地消息表封装到了消息中间件中”
。
执行流程:
- 发送
prepare
消息到消息中间件MQ
。 MQ
收到发送方(生产者)
发送的消息后,将消息状态标记为Prepared
(预备状态)。注意此时这条消息消费者(MQ 订阅方
)是无法消费到的。MQ
接收到Producer
发送给的消息则"Ack"
回应发送成功表示MQ
已接收到消息。Producer
端发送成功后,该端继续执行业务代码逻辑,执行本地事务(通过本地数据库事务控制)。- 如果本地事务执行成功,则
commit
,消息中间件将消息下发至消费端。如果事务执行失败,则回滚,消息中间件将这条prepare
消息删除。 Consumer
消费端接收到消息进行消费,如果消费失败,则不断重试。
这种方案也是实现了 最终一致性
,对比 本地消息表
实现方案,不需要再建消息表,不再依赖本地数据库事务了,因此该方案更适用于高并发的场景
。目前市面上实现该方案的只有阿里的 RocketMQ
。
2.6、最大努力通知
最大努力通知的方案实现比较简单,适用于一些最终一致性要求较低的业务
。
执行流程:
- 系统 A 本地事务执行完之后,发送条消息到 MQ。
- 这里会有个专门消费 MQ 的服务,该服务会消费 MQ 并调用系统 B 的接口。
- 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次直到成功为止,如果最后还是不行就放弃,只能人工介入。
2.7、Sagas 事务模型(长时间运行的事务)
其核心思想是 将长事务拆分为多个本地短事务
,由 Saga
事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
Seata
框架中一个分布式事务包含三种角色:
Transaction Coordinator, TC
:事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。Transaction Manager, TM
:控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。Resource Manager, RM
:控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
seata
框架 「为每一个 RM 维护了一张 UNDO_LOG 表」,其中保存了每一次本地事务的 rollback
回滚数据。
说明:seata 框架是阿里开源的分布式事务中间件,官网地址: https://seata.io/zh-cn/
具体流程:
- 首先
TM
向TC
申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。 XID
在微服务调用链路的上下文中传播。RM
开始执行这个分支事务,RM
首先解析这条SQL
语句,生成对应的UNDO_LOG
记录。下面是一条UNDO_LOG
中的记录,UNDO_LOG
表中记录了分支ID
,全局事务ID
,以及事务执行的redo
和undo
数据以供二阶段恢复。RM
在同一个本地事务中执行业务SQL
和UNDO_LOG
数据的插入。在提交这个本地事务前,RM
会向TC
申请关于这条记录的全局锁 。- 如果申请不到,则说明有其他事务也在对这条记录进行操作,因此它会在一段时间内重试,重试失败则回滚本地事务,并向
TC
汇报本地事务执行失败。 RM
在事务提交前,申请到了相关记录的全局锁,然后直接提交本地事务,并向TC
汇报本地事务执行成功 。此时全局锁并没有释放,全局锁的释放取决于二阶段是提交命令还是回滚命令。TC
根据所有的分支事务执行结果,向RM
下发commit
提交或rollback
回滚命令。
RM
如果收到TC
的提交命令 ,首先立即释放
相关记录的全局锁
,然后把提交请求放入一个异步任务的队列中,马上返回提交成功的结果给TC
。异步队列中的提交请求真正执行时,只是删除相应UNDO LOG
记录而已。RM
如果收到TC
的回滚命令 ,则会开启一个本地事务,通过XID
和Branch ID
查找到相应的UNDO LOG
记录。将UNDO LOG
中的镜像与当前数据进行比较。
UNDO LOG
中的镜像与当前数据进行比较可能产生的结果:
- 如果不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理。
- 如果相同,根据
UNDO LOG
中的前镜像和业务SQL
的相关信息生成并执行回滚的语句并执行,然后提交本地事务达到回滚的目的,最后释放相关记录的全局锁。
总结
在分布式环境中,web 分布式系统面临的复杂性挑战是艰巨的,其中分布式事务就是一个技术难题,业务中具体使用哪种方案还是需要结合业务本身特点自行选择。分布式事务提升了流程的复杂度,带来很多额外的开销工作,代码量增加了,业务复杂了,性能下跌了。所以,真实开发的过程中,非必要应用场景尽量不使用分布式事务。