文章目录
总结:
1.分布式事物主要是解决分布式环境下,组合事物的一致性问题。
2.事务也分本地事务和分布式事务,这里主要介绍分布式事务,本地事务可以参考我的另一篇文章 数据库原理之数据库事物
3.常见的分布式事务协议 2PC、3PC、基于分布式消息的最终一致性方案、SAGA、TCC、XA
- 分布式事务的2pc和3pc 方案主要依靠节点自己的本地事务的开启和回滚加上协调者的协调实现了分布式事务的ACID。并且由于全程开启本地事务,所以分布式执行过程中数据都是强一致性的,不会出现数据不一致的状态。但是这样性能较低,并且协调者或者参与者出错时可能无法保证系统最终数据的一致性。
- 基于分布式消息的一致性方案,在事务的执行过程中可能会数据不一致,并且隔离性也不强,会有幻读问题。但是执行完成,不论失败还是成功,都能保证数据的最终一致性。是一种弱一致性的柔性事务。
- TCC事务方案则提前对资源进行锁定,后续的执行过程中几乎不涉及锁和竞争,在实现最终一致性之外,还天然有着较好的隔离性且性能好,但是对与业务侵入较高,也必须要求涉及的资源可被提前锁定,一旦涉及系统外部的资源想要提前锁定几乎是不可能的。
- SAGA基于数据补偿代替事务回滚方案,先获取所需资源并设计好失败时的补偿操作,也有着天然的隔离性,并且几乎也不涉及锁和竞争,还对本身服务可能出现的崩溃设计了日志。但是复杂度较高并且一般需要依靠其他的中间件来对服务进行编排。
- AT事务既参考了2PC也参考了数据补偿代替回滚的思想,优化了第一阶段长时间的事务阻塞,在第一阶段直接提交事务并生成补偿的SQl,但是缺点是需要全局锁对资源施加写锁,否则无法保证原子性会发生脏写。并且隔离级别只能维持在读未提交。
博客内容是自己的一些学习和理解,有理解错误的地方欢迎指正。
什么是分布式事物
事务 ALL OR Nothing
事物:事物是一个具有一组原子性操作的工作单元,它被视为一个不可分割的工作单元,要么全部完成,要么全部不完成。事物的ACID特性定义了事物的基本特征和要求,即原子性、一致性、隔离性和持久性。
分布式事物 : 在分布式环境下,多个节点之间进行的一组原子性操作的工作单元。由于分布式系统中存在多个节点之间的网络延迟和不确定性,因此在设计和实现分布式事务时,需要使用特殊的技术和方法来保证事务的原子性、一致性、隔离性和持久性。
分布式事物作用
分布式事物主要是解决分布式环境下,组合事物的一致性问题。
常见的实现分布式事物方法
实现分布式事物有以下3中基本方法
- 基于XA协议的二段提交协议方法
- 三阶段提交协议方法
- 基于消息的最终一致性方法。
一、 两阶段提交协议 2PC (2 Prepare Commit)
2PC是比较常见的解决分布式事物的方式,要么所有进程都提交事物,要么就都取消事物,是实现ACID中原子性的常用手段。是一种分布式原子提交协议。
简单来说2PC相比于单机的事物提交,引入了一个协调者的角色,来负责确认各个子系统是否可以执行事物,如果某个子系统不能执行事物,就通知其他系统回滚事物本次事物执行失败。如果问询的结果是都可以执行事物,则通知所有子系统提交事物
1.1 2PC正常提交事物流程
阶段一 prepare
- 事物询问:协调者向所有参与者发送事物的内容,询问是否可以执行事物提交操作,并等待各个参与者的响应。
- 执行事物:写本地的 Undo/Redo 日志
- 各参与者向协调者反馈事物询问的响应
阶段2 commit
- 发送提交请求:协调者向所有参与者发送commit请求
- 事物提交:参与者收到commit请求后,会正式执行事物提交操作,并在完成提交后释放整个事物执行期间占用的事物资源。
- 反馈事物提交结果:参与者完成事务提交后,向协调者发送ACK信息
- 完成事物:协调者收到所有参与者反馈的ACK信息后,事物执行结束。
1.2 2PC中断回滚事物流程
假如任何一个参与者向协调者反馈了NO,无法执行事物的响应,或者在等待超时之后,协调者尚未收到所有参与者的反馈响应,那么就会中断事物。
阶段一 prepare
- 事物询问:协调者向所有参与者发送事物内容,询问是否可以执行提交事物并等待参与者响应。
- 有参与者超时未响应(超时),或者返回了NO无法执行事物
阶段二 rollback
- 协调者向所有参与者发送Rollback请求
- 参与者收到Rollback请求,会根据其记录的Undo日志,来回滚事物操作,并在回滚完成后释放整个事物执行期间的资源
- 参与者反馈Rollback的结果给协调者
- 协调者收到所有参与者反馈的ACK信息,完成事物中断。
1.3 2RPC的优缺点分析
优点
- 原理简单
缺点
- 同步阻塞:参与者收到Parpare消息后,就会执行并开启事物,事物的开启可能就会对一些公共资源进行锁定,系统的性能受到极大地影响。并且这个过程可能会很长,要等到协调者继续收集完所有的参与者的消息并且发送下一步动作才能结束事物。(提交/回滚)
- 单点问题:如果协调器出现问题,那么两个阶段都无法运转。如果协调器在阶段二执行过程中出现问题,那么其他参与者将一直处于锁定事物资源的状态中,无法继续完成事物操作。
- 数据不一致:如果阶段二提交过程,在某些参与者收到commit之前,协调器崩溃了,那么只会有部分参与者执行commit 出现了数据不一致问题。
- 太过保守:在进行事物提交问询时,如果某个参与者故障,那么只能依靠协调者的超时机制来判断是否需要中断事物。策略过于保守,没有完善的容错机制。任意一个节点故障,都会导致整个事物失败。
二、 三阶段提交协议 3PC
- 基于2PC的事物提交流程,3PC将2PC的准备阶段再次一分为二,这样三阶段提交有三个阶段分别是:CanCommit、PreCommit、DoCommit三个阶段。
- 三阶段提交协议、引入了超时机制。
- 三阶段提交协议的准备提交阶段。保证了在最后提交阶段之前各个阶段的状态是一致的。
2.1 CanCommit阶段
- 类似于2PC的准备阶段,协调者发送CanCommit请求,参与者如果可以执行事物就返回Yes响应,否则就返回No响应。
- 注意: 这一阶段只是协调者和参与者就能否完成操作进行了交流,没有进行实际的工作。
2.2 PreCommit阶段
- 成功收到所有参与者的Yes消息
- 协调者向所有参与者发送PreCommit消息,然后协调者等待各个参与者的响应。
- 参与者收到PreCommit消息后执行事物,但是不提交
- 参与者事物执行完成后,向协调者回应ACK消息。这种情况下,一致性操作将今年入DoCommit阶段。
- 一段时间(超时)没集齐所有参与者的消息,或者收到了返回No的消息
- 协调者向所有参与者发送Abort消息,标明本次一致性操作取消
- 参与者收到Abort后,中指当前操作。
- 参与者一段时间没收到协调者的消息(超时),也会中断当前操作
- 这种情况下一致性操作结束,不会进入DoCommit阶段
2.3 DoCommit阶段
- 集齐了所有参与者的 ACK 消息
- 协调者向参与者发送Commit消息,提交已完成但尚未提交的事物
- 参与者收到Commit消息后,提交事务。
- 参与者一段时间没收到Commit消息的话(超时),会自动提交事物。
- 参与者提交事物后,向协调者发送Done消息
- 协调者收到素有参与者Done消息,标识该一致性操作成功结束。
- 协调者一定时间内未集齐参与者的ACK消息(超时)
- 协调者向所有参与者发送 RollBack消息
- 参与者收到Rollback消息后,回滚准备提交的事物。
- 参与者向协调者发送Done消息
- 协调者收到所有参与者的Done消息,此次一致性操作以失败的形式结束。
2.4 2PC和3PC对比
- 降低了阻塞程度2PC中,如果协调者突然宕机那么事物将一直阻塞。3PC中如果协调者宕机,参与者不会因为协调者的宕机而导致事物无法关闭。
- 3PC多设置了一个缓冲阶段 PreCommit,保证了提交阶段前各个节点状态是一致的。
- 3PC减轻了单点故障问题,参与者和协调者都有超时机制。而2PC参与者没有超时机制。
- 都没有完全解决数据一致性的问题,都是同步执行性能较差
三、基于分布式消息的最终一致性方案
在 eBay 的分布式系统架构中,架构师解决一致性问题的核心思想就是:将需要分布式处理的事务通过消息或者日志的方式异步执行,消息或日志可以存到本地文件、数据库或消息队列中,再通过业务规则进行失败重试。这个案例,就是使用基于分布式消息的最终一致性方案解决了分布式事务的问题。也称为本地消息表方案。
而“可靠事件队列”有一种更普通的形式,被称为“最大努力一次提交”(Best-Effort 1PC),意思就是系统会把最有可能出错的业务,以本地事务的方式完成后,通过不断重试的方式(不限于消息系统)来促使同个事务的其他关联业务完成。
- 2PC和3PC的核心思想,都是以集中式的方式实现分布式事物,这种方法都存在两个共同的缺点,一个是同步执行性能差,一个是没有解决数据不一致的问题。分布式消息确保事物最终一致性的方案就出现了。
- 分布式消息的最终一致性方案,需要引入一个消息中间件,用来进行消息传递。例如阿里就是采用的RocketMQ 机制来支持消息事物。
购物案例
假设用户 A 在某电商平台下了一个订单,需要支付 50 元,发现自己的账户余额共 150 元,就使用余额支付,支付成功之后,订单状态修改为支付成功,然后通知仓库发货。假设系统中三个系统互相独立,涉及订单系统、支付系统、仓库系统。
下单成功执行过程
- 订单系统 发送订单信息给 MQ 消息状态为“待确认”
- MQ 持久化消息,存储一条消息 状态为 "待发送”
- MQ 返回消息持久化结果,订单系统根据结果判断业务如何操作。如果失败可能需要放弃订单,必要时向上层返回结果。成功则创建订单。
- 订单系统将结果发送给消息中间件。
- 消息中间件收到业务操作结果,根据结果进行处理。失败放弃订单则删除已经存储的消息;成功则将消息状态转换为“待发送”并进行消息投递。
- 如果消息为 “待发送”,则MQ会将消息发送给支付系统,表示订单创建完成需要支付。支付系统也会按照上述方式进行订单支付操作。
- 支付完成后,支付成功的消息会返回给消息中间件,中间件将消息回传给订单系统。如果支付失败,则订单操作失败订单系统将回滚到上一个状态,MQ中相关的消息也将被删除。如果支付成功,则订单系统再调用仓库系统,进行出货操作。
下单过程中可能出现的异常
- 订单保存到MQ中失败了,订单系统不进行任何操作。数据保持一致
- MQ 将消息发送给支付系统或者仓库系统,但是支付系统操作成功的ACK消息未回传成功。MQ会确认各系统的操作结果,删除相关消息。支付系统或仓库系统操作回滚。数据保持一致
- MQ 成功将消息发送给支付系统(或仓库系统),但是支付系统(或仓库系统)操作成功的 ACK 消息回传成功,订单系统操作后的最终结果(成功或失败)未能成功发送给 MQ,此时各系统数据可能不一致,MQ 也需确认各系统的操作结果,若数据一致,则更新消息;若不一致,则回滚操作、删除消息。
总结
- 基于分布式消息的最终一致性方案采用消息传递机制,并使用异步通信的方式,避免了通信阻塞,从而增加系统的吞吐量。同时,这种方案还可以屏蔽不同系统的协议规范,使其可以直接交互。
- 在不需要请求立即返回结果的场景下, 这些特性就带来了明显的通信优势,并且通过引入消息中间件,实现了消息生成方(如上述的订单系统)本地事务和消息发送的原子性,采用最终一致性的方式,只需保证数据最终一致即可,一定程度上解决了二阶段和三阶段方法要保证强一致性而在某些情况导致的数据不一致问题。
- 缺陷:可靠消息队列方案,是没有事物隔离性可言的,如果两个用户短时间都下单了一件商品就可能出现超售问题(例如同时下单数量超过库存的数量,最后一部先后扣除库存就会发现超售了)。如果达到可重复读的隔离级别,就可以避免超售。但是可靠消息队列无法保证这一点。
四、TCC方案
TCC(Try-Confirm-Cancel)是除可靠消息队列以外的另一种常见的分布式事务机制,它是由数据库专家帕特 · 赫兰德(Pat Helland)在 2007 年撰写的论文《Life beyond Distributed Transactions: An Apostate’s Opinion》中提出的。
- TCC也是一个最终一致性方案,是一种柔性事物。
- TCC方案对比消息一致性方案,天生适合于需要强隔离性的分布式业务中。
- 但是它是一种业务侵入性较强的事物方案,业务处理过程必须拆分为 “预留业务资源”和“确认/释放消费资源”两个子过程。
- TCC分为了三个阶段,Try、Confirm、Cancel
- Try 尝试执行阶段,完成所有业务可执行性检查,并且预留好事物需要用到的所有业务资源。
- Confirm 确认执行阶段,不进行任何业务检查,直接使用Try阶段准备的资源来完成业务处理。Confirm阶段可能会重复执行,所以需要满足幂等性。
- Cancel 取消执行阶段,释放Try阶段预留的业务资源。Cancel阶段也可能会重复执行,需要保证幂等性。
4.1 TCC执行过程
- 第一步 用户发送交易请求,购买《极客时间》¥100
- 第二部 创建事物,生成事物ID,记录在日志中,开始尝试预留资源。
- 用户服务:检查业务可行性,可行则把用户100元设置为冻结状态。可行则通知进入Confirm状态否则则进入Cancel状态
- 仓库服务:检查业务可行性,将仓库的1一本《极客时间》冻结,通知下一步进入confirm状态。
- 商家服务:检查业务可行性,不冻结资源。
- 第三步:第二部中所有的业务反馈可行,就将活动日志的状态设置为Confirm进入Confirm阶段。
- 用户服务:完成业务操作 扣减100元
- 仓库服务:标记被冻结的《极客时间》为出库,扣减库存
- 上架服务:收款100元
- 第四步:如果第三步全部完成,事物就正式结束。如果第三步任何一步出现异常,就会根据日志来重复执行。进行“最大努力交付”
- 第五步:如果第二部任何一方反馈业务不可行,或者超时,就将活动日志的状态记录为Cancel进入Cancel阶段。
- 用户服务:取消业务操作(释放被冻结的 100 元)。
- 仓库服务:取消业务操作(释放被冻结的 1 本书)。
- 商家服务:取消业务操作(大哭一场后安慰商家谋生不易)。
- 第六步:如果第五步全部完成,事物就以失败回滚结束,如果回滚过程出现任何异常,也都会根据活动日志中的记录重复 进行最大努力交付。
4.2 TCC总结
优点:
- TCC事物具有较强的隔离性,能够避免“超售问题”
- 只操作预留资源,几乎不涉及锁和资源的竞争,具有很高的性能潜力
缺点:
- 需要编码上配合,增加了开发工作量
- 业务侵入性非常强,需要资源满足可提前冻结的条件。对于一些外部资源无法完成提前冻结就无法使用该方案。
五、SAGA-基于数据补偿代替回滚的解决思路
5.1 介绍
SAGA的起源
- SAGA的提出比分布式事物的历史还要久远,起源于1987年普林斯顿大学发表在ACM的一片论文《SAGAS》
- SAGA的意思是“长篇故事、长篇记叙、一长串事件”
SAGA大致思路
将一个大事物,分解为可以交错运行的一系列子事物的集合,原本的目的是为了避免大事物长时间锁定数据库资源。后来发展成将一个分布式环境中的大事物,分解为一系列本地事物的设计模式。
SAGA的两部分操作
- 事物拆分 将大事物拆成若干个小事物,将整个分布式事物分解为n个子事物,命名为 T1、T2…Ti,…Tn 。每个子事物都应该能被看做原子行为。如果分布式事物T能被正常提交,那么它对数据的影响(最终一致性)就应该与连续成功提交自事物Ti等价。
- 事物补偿 每一个子事物都要设计对应的补偿动作,命名为C1、C2、Ci。Ti与Ci满足以下条件。
- Ti 与 Ci 都具备幂等性
- Ti 与 Ci 满足交换律,不论哪个先执行,效果都是一样的。
- Ci 必须能成功,不考虑Ci本身提交失败被回滚的情况,一旦出现就要持续重试或者人工介入。
SAGA的失败回复策略
- 正向恢复 Forward Recovery 如果Ti提交失败,就一直对Ti进行重试直至成功为止(最大努力交付)这种恢复不需要补偿,适用于事物最终都要成功的场景,例如转账账户A扣款,账户B就一定要入账。正向恢复的执行模式
T1、T2....、Ti(失败)、Ti(重试) ....Tn
- 反向恢复 Backward Recovery 如果Ti事物提交失败,则一直执行Ci对Ti进行反向补偿,直至成功为止(最大努力交付)。这里要求Ci必须(可持续重试)执行成功。反向恢复的执行模式为:T1、T2 … Ti(失败)、Ci(补偿), …C2 、C1
对比TCC不需要冻结操作
SAGA不需要为资源设计冻结状态和撤销冻结的操作,补偿操作往往比冻结额操作容易实现的多。
例如在购物场景中,假设从用户账户扣款成功,后续例如发货等步骤执行失败,系统无法要求银行撤销扣款操作,但是基于补偿,我们可以再将扣款重新转账回客户账户。
用于崩溃恢复的 SAGA Log
SAGA需要保证所有子事物都能被提交或者补偿,但是SAGA系统本身也有可能会出问题,所以它也涉及了与数据库类似的日志机制 比如SAGA Log,用来保证系统崩溃恢复后可以追踪到子事物的执行情况。
5.2 总结
- 补偿操作通常会比冻结/撤销更容易实现,但是要保证正向和反向撤销的过程能严谨的进行也是非常有难度的。
- 补偿操一般需要通过服务编排,可靠事件队列来完成,所以SAGA事物一般不依靠裸编码实现,一般在中间件的基础上实现
六、AT事务模式
简介
- AT事务由阿里的GTS(Global Transcation Service ) 提出的,Seata就是基于GTS开源的。
- AT事务模式包含了SAGA一样的数据补偿代替回滚的思路,同时整体上又参考了XA两段提交协议来实现。
XA事务原理
- XA针对2PC在准备阶段需要等待所有数据源成功返回,协调者才能发出统一的Commit命令导致的木桶效应(最慢的事务都完成才能释放)做了优化。
- 大致做法是,在业务数据提交时,自动拦截所有SQL保存SQL执行前后的结果快照,生成行锁,通过本地事务一起提交到操作的数据源中,这就相当于自动记录了重做和回滚日志。
- 如果事务成功提交,只要清理数据源的日志即可,如果事务需要回滚,则根据日志数据生成用于补偿的 逆向SQL
XA事务优缺点分析
- 优点 由于第一阶段直接就提交了事务,立即是发放了锁和资源。避免了事务长时间阻塞,极大的提高了系统吞吐量。
- 缺点 但是也对应的牺牲了数据的隔离性,甚至影响了原子性。因为缺乏隔离的情况下,补偿代替回滚不一定总能成功。例如 本地事务提交后,分布式事务完成前,数据补偿之前又被其他操作修改过,即出现了**脏写,**这个时候如果需要数据回滚,如果直接使用之前的逆向SQL那么就出现了丢失更新了。只能人工处理。
避免脏写的全局锁 Global Lock
- 为了避免脏写丢失更新问题,GTS增加了全局锁来实现写隔离,要求所有本地事务提交前,一定要先拿到针对修改记录的全局锁才能提交没有获得全局锁之前必须要一直等待。
AT事务默认隔离级别为读未提交
- 读未提交的隔离级别会产生脏读问题
- 也可以用全局锁来实现读隔离,但是代价非常大。
七、总结
- 分布式事务一致性是保证一组需要多个节点的本地事务的一致执行,目的是和本地事务一样,保证分布式事务执行后系统数据状态的一致性。所以分布式事务也需要满足原子性、持久性和隔离性。
- 2pc和3pc 方案主要依靠节点自己的本地事务的开启和回滚加上协调者的协调实现了分布式事务的ACID。并且由于全程开启本地事务,所以分布式执行过程中数据都是强一致性的,不会出现数据不一致的状态。但是这样性能较低,并且协调者或者参与者出错时可能无法保证系统最终数据的一致性。
- 基于分布式消息的一致性方案,在事务的执行过程中可能会数据不一致,并且隔离性也不强,会有幻读问题。但是执行完成,不论失败还是成功,都能保证数据的最终一致性。是一种弱一致性的柔性事务。
- TCC事务方案则提前对资源进行锁定,后续的执行过程中几乎不涉及锁和竞争,在实现最终一致性之外,还天然有着较好的隔离性且性能好,但是对与业务侵入较高,也必须要求涉及的资源可被提前锁定,一旦涉及系统外部的资源想要提前锁定几乎是不可能的。
- SAGA基于数据补偿代替事务回滚方案,先获取所需资源并设计好失败时的补偿操作,也有着天然的隔离性,并且几乎也不涉及锁和竞争,还对本身服务可能出现的崩溃设计了日志。但是复杂度较高并且一般需要依靠其他的中间件来对服务进行编排。
- AT事务既参考了2PC也参考了数据补偿代替回滚的思想,优化了第一阶段长时间的事务阻塞,在第一阶段直接提交事务并生成补偿的SQl,但是缺点是需要全局锁对资源施加写锁,否则无法保证原子性会发生脏写。并且隔离级别只能维持在读未提交。
分布式事务方案 | 优点 | 缺点 |
---|---|---|
2PC | 1. 原理简单 2. 强一致性的刚性事务 | 1. 阻塞时间长,有一直阻塞的问题 2. 单点故障问题无法解决 3. 无法保证数据一致性 4. 同步执行,性能低下 |
3PC | 1. 强一致性 | 1. 减轻了阻塞问题 2. 减轻了单点故障问题 3. 无法保证数据一致性 4. 同步执行,性能低下 |
分布式消息最终一致性事务 | 1. 异步通信执行性能高 2. 可以保证最终数据一致性3. 基于消息中间件可以屏蔽不同服务间协议的差异 | 1. 隔离性差,有幻读问题。会出现超卖的情况 |
TCC | 1. 隔离性强,提前冻结资源后相当于加了读写锁。级别达到了可重复读 2. 操作的都是预留资源,不涉及资源竞争和锁性能非常高 | 1. 业务侵入性强,必须要求资源可被提前锁定。 2. 实现的工作量大 |
SAGA | 1. 基于补偿回滚机制,不需要锁定资源 2. 子事务异步执行性能高 | 1. 实现难度大,需要合理实现事物的正向和撤销补偿 2. 一般要依靠其他中间件进行服务编排 |
AT | 1. 各系统直接提交本地事务,吞吐量大 | 1. 隔离性没法保证,需要引入全局锁对修改的数据加锁否则原子性都会被破坏 2. 读的隔离性低,会有幻读问题。 |
八、其他
分布式事务产品
阿里 Seata
是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata将为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的分布式解决方案。
开源地址:https://github.com/seata/seata
文档地址:https://seata.io/zh-cn/docs/overview/what-is-seata.html
刚性事物和柔性事物
- 刚性事务,遵循 ACID 原则,具有强一致性。比如,数据库事务。、
- 柔性事务,其实就是根据不同的业务场景使用不同的方法实现最终一致性,也就是说我们可以根据业务的特性做部分取舍,容忍一定时间内的数据不一致。遵循的是 BASE 理论。TCC、SAGA、分布式消息一致性