前言
分布式系统中,不同服务之间的交互可能会出现各种问题,如网络、异常等,可能会导致服务间的数据产生不一致的情况,如何避免?本文将详细讲述分布式事务的原理和解决方案。
为什么需要分布式事务
目前大多是互联网公司都选择的是分布式系统架构,随之而来暴露
本地事务
出现的问题。所以了解分布式事务之前,需要了解什么是
本地事务
。
那么本地事务,在分布式系统中会出现哪些问题?这里我们用
订单服务
、
库存服务和优惠券服务
来举例说明。
第一种情况:保存订单成功,调用库存服务时,
库存锁定更改成功
,但是由于服务器卡顿等原因,导致调用超时,订单服务报调用超时异常,并回滚数据,此时库存已锁定更改,但是订单数据已全部回滚,导致数据不一致。
第二种情况:保存订单成功,调用库存服务也成功,接着调用优惠券服务去锁定优惠券,这时优惠券服务报了异常,订单和优惠券服务进行了回滚,但是
已经执行过事务的库存服务无法回滚
,导致数据不一致。
总的而言,
本地事务在分布式系统,只能控制住自己的回滚,控制不了其他服务的回滚,出现异常很可能产生数据不一致的情况
。所以需要分布式事务来解决此类问题。
了解分布式事务之前,我们还需要了解 CAP 原则和 BASE 理论。
CAP 原则
什么是 CAP 原则
CAP 原则又称 CAP 定律,指的是
在一个分布式系统中,一致性、可用性、分区容错性,三者不可兼得
。
特性 | 说明 |
---|---|
一致性(Consistency) |
所有节点访问的是同一份最新的数据副本, 属于强一致性 。 |
可用性(Availability) | 服务一直可以访问,且是正常访问时间。 |
分区容错性(Partition tolerance) | 分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供服务。 |
既然三者不可兼得,对待数据就有以下几种不同的一致性策略。
一致性策略 | 说明 |
---|---|
强一致性 | 所有节点访问的是同一份最新的数据副本。 |
弱一致性 | 数据更新后,能容忍后续的访问只能访问到部分或者全部访问不到。 |
最终一致性 | 在一段时间后,节点间的数据会最终达到一致状态。 |
为什么 CAP 不可兼得
我们假设一个分布式网络中,有两个节点
Host1
和
Host2
,分别对应数据库
Data0
和
Data1
,我们将一个数据备份到这两个数据中,假设这个数据值为
age = 18
,
此时
**Data0**
更改数据值为
**age = 28**
。
接下来,我们分析如何才能满足 CAP 原则:
-
满足一致性:
如果
Data0
中更改
age = 28
,则
Data1
中必须同步更改; -
满足可用性:
用户不管请求
Host1
或
Host2
,都会在正常时间内响应结果。 -
满足分区容错性:
Host1
或
Host2
故障的时候,仍然能够对外提供服务。
最后,我们根据不同情况进行分析证明:
-
保证 C 和 P 的情况下:
一致性要求
Data0
须将值复制给
Data1
,分区容错代表
Data0
和
Data1
可能会有一个出错,这时
Data1
并不能及时同步数据,
为了保证数据一致性只能阻塞等待数据同步,此时则无法保证可用性了
。 -
保证 A 和 P 的情况下:
可用性要求
Host1
和
Host2
在正常时间内响应,同样
由于网络问题
**Data1**
还没来得及同步数据,但为了保证可用性直接返回数据,返回的可能是旧的数据,此时则无法保证一致性了
。 -
保证 C 和 A 的情况下:
一致性和可用性,必须保证网络可靠不出故障的情况下才可能实现,此时只有不分区才能实现,这样则无法保证分区容错性,且此时将不再算是分布式系统了。
综上所述,
在一个分布式系统中,一致性、可用性、分区容错性,三者不可兼得
。
CAP 的取舍策略
CA 舍弃 P
舍弃分区容错性
,既不分区,违背了分布式系统的设计初衷。
所以对于一个分布式系统来说,P 是一个基本要求,CAP 三者中,只能在 CA 两者之间做权衡,并且要想尽办法提升 P
。
如关系型数据库 Oracle、MySQL 就是 CA。
CP 舍弃 A
舍去可用性
,要求数据强一致性,此时分区需要数据痛,而网络故障或消息丢失可能导致同步时间无限延长,
可能影响用户体验
,等数据完全一致后,才能成功响应。
如非关系型数据看 Redis、HBASE等,分布式系统中常用的 Zookeeper 也是选择优先保证 CP。
还有跨行转账场景中,一次转账请求要等待双方银行系统都完成整个事务才算完成。
AP 舍去 C
舍弃一致性
,保证可用性和分区容。前提是用户能够接受在一定时间内,查询到的数据不一定是最新的。这种通常会保证最终一致性,后面的 BASE 理论就是根据 AP 来扩展的。
如抢购场景,你看到的可购库存数量是有的,你点进去就会显示购买失败,库存已空,这种就是舍弃了一致性,这里属于强一致性,但是最后会保证数据最终一致性。
BASE 理论
什么是 BASE 理论
BASE 是 Basically Available、Soft State 和 Eventually Consistent 的缩写,含义分别是基本可用、软状态和最终一致性。
我们已了解分布式系统在保证 AP 性质的同时,无法做到强一致性。所以基本可用的核心思想是,
即便无法保证强一致性,也可以根据应用特点采取适当措施,来达到最终一致性的效果
。
Basically Available
基本可用
,本质是一种妥协,就是说当出现节点故障或系统过载时,通过牺牲非核心功能的可用性,保证核心功能的稳定运行。
实现基本可用的策略:
策略 | 示例 |
---|---|
流量削峰 | 秒杀活动将热门商品时间错开,削弱请求峰值。 |
延迟响应、异步处理 | 抢购商品,基于队列收到下单请求,排队异步处理,延迟响应。 |
体验降级 | 看到的非实时数据,采用缓存数据提供服务。压缩图片质量,提高性能。 |
熔断、限流 | 直接拒绝掉部分请求,或当请求队列满后移除部分请求,保证整体系统可用。 |
故障隔离 | 出现故障,做到故障隔离,避免影响其他服务。 |
Soft State
软状态
,允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延迟。
与硬状态对应,硬状态要求多个节点的数据副本都是一致的。
Eventually Consistent
最终一致性
,指系统中的所有数据副本,再经过一定时间的同步后,最终能达到一致的状态。在没有发生故障的前提下,这里的“一定时间”取决于网络延迟,系统负载和数据复制方案设计等因素。
本质就是系统保证数据最终一致,而不需要实时保证数据强一致性。
总的来说,
BASE 理论面向的是大型高可用可扩展的分布式系统,和传统事务的 ACID 是相反的,它完全不同于 ACID 的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间是不一致的
。
什么是分布式事务
分布式事务
是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
简单说就是
这个事务需要多个系统通过网络协同完成
,来保证分布式系统中的数据一致性。
分布式事务实现方案
2PC
2PC 和 3PC 都是基于
XA
协议实现的分布式事务。
XA
接口提供了
事务管理器和本地资源管理器
,其中本地资源管理器多由数据库实现,如 Oracle、MySQL。
2PC 就是两阶段是提交,
第一阶段为准备阶段,若所有事务参与者都预留资源成功,则第二阶段进行提交,否则事务协调者回滚全部资源
。
第一阶段
如图,由事务协调者询问通知各个事务参与者,是否准备好了执行事务。
详细流程:
- 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复;
- 各参与者执行本地事务操作,将 undo 和 redo 信息记入事务日志中,但不提交;
- 如参与者执行成功,给协调者反馈同意,否则反馈中止,表示事务不可以执行;
第二阶段
协调者收到各个参与者的准备消息后,根据反馈情况通知各个参与者提交或回滚。
2.1 事务提交
如图,当第一阶段所有参与者都反馈同意时,协调者发起正式提交事务的请求,当所有参与者都回复同意时,则意味着完成事务,具体流程如下:
- 协调者节点向所有参与者节点发出正式提交的 commit 请求。
- 收到协调者的 commit 请求后,参与者正式执行事务提交操作,并释放在整个事务期间内占用的资源。
- 参与者完成事务提交后,向协调者节点发送 ACK 消息。
- 协调者节点收到所有参与者节点反馈的 ACK 消息后,完成事务。
2.2 事务回滚
如果任意一个参与者节点在第一阶段返回的消息为中止,或者协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时,那么这个事务将会被回滚,具体流程如下:
- 协调者向所有参与者发出 rollback 回滚操作的请求
- 参与者利用阶段一写入的 undo 信息执行回滚,并释放在整个事务期间内占用的资源
- 参与者在完成事务回滚之后,向协调者发送回滚完成的 ACK 消息
- 协调者收到所有参与者反馈的 ACK 消息后,取消事务
2PC 缺点
它是一个
强一致性的同步阻塞协议,事务执⾏过程中需要将所需资源全部锁定,也就是俗称的刚性事务
。所以它比较适⽤于执⾏时间确定的短事务,
整体性能比较差
。
一旦事务协调者宕机或者发生网络抖动,会让参与者一直处于锁定资源的状态或者只有一部分参与者提交成功,
导致数据的不一致
。因此,在⾼并发性能⾄上的场景中,基于 XA 协议的分布式事务并不是最佳选择。
3PC
3PC,三阶段提交协议,是二阶段提交协议的改进版本,三阶段提交有两个改动点:
- 在协调者和参与者中都引入超时机制
- 在第一阶段和第二阶段中插入一个准备阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。
虽然 3PC 用超时机制,解决了协调者故障后参与者的阻塞问题,但与此同时却多了一次网络通信,性能上反而变得更差,也不太推荐。
2PC 和3PC 都无法保证数据绝对的一致性,一般为了预防这种问题,可以添加一个报警,比如监控到事务异常的时候,通过脚本自动补偿差异的信息。
TCC
什么是 TCC
两阶段提交的一个变种,不同的是 TCC 为在业务层编写代码实现的两阶段提交。TCC 分别指 Try、Confirm、Cancel ,一个业务操作要对应的写这三个方法。
TCC 不存在资源阻塞的问题,因为每个方法都直接进行事务的提交,一旦出现异常通过则 Cancel 来进行回滚补偿,这也就是常说的
补偿性事务
。
TCC 对业务的侵入性很强
,原本一个方法,现在却需要三个方法来支持 ,而且这种模式并不能很好地被复用,会导致开发量激增。
还要考虑到网络波动等原因,
为保证请求一定送达都会有重试机制,所以考虑到接口的幂等性
。
执行流程
如下图,可将执行流程分为两个阶段:
-
第一阶段:
执行 Try,业务服务做检测并锁住资源; -
第二阶段:
根据第一阶段的结果决定是执行 Confirm 还是 Cancel,-
Confirm:
执行真正的业务,并释放锁; -
Cancel:
若出问题,则对 Try 阶段预留资源的释放;
-
以下单扣库存为例,Try 阶段去锁库存,不真正删减,Confirm 阶段则实际扣库存,如果库存扣减失败 Cancel 阶段进行回滚,释放库存。
TCC 优缺点:
TCC 事务相比于上面介绍的 XA 事务机制,有以下优点:
-
性能提升:
具体业务来实现,控制资源锁的粒度变小,不会锁定整个资源。 -
数据最终一致性:
基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。 -
可靠性:
解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。
缺点是 TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。
Saga 事务
什么是 Sage 事务
其核心思想是
将长事务拆分为多个本地短事务并依次正常提交
,如果所有短事务均执行成功,那么分布式事务提交;如果出现某个参与者执行本地事务失败,则由 Saga 事务协调器协调
根据相反顺序调用补偿操作
,回滚已提交的参与者,使分布式事务回到最初始的状态。
与TCC事务补偿机制相比,TCC有一个预留(Try)动作,相当于先报存一个草稿,然后才提交。
Saga事务没有预留动作,直接提交
。
如图, Sage 事务基本协议:
- 每个 Saga 事务由一系列幂等的有序子事务 Ti 组成;
- 每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。
Sage 的恢复策略
2.1 向后恢复
如图,当执行事务失败时,补偿所有已完成的事务,
撤销掉之前所有成功的子事务
。
2.2 向前恢复
对于执行不通过的事务,
会尝试重试事务直到成功,不需要补偿,这种方式适用于必须要成功的场景
。
Sage 优缺点
由于 Saga 模型没有 Prepare 阶段,
因此事务间不能保证隔离性
。当多个 Saga 事务操作同一资源时,就会产生
更新丢失、脏数据读取
等问题。这时需要在业务层控制并发,在应用层面加锁。
本地消息表
什么是本地事务表
核心思路就是将分布式事务拆分成本地事务进行处理,有
事务主动方和事务被动方
两种角色。
事务主动发起方需要额外
新建事务消息表
,用来在本地事务中和记录事务消息、完成业务处理,并
轮询事务消息表的数据发送事务消息
,事务被动方基于消息中间件消费事务消息表中的事务。
这样可以避免以下两种情况导致的数据不一致性:
- 业务处理成功、事务消息发送失败;
- 业务处理失败、事务消息发送成功。
本地消息表的执行流程
一些必要的容错处理如下:
- 当第 2 步出错,事务主动方本地保存了消息,只需要轮询消息表重新通过消息中间件发送即可;
- 如果是事务被动方业务上处理失败,可以发消息给事务主动方回滚事务;
- 如果事务被动方已经消费了消息,事务主动方需要回滚事务的话,需要再发消息通知。
本地消息表优缺点
优点:从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件
,弱化了对 MQ 中间件特性的依赖。
缺点:
- 与具体的业务场景绑定,耦合性强,不可共用;
- 消息数据与业务数据同库,占用业务系统资源;
- 消息服务性能会受到关系型数据库并发性能的局限。
基于可靠消息
什么是基于可靠消息
消息的可靠性依赖于消息中间件,本质上是对本地消息表的封装,整体流程与本地消息表一致,唯一不同的就是将本地消息表存在了MQ内部,而不是业务数据库中,如下图。
不同的 MQ 有不同的实现方式,详细介绍放到对应的中间件介绍。
基于可靠消息优缺点
相比本地消息表方案,优点是:
- 消息数据独立存储 ,降低业务系统与消息系统之间的耦合
- 吞吐量大于使用本地消息表方案
缺点:
- 一次消息发送需要两次网络请求。
- 业务处理服务需要实现消息状态回查接口。
最大努力通知
最大努力通知也称为定期校对,是对基于可靠消息的进一步优化。它在
事务主动方增加了消息校对的接口
,如果事务被动方没有接收到主动方发送的消息,此时可以调用事务主动方提供的消息校对的接口主动获取。
在可靠消息事务中,事务主动方需要将消息发送出去,并且让接收方成功接收消息,这种可靠性发送是由事务主动方保证的。
而最大努力通知中,事务主动方仅是通过重试、轮询,将事务发送给事务接收方,所以存在事务被动方接收不到消息的情况,此时需要事务被动方主动调用事务主动方的消息校对接口查询业务消息并消费,这种通知的可靠性是由事务被动方保证的。
所以最大努力通知
适用于业务通知类型
,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口。
实现方案场景总结
方案 | 特性 | 场景 |
---|---|---|
2PC/3PC | 刚性事务 | 适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。 |
TCC | 柔性事务 | 只要能把 Try、Confirm、Cancel 各阶段的逻辑捋清楚就可以使用TCC了, 但是存在业务耦合。 |
基于 MQ | 柔性事务 | 适用于事务中参与方支持操作幂等,业务上能容忍数据不一致。 |
Saga | 柔性事务 | 由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。Saga 由于缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。所以,Saga 事务较适用于补偿动作容易处理的场景 |
一些分布式事务中间件总结:
Seata
参考:
[1] 高级互联网专家. 分布式事务理论.
[2] 还能在学一小时. 分布式常见解决方案.
[2] 程序员小富. 五种分布式方案,最终选择了Seata.