1 介绍
分布式事务用于在分布式系统中保证不同节点之间的数据一致性。
1.1 事务问题
在分布式系统上一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务节点上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。如下图所示,以常见的交易业务逻辑为例
正常情况下,购买A商品应调用库存服务使得A商品库存减一,然后调用订单服务生成一笔A商品的订单。但是在非正常情况下,可能会出现库存成功减一,但是订单记录因为某些原因插入失败了,这种时候两边数据就失去了一致性。
1.2 CAP定理
CAP定理是由加州大学伯克利分校Eric Brewer教授提出来的,他指出WEB服务无法同时满足一下3个属性:
- 一致性(Consistency) : 客户端知道一系列的操作都会同时发生(生效)
- 可用性(Availability) : 每个操作都必须以可预期的响应结束
- 分区容错性(Partition tolerance) : 即使出现单个组件无法可用,操作依然可以完成
1.2.1 一致性(C)
更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,不能存在中间状态。例如对于电商系统用户下单操作,库存减少、用户资金账户扣减、积分增加等操作必须在用户下单操作完成后必须是一致的。不能出现类似于库存已经减少,而用户资金账户尚未扣减,积分也未增加的情况。如果出现了这种情况,那么就认为是不一致的。
一致性可分为三类:
- 强一致性: 时刻保证客户端看到的数据都是一致的,那么称之为强一致性。例如上述例子
- 最终一致性: 允许存在中间状态,只要求经过一段时间后,数据最终是一致的,则称之为最终一致性
- 弱一致性: 允许存在部分数据不一致,那么就称之为弱一致性
1.2.2 可用性(A)
系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。主要有两个关键维度的考量
- 有限时间内: 对于用户的一个操作请求,系统必须能够在指定的时间(响应时间)内返回对应的处理结果
- 返回正常结果: 正常的响应结果通常能够明确地反映出对请求的处理结果,即成功或失败,而不是一个让用户感到困惑的返回结果。比如返回一个系统错误如OutOfMemory,则认为系统是不可用的
1.2.3 分区容错性(P)
分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。
- 网络分区:分布式系统中,不同的节点分布在不同的子网络(机房/异地网络)中
- 网络分区故障:由于一些特殊的原因导致这些子网络之间出现网络不连通的状态,但各个子网络的内部网络是正常的,从而导致整个系统的网络环境被切分成了若干孤立的区域
对于分布式系统而言,分区容错性(P) 是一个最基本的要求,因此基本上我们在设计分布式系统的时候只能从一致性(C) 和可用性(A) 之间进行取舍。
- 如果保证了一致性(C):对于节点N1和N2,当往N1里写数据时,N2上的操作必须被暂停,在N2被暂停操作期间客户端提交的请求会收到失败或超时。显然,这不满足可用性。
- 如果保证了可用性(A):N1在写数据的同时,就不能暂停N2的其他读写操作,这就违背了一致性的要求。
所以说在分布式系统中无法同时保证一致性和可用性
1.3 BASE定理
BASE是CAP理论中AP方案的延伸,对于C采用的策略就是保证最终一致性
-
基本可用性(Basically Available):分布式系统出现不可预知的故障时,允许损失部分可用性,但不等于完全不可用
- 响应时间上的损失:出现故障时,响应时间增加
- 功能上的损失:流量高峰期间,屏蔽某些功能保证系统稳定性(服务降级)
-
软状态(Soft state):允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时
-
最终一致性(Eventually consistent):系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。其本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性
1.4 分布式事务分类
分布式事务实现方案从类型上去分刚性事务、柔性事务:
- 刚性事务:满足CAP的CP理论。如XA协议,但由于同步阻塞,处理效率低,不适合大型网站分布式场景
- 柔性事务:满足BASE理论。如TCC/FMT、Saga(状态机模式、Aop模式)、本地事务消息、消息事务(半消息)
2 刚性事务
例如XA分布式协议,由Oracle Tuxedo系统提出,包含两阶段提交(2PC) 和三阶段提交(3PC) 两种实现。该协议有两个重要角色:事务协调者和事务参与者
2.1 两阶段提交(2PC)
正常无误的情况下,流程如下图所示
当某个参与者发生业务错误时,流程如下图所示
2PC存在以下弊端:
- 性能问题: 遵循强一致性。在事务执行过程中,各个节点占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知提交,参与者提交后释放资源。这样的过程有着非常明显的性能问题。
- 协调者单点故障: 一旦事务协调者节点挂掉,参与者收不到提交或是回滚通知,参与者会一直处于中间状态无法完成事务。
- 消息丢失导致的不一致问题: 在XA协议的第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。
2.2 三阶段提交(3PC)
作为2PC的改进版,3PC将原有的两阶段过程,重新划分为canCommit、preCommit和doCommit三个阶段。
第一阶段:CanCommit
- 协调者向所有参与者发送包含事务内容的CanCommit的请求,询问是否可以执行事务提交,并等待应答
- 各参与者反馈事务询问。正常情况下,如果参与者认为可以顺利执行事务,则返回Yes,否则返回No
第二阶段:PreCommit
协调者会根据上一阶段的反馈情况来决定是否可以执行事务的PreCommit操作。有以下两种可能:
执行事务预提交
- 发送预提交请求。协调者向所有节点发出PreCommit请求,并进入prepared阶段
- 事务预提交。参与者收到PreCommit请求后,会执行事务操作,并将Undo和Redo日志写入本机事务日志
- 各参与者成功执行事务操作,同时将反馈以Ack响应形式发送给协调者,等待最终的Commit或Abort指令
中断事务:任意一个参与者向协调者发送No响应,或者协调者在没有得到所有参与者响应时(超时等待),即可以中断事务
- 发送中断请求:协调者向所有参与者发送Abort请求
- 中断事务:参与者无论是收到协调者的Abort请求,还是等待协调者请求过程中出现超时,都会中断事务
第三阶段:DoCommit
在这个阶段,会真正的进行事务提交,同样存在两种可能:
执行提交
- 发送提交请求。假如协调者收到了所有参与者的Ack响应,那么将从预提交转换到提交状态,并向所有参与者,发送doCommit请求
- 事务提交。参与者收到doCommit请求后,会正式执行事务提交操作,并在完成提交操作后释放占用资源
- 反馈事务提交结果。参与者将在完成事务提交后,向协调者发送Ack消息
- 完成事务。协调者接收到所有参与者的Ack消息后,完成事务
(在该阶段,如果某参与者无法及时接收到来自协调者的doCommit或者abort请求时,会在等待超时之后,继续进行事务的提交。因为进入doCommit阶段,说明协调者已经发送过了preCommit请求,而该请求的前提是所有参与者都认为自己可以顺利执行事务,因此该参与者有理由认为成功提交事务的概率很大)
中断事务:任意一个参与者向协调者发送No响应,或者协调者在没有得到所有参与者响应时(超时等待),即可以中断事务
- 发送中断请求:协调者向所有的参与者发送abort请求
- 事务回滚:参与者收到abort请求后,会利用阶段二中的Undo消息执行事务回滚,并在完成回滚后释放占用资源
- 反馈事务回滚结果:参与者在完成回滚后向协调者发送Ack消息
- 中断事务:协调者接收到所有参与者反馈的Ack消息后,完成事务中断
2.3 两者区别
- 3PC解决了2PC的协调者单点故障问题,一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit,而不会一直持有事务资源并处于阻塞状态。但这也引发了新的问题:由于网络原因,协调者发送的abort响应没有及时被某参与者接收到,那么该参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
- 3PC在协调者和参与者都设置了超时机制,2PC只有协调者才有超时机制。避免了参与者在长时间无法与协调者节点通讯(协调者宕机)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
- 三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。
3 柔性事务
3.1 TCC事务
TCC(Try-Confirm-Cancel):成为补偿事务,思想与2PC相似。但是2PC是引用于DB层面,而TCC是应用于业务层面,需要编写业务逻辑来实现。
例如在全局业务 用户下单创建订单 ➡ 库存减少 ➡ 账户扣减100元 中,账户服务对应的三个阶段如下:
-
Try: 调用 Try 接口,尝试执行业务,完成所有业务检查,预留业务资源。
- 余额保持不变
- 在表中新保存一个字段,叫"冻结资金",让这100元变成冻结资金。这是一段额外的业务逻辑,体现"TCC是应用于业务层面"这一特点
-
Confirm:对业务系统做确认提交,确认执行业务操作,不做其他业务检查,只使用 Try 阶段预留的业务资源。
- 冻结资金修改为0
- 真正把余额扣减100元
-
Cancel:在全局业务执行错误,需要回滚的状态下执行业务取消,释放预留资源
- 余额仍然保持不变
- 直接把冻结资金修改为0即可
- 优势:每个阶段都会都会提交本地事务并释放资源,并不需要像2PC那样等待其他事务的执行结果。而且当其他事务失败后,不是执行回滚操作,而是执行补偿代码。避免资源的长期锁定,性能较好
- 缺点:开发成本高,需要把一个业务拆开三个阶段来编写,需要考虑各个阶段的安全问题
3.2 MQ事务
主要依靠MQ的半消息机制来实现投递消息和参与者自身本地事务的一致性保障。半消息机制实现原理其实借鉴的2PC的思路,是二阶段提交的广义拓展。
半消息:在原有队列消息执行后的逻辑,如果后面的本地逻辑出错,则不发送该消息,如果通过则告知MQ发送。
RocketMQ,ActiveMQ支持该机制;RabbitMQ 和 Kafka 不支持
流程如下
3.3 Saga模式
Saga模型是把一个分布式事务拆分为多个本地事务,每个本地事务都有相应的执行模块和补偿模块(对应TCC中的Confirm和Cancel),当Saga事务中任意一个本地事务出错时,可以通过调用相关的补偿方法恢复之前的事务,达到事务最终一致性。
Saga模型由三部分组成:
- LLT(Long Live Transaction):由一个个本地事务组成的事务链
- 本地事务:事务链由一个个子事务(本地事务)组成,LLT = T1+T2+T3+…+Ti
- 补偿:每个本地事务 Ti 有对应的补偿 Ci
4 Seata AT模式
Seata是阿里巴巴推出的一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。其中AT模式是一种无入侵的解决方案,可以看成是对TCC或2PC的一种优化。在该模式下,用户业务SQL作为第一阶段,Seata会自动生成事务的二阶段提交和回滚操作。
-
一阶段:执行各个本地事务并提交
Seata 会拦截业务SQL,首先解析 SQL 语义,找到业务更新前的数据生成undo log。然后执行更新业务数据,在业务数据更新之后,再保存业务更新后的数据生成redo log,最后获取全局行锁,提交事务。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
-
二阶段:如果各个本地事务均执行成功,则成功无需做其他事情;否则Seata执行反向补偿操作(注意不是回滚),与TCC相似,但是TCC的补偿操作是需要人工编码的,这里则是Seata自动执行。
如果事务全部成功,Seata直接删除保存的临时数据。否则根据redo log找到最新的数据,并把它还原为undo log的旧数据。若没找到最新数据,说明这个期间被修改,已经出现脏数据,需要人工介入。