CAP理论
1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标。
:::info
- Consistency(一致性)
- Availability(可用性)
- Partition tolerance (分区容错性)
:::
它们的第一个字母分别是 C、A、P。
Eric Brewer 说,这三个指标不可能同时做到。这个结论就叫做 CAP 定理。
一致性
Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。
可用性
Availability (可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。
分区容错
Partition(分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。
Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务
矛盾
在分布式系统中,系统间的网络不能100%保证健康,一定会有故障的时候,而服务有必须对外保证服务。因此Partition Tolerance不可避免。
如果此时要保证一致性,就必须等待网络恢复,完成数据同步后,整个集群才对外提供服务,服务处于阻塞状态,不可用。
如果此时要保证可用性,就不能等待网络恢复,那node01、node02与node03之间就会出现数据不一致。
也就是说,在P(分区情况下)一定会出现的情况下,A和C之间只能实现一个。
BASE理论
BASE理论是对CAP的一种解决思路,包含三个思想:
- Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
- Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。
- Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
解决分布式事务的思路
分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论,有两种解决思路:
- AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。 既最终一致性
- CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。既最终一致性
实际应用
在实际应用中,
2PC与3PC都是使用的AP模式
本地事务表使用的CP模式
2PC与3PC
二阶段提交(2PC)
阶段一:准备阶段
- 协调者向所有参与者发送commit请求,询问是否可以提交事务,并等待答复。
- 各参与者开始准备执行事务,将uodo log和redo log记入事务日志中,并不提交事务。
- 如果参与者执行成功,则向协调者返回yes,否则返回no。
阶段二:提交阶段
协调者收到各个参与者的准备信息后,根据反馈情况,通知各个参与者Commit或者Rollback。
事务提交
当第一阶段所有参与者都反馈同意时,协调者发起正式提交事务请求,当所有的参与者都回复成功,则表明完成事务,具体流程如下:
- 协调者向所有参与者发送正式提交事务请求(即:commit请求)。
- 参与者收到协调者的commit请求后,参与者正式执行事务提交操作,并释放整个事务期间占用的资源。
- 参与者完成事务提交后,向协调者发送ACK消息。
- 协调者收到所有参与者反馈的ACK消息后,完成事务。
事务回滚
如果任意一个参与者在第一阶段返回中止信息,或者由于超时协调者无法获取到所有参与者的信息,那么这个事务将会被回滚,具体流程如下:
- 协调者向所有参与者发送回滚请求(即:rollback请求)。
- 参与者收到协调者发送的回滚请求后,参与者使用第一阶段中的undo log信息执行回滚操作,并释放在整个事务期间占用的资源。
- 参与者在执行完回滚操作之后,向协调者发送ACK信息。
- 协调者受到所有参与者反馈的信息后,取消事务。
2PC的优缺点
二阶段的确可以提供原子性操作,但是仍有如下缺点:
- 性能问题:所有参与者在提交阶段,都处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
- 可靠性问题:如果协调者出现单点故障,或者出现不可用状态,参与者将一直处于锁定状态。
- 数据一致性问题:在阶段2中,如果出现协调者和参与者都挂了,有可能导致数据不一致。
三阶段提交(3PC)
与两阶段提交不同的是,三阶段提交有两个改动点:
- 引入超时机制,在协调者与参与者中都引入了超时机制。
- 在第一阶段与第二阶段中,插入了一个
准备阶段
。保证了在最后提交阶段之前,各参与节点的状态是一致的。故3PC有CanCommit,PreCommit,DoCommit。
所以3PC处理流程如下:
CanCommit阶段
3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回yes,否则返回no。
- 事务询问: 协调者向参与者发送CanCommit请求,询问是否可以执行事务提交操作,然后开始等待答复。
- 响应反馈: 参与者接到CanCommit请求后,如果可以顺利执行事务,则反馈yes响应,并进入
预备状态
,否则反馈no。
PreCommit阶段
协调者
根据参与者
的反馈情况决定是否可以继续执行事务的PreCommit
阶段。
有两种反馈情况:
一、所有的参与者都反馈yes响应,那么就会执行事务的PreCommit
阶段。
- 发送预提交请求: 协调者向参与者发送
PreCommit
请求,并进入Prepared阶段。 - 预提交事务: 参与者收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中 (但不提交事务) 。
- 响应反馈: 如果参与者都成功了执行事务操作,则返回ack响应,同时开始等待最终命令。
二、加入有任何一个参与者反馈no响应,或者等待超时之后,协调者也没收到参与者的反馈,那么就执行事务中断。
- 发送中断请求: 协调者向所有参与者发送abort请求。
- 中断事务: 参与者收到协调者发送的abort请求后(或者超时后,仍未收到协调者的请求),开始执行事务的中断。
DoCommit阶段
该阶段进行真正的事务提交,可以分为如下两种情况:
一、执行事务提交:协调者收到所有参与者的ack信息,开始执行提交事务。
- 发送DoCommit请求:协调者收到参与者发送的ack响应后,那么就会从
预提交状态
进入到提交状态
,并向所有的参与者发送DoCommit请求。 - 事务提交:参与者接收到协调者发送的DoCommit请求,开始执行事务提交操作,并在完成事务操作后释放所有的事务资源。
- 响应反馈:事务提交完成后,参与者向协调者发送ack信息。
- 事务完成:协调者受到所有参与者的ack信息后,完成事务。
二、中断事务:协调者没能收到所有参与者的ack信息,开始执行中断事务。
- 发送中断请求:协调者性所有的参与者发送abort请求。
- 事务回滚:参与者收到abort请求后,开始利用阶段2中记录的undo log执行事务的回滚操作,并在完成回滚后,释放所有的事务资源。
- 响应反馈:参与者完成事务回滚后,向协调者发送ack信息。
- 中断事务:协调者受到所有参与者的ack信息后,执行事务中断。
在进入DoCommit阶段是,如果协调者或者参与者出现问题,导致参与者无法接收到协调者发出的提交事务/中断事务
请求,此时,参与者都会在等待超时之后,继续执行事务提交。这是基于概率来决定,当进入第三阶段时,说明第一阶段,所有的参与者都同意进行修改操作,同时在第二阶段,所有的参与者都统一同意PreCommit操作。所以,如果在第三阶段,如果出现网络问题,虽然参与者没有收到commit/abort请求,但是它有理由相信:成功提交的几率很大。
3PC的优缺点
与2PC相比,3PC降低了阻塞范围,并且在等待超时后,协调者或者参与者中断事务,避免了协调者单点问题,阶段3中协调者出现问题时,参与者会继续提交事务,可能造成数据不一致。
2PC和3PC都无法保证数据绝对的一致性,一般为预防这种问题,可以提交一个告警。
TCC
TCC: TCC(Try Confirm Cancel)是应用层的两阶段提交,所以对业务有侵入。
核心思想: 针对每个操作,都要实现对应的<font style="color:rgb(255, 80, 44);background-color:rgb(255, 245, 245);">确认</font>
和<font style="color:rgb(255, 80, 44);background-color:rgb(255, 245, 245);">补偿</font>
操作,也就是业务逻辑的每个分支,都要实现Try、Confirm、Cancel三个操作。
TCC的执行流程
TCC的执行过程可以分成两个阶段:
- 第一阶段: Try。该阶段,通过try操作做检测并预留资源。(比如:下单,在try阶段,并不是真正的扣减库存,只是把下单的库存进行锁定。)
- 第二阶段: Confirm/Cancel。根据第一阶段的结果决定是执行confirm还是cancel。
- Confirm:对Try阶段锁定的资源实际扣除。
- Cancel:对Try阶段锁定的资源进行释放。
TCC是如何保证最终一致性
- TCC是以Try为中心的,Confirm操作和Cancel操作都是围绕着Try展开的。所以在Try阶段保障性是最好的,即使出现失败,也可以通过Cancel操作将其执行结果撤销。
- 在Try阶段执行成功,并进入到Confirm阶段时,默认Confirm阶段是不会出错的,也就是说只要Try成功,Confirm一定成功(TCC设计之初的定义)。
- Confirm和Cancel如果失败,则由TCC框架进行重试补偿(定时)。
- 但仍存在极低情况下在CC阶段彻底失败,则需要人工介入。
TCC的注意事项
(1) 允许空回滚:
空回滚的原因是Try阶段超时或者丢包,导致TCC二阶段进行回滚,触发Cancel操作,此时事务参与者可能可能未收到Try操作,但是收到了Cancel操作。
所以,Cancel操作在实现的时候需要允许空回滚,即在Cancel执行时,如果没有查到对应业务的Try操作时,也是需要返回成功,让事务管理器认为已回滚。
(2) 防悬挂控制:
悬挂是指二阶段的Cancel比一阶段的Try操作先执行,出现该问题的原因是Try阶段由于网络拥堵而超时,导致事务管理器生成回滚,触发Cancel操作,但之后拥堵网络的Try又被资源管理器收到了,但是Cancel操作比Try操作先到。如果按照前面允许空回滚的逻辑,回滚是会成功,事务管理器认为回滚成功,所以,此时应该拒绝
空回滚之后的Try操作,否则会产生数据不一致。
因此,我们在Cancel空回滚返回成功之前,应记录该条事务xid或者业务主键,标识该记录已经回滚过,Try操作在执行前先检查这条事务xid或者业务主键是否标记为回滚成功,如果是,则不执行Try操作。
(3) 幂等控制
由于网络原因或者重试操作都有可能导致Try - Confirm - Cancel三个操作重复执行,所以使用TCC时需要注意这三个操作的幂等控制。通常针对具体业务选择对应的业务幂等键
来做防重控制。
TCC方案的优缺点
TCC事务机制相比上面的2PC3PC事务机制,有如下优点:
- 性能提升:具体业务实现,控制资源锁的粒度变小,不会锁定整个资源。
- 数据最终一致性:基于Confirm和Cancel操作的幂等性,确保事务最终完成或者取消,保证了数据一致性。
- 可靠性:解决了XA协议的协调者单点故障问题。由主业务发起并控制整个业务活动,业务活动管理器可以变成多点,引入集群。
缺点:
TCC的Try - Confirm - Cancel操作功能需要按具体业务实现,业务耦合度高,提高了开发成功。
本地事务表
什么是本地消息表
本地事务表的核心思路是将**分布式事务
拆分**成本地事务**
进行处理,该方案中主要有两个角色:事务主动方
和事务被动方
**。
**事务主动方
需要额外新建事务消息表,并在本地事务中完成业务处理和记录事务消息,并轮训事务消息表的数据发哦那个事务消息,事务被动方
**基于消息中间件消费事务消息表中的事务。
这样可以避免一下两种情况导致的数据不一致性:
- 业务处理成功,事务消息发送失败。
- 业务处理失败,事务消息发送成功。
本地事务表的执行流程
- 事务主动方在同一个本地事务中处理业务和写消息表操作。
- 事务主动方通过消息中间件发送消息,通知事务被动发处理事务消息。
- 事务被动方接收到消息后,处理业务逻辑。
- 事务被动方通过消息中间件发送消息,通知事务主动方事务已处理。
- 事务主动方接收到消息后,更新消息表的状态为已处理。
一些必要的容错处理如下
- 当①处理出错,由于事务还在事务主动方的本地事务中,直接回滚即可。
- 当②、④、⑤处理出错,由于事务主动方本地保存了消息,只需要轮询消息重新通过消息中间件发送,通知事务被动方重新读取消息处理业务即可。
- 当③业务上处理失败,事务被动方可以发送消息给事务主动方回滚事务。
本地消息表优缺点
1) 优点:
- 由于写消息表和业务数据在同一个本地事务中处理,确保了消息数据的可靠性,消息数据可靠性不依赖于消息中间件,弱化了对MQ中间件特性的依赖。
- 方案轻量,容易实现。
2) 缺点:
- 与具体业务场景绑定,耦合性强,不可公用。
- 消息数据和业务数据同库,占用业务系统资源。
- 由于需要将消息持久化到数据库中,消息服务性能会受到一定的影响。
事务消息
MQ事务消息执行流程
通过消息的异步事务,可以保证本地事务
和消息发送
同时执行成功或失败,既能实现系统间的解耦,又能保证数据的最终一致性。
本地消息表
方案中,事务主动方
通过在同一个本地事务
中写业务数据
和消息数据
来保证数据的一致性。而事务消息相对于普通的MQ提供了2PC
的提交接口,流程如下:
1) 正常执行情况
在事务主动方服务正常,没有发生故障的情况下,发消息流程如下:
- 步骤①:发送Half消息
- 步骤②:Half消息发送成功
- 步骤③:开始执行本地事务
- 步骤④:根据本地事务执行结果向MQ Server发送Commit/Rollback。
- 最终步骤:MQ Server基于Commit/Rollback进行消息投递或者删除。
2) 异常情况
在断网或者应用重启情况下,图中的步骤④Commit或者Rollback未到达MQ Server,此时处理逻辑如下:
- 步骤⑤:MQ Server未收到二次确认消息,发起消息回查。
- 步骤⑥:收到消息回查后,需要检查对应消息的本地事务执行状态。
- 步骤⑦:根据检查的本地事务执行状态,再次向MQ发送Commit/Rollback。
- 最终步骤:MQ Server基于Commit/Rollback进行消息投递或者删除
MQ事务消息的优缺点
1) 优点( 相较于本地消息表 )
- 消息数据独立存储,降低业务系统与消息系统的耦合性。
- 吞吐量有所提升。
2) 缺点
- 一次消息发送需要两次网络请求(Half消息+Commit/Rollback)。
- 业务处理服务需要实现消息状态回查接口。
最大努力通知
最大努力通知也称为定期校对,是对MQ事务方案的进一步优化。它在事务主动方增加了消息校对接口,如果事务被动方没有接收到主动方发送的消息,此时可以调用事务主动方提供的消息校对接口主动获取
。
在可靠事务消息中,事务主动方需要将消息发送出去,并且让接收方成功接收消息,这种可靠性是由事务主动方保证的。
但是最大努力通知,事务主动方仅仅是做到尽最大努力(重试,轮询…)将信息发送给事务被动方,所以存在事务被动方接收不到信息的情况,所以需要事务被动发通过消息校对接口主动查询获取消息并消费,这种通知的可靠性是由事务被动方保证的。
适用场景:
适用于业务通知类型,如支付宝/微信交易的结果,就是通过最大努力通知方式通知商户,既有回调通知,也有交易查询接口。
分布式事务中间件Seata
初识Seata
Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案。
官网地址:http://seata.io/,其中的文档、播客中提供了大量的使用说明、源码分析。
Seata的架构
Seata事务管理中有三个重要的角色:
- TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
整体的架构如图:
Seata基于上述架构提供了四种不同的分布式事务解决方案:
- XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
- TCC模式:最终一致的分阶段事务模式,有业务侵入
- AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
- SAGA模式:长事务模式,有业务侵入
:::info
无论哪种方案,都离不开TC,也就是事务的协调者。
:::
这里XA模式可以理解为2PC或者3PC
TCC模式:上面已经讲过
SAGA模式和tcc很想,但是TCC操作的是冻结的预留数据,这个是直接操作数据
但是我们主要需要学习的是AT模式
AT模式
AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。
Seata的AT模型
基本流程图:
阶段一RM的工作:
- 注册分支事务
- 记录undo-log(数据快照)
- 执行业务sql并提交
- 报告事务状态
阶段二提交时RM的工作:
- 删除undo-log即可
阶段二回滚时RM的工作:
- 根据undo-log恢复数据到更新前
:::info
事务执行成功 删除快照 事务执行失败 同一回复到快照
:::
流程梳理
我们用一个真实的业务来梳理下AT模式的原理。
比如,现在又一个数据库表,记录用户余额:
id | money |
---|---|
1 | 100 |
其中一个分支业务要执行的SQL为:
1 | update tb_account set money = money - 10 where id = 1 |
AT模式下,当前分支事务执行流程如下:
一阶段:
1)TM发起并注册全局事务到TC
2)TM调用分支事务
3)分支事务准备执行业务SQL
4)RM拦截业务SQL,根据where条件查询原始数据,形成快照。
5)RM执行业务SQL,提交本地事务,释放数据库锁。此时 <font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">money = 90</font>
6)RM报告本地事务状态给TC
二阶段:
1)TM通知TC事务结束
2)TC检查分支事务状态
a)如果都成功,则立即删除快照
b)如果有分支事务失败,需要回滚。读取快照数据(<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">{"id": 1, "money": 100}</font>
),将快照恢复到数据库。此时数据库再次恢复为100
流程图:
AT与XA的区别
简述AT模式与XA模式最大的区别是什么?
- XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
- XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
- XA模式强一致;AT模式最终一致 中间状态是软提交
脏写问题
在多线程并发访问AT模式的分布式事务时,有可能出现脏写问题,如图:
假如多个线程操作同一事务,其中一个线程先拿到数据锁执行完sql 提交事务以后释放锁,另一个线程就能拿到锁在进行一次sql执行,并且线程一刚好完at模式中有个事务失败需要回滚,这个时候数据就脏写了
解决思路就是引入了全局锁的概念。
在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。
优缺点
AT模式的优点:
- 一阶段完成直接提交事务,释放数据库资源,性能比较好
- 利用全局锁实现读写隔离
- 没有代码侵入,框架自动完成回滚和提交
AT模式的缺点:
- 两阶段之间属于软状态,属于最终一致
- 框架的快照功能会影响性能,但比XA模式要好很多
:::info
XA事务的强一致性是通过事务等待来实现的,分支事务等待所有事务完成才能进行提交,提交后才能释放锁sql锁,对性能一致处于消耗,AT模式是直接先进性提交 然后释放sql锁,最后靠全局锁来实现防止脏数据,且回复靠快照,无序等待事务链中所有数据
:::
小结:
在现实的架构设计中分布式事务,我们一般会本地事务表和seata结合使用,本地事务表来实现最终一致性,用来实现可以允许一定程度的不一致,但是最终结果一致的业务,比如涉及到第三方的支付,开票,优惠结算等。seata我们一般使用AT来实现一些要求强一致性的业务,比如下单库存扣减之类的场景。在实际的开发中我们应该尽量避免使用事务,因为涉及到第三方系统的可用性隐患+1,而且数据覆写需要尽量避免掉