参考原文:两天,我把分布式事务搞完了
2PC和3PC的区别
- 3PC在2PC基础上,引入了协调者超时和参与者超时机制。
协调者超时:当协调者接收不到参与者的反馈时即协调者超时,协调者会默认回滚所有参与者。属于悲观型
参与者超时:当协调者挂了后,参与者一直收不到协调者的指令。那么参与者会默认提交事务,因为当前参与者有理由相信所有参与者是同意3PC的第一个阶段canCommit的,所以它大概率是可以提交的。这个参与者超时机制,可以解决当协调者挂了之后,2PC协议的所有参与者一直阻塞的问题,以及顺便解决了2PC的协调者单点问题。
总结:
- 3PC只是针对2PC的特殊情况做了优化,比如:协调者挂了后,参与者没法推进下去的问题。但是3PC并没有从根本上解决2PC的性能问题,相反由于新增一次网络交互,它的性能更差了。所以,目前并没有3PC的实现。XA实现的是2PC。
Seata的AT、TCC、SAGA模式之间的区别
AT模式
Seata框架的默认模式,属于两阶段提交。但是呢,它在第一个阶段就直接提交了事务,不让数据库占用事务资源。直接提交了,万一其他分支事务有一个或多个执行失败,想回滚咋办呢?这个就是AT模式牛逼的地方,它会创建undo_log表,在执行事务提交的sql时,seata框架会默认改造sql,得到执行前数据的快照,然后执行后再次得到数据快照。用这两个快照,构造一个undo log保存在undo_log表。这个表,一定是要和执行事务的表在一个数据库,属于一个本地事务,保证它们的原子性。
回滚的数据(叫补偿更合适)已经保存好了,就算当前事务提交了,我也能明明白白的回滚数据。但是还有个问题,当前分支事务提交了,但是其他分支事务还在执行,相当于整个分布式事务还在进行中。那么又来一个分布式事务,之前执行完成的分支事务是不是就可以为这个分布式事务服务了,但是前面的分布式事务还在执行中,它还不知道最终执行是commit还是rollback呢。这个时候,那个提交的分支事务就不能为其他任何分布式事务服务,所以还需要一个全局lock表,用来阻塞并发的分布式事务。把分布式事务从并发变成串行执行。注意:这个串行可不是表级别,而是行级别,lock表只是锁住行。如果其他分布式事务想要操作该表的其他行数据,能够拿到全局锁,可以并发执行。
TCC模式
TCC 分为指代 Try、Confirm、Cancel,是一种业务层面或者是应用层的两阶段提交。所以,在Try阶段数据库层面的事务就已经提交了,所以每个阶段都无需占用数据库资源。
Try阶段需要引入临时状态,比如:冻结状态,预添加等等字段,用来保存try阶段执行的结果。这个中间态的引入,对业务的数据库设计有非常大的侵入。同时,每个服务都需要改造成Try、Confirm、Cancel三个阶段的服务,对代码侵入也很大。
比如有一个扣款服务,我需要写 Try 方法,用来冻结扣款资金,还需要一个 Confirm 方法来执行真正的扣款,最后还需要提供 Cancel 来进行冻结操作的回滚,对应的一个事务的所有服务都需要提供这三个方法。
TCC 的注意点
幂等问题,因为网络调用无法保证请求一定能到达,所以都会有重调机制,因此对于 Try、Confirm、Cancel 三个方法都需要幂等实现,避免重复执行产生错误。
空回滚问题,指的是 Try 方法由于网络问题没收到超时了,此时事务管理器就会发出 Cancel 命令,那么需要支持 Cancel 在未执行 Try 的情况下能正常的 Cancel。
悬挂问题,这个问题也是指 Try 方法由于网络阻塞超时触发了事务管理器发出了 Cancel 命令,但是执行了 Cancel 命令之后 Try 请求到了,你说气不气。
这都 Cancel 了你来个 Try,对于事务管理器来说这时候事务已经是结束了的,这冻结操作就被“悬挂”了,所以空回滚之后还得记录一下,防止 Try 的再调用。
Saga 模式
参考原文:Saga模式
这个 Saga 是 Seata 提供的长事务解决方案,适用于业务流程多且长的情况下,这种情况如果要实现一般的 TCC 啥的可能得嵌套多个事务了。
Saga的组成
每个Saga由一系列sub-transaction Ti 组成
每个Ti 都有对应的补偿动作Ci,补偿动作用于撤销Ti造成的结果
可以看到,和TCC相比,Saga没有“预留”动作,它的Ti就是直接提交到库。
Saga的执行顺序有两种:
执行成功:T1, T2, T3, ..., Tn
执行失败:T1, T2, ..., Tj, Cj,..., C2, C1,其中0 < j < n
Saga定义了两种恢复策略:
- backward recovery,向后恢复,补偿所有已完成的事务,如果任一子事务失败。即上面提到的第二种执行顺序,其中j是发生错误的sub-transaction,这种做法的效果是撤销掉之前所有成功的sub-transation,使得整个Saga的执行结果撤销。
- forward recovery,向前恢复,重试失败的事务,假设每个子事务最终都会成功。适用于必须要成功的场景,执行顺序是类似于这样的:T1, T2, ..., Tj(失败), Tj(重试),..., Tn,其中j是发生错误的sub-transaction。该情况下不需要Ci。
显然,向前恢复没有必要提供补偿事务,如果你的业务中,子事务(最终)总会成功,或补偿事务难以定义或不可能,向前恢复更符合你的需求。
理论上补偿事务永不失败,然而,在分布式世界中,服务器可能会宕机,网络可能会失败,甚至数据中心也可能会停电。在这种情况下我们能做些什么? 最后的手段是提供回退措施,比如人工干预。
对于ACID的保证:
Saga对于ACID的保证和TCC一样:
- 原子性(Atomicity):不支持
- 一致性(Consistency),在某个时间点,会出现A库和B库的数据违反一致性要求的情况,但是最终是一致的。
- 隔离性(Isolation):不支持,A事务能够读到B事务部分提交的结果。
持久性(Durability):和本地事务一样,只要commit则数据被持久。
Saga不提供ACID保证,因为原子性和隔离性不能得到满足。原论文描述如下:
full atomicity is not provided. That is, sagas may view the partial results of other sagas
通过saga log,saga可以保证一致性和持久性。
和TCC对比
Saga相比TCC的缺点是缺少预留动作,导致补偿动作的实现比较麻烦:Ti就是commit,比如一个业务是发送邮件,在TCC模式下,先保存草稿(Try)再发送(Confirm),撤销的话直接删除草稿(Cancel)就行了。而Saga则就直接发送邮件了(Ti),如果要撤销则得再发送一份邮件说明撤销(Ci),实现起来有一些麻烦。
如果把上面的发邮件的例子换成:A服务在完成Ti后立即发送Event到ESB(企业服务总线,可以认为是一个消息中间件),下游服务监听到这个Event做自己的一些工作然后再发送Event到ESB,如果A服务执行补偿动作Ci,那么整个补偿动作的层级就很深。
不过没有预留动作也可以认为是优点:
- 有些业务很简单,套用TCC需要修改原来的业务逻辑,而Saga只需要添加一个补偿动作就行了。
- TCC最少通信次数为2n,而Saga为n(n=sub-transaction的数量)。
- 有些第三方服务没有Try接口,TCC模式实现起来就比较tricky了,而Saga则很简单。
- 没有预留动作就意味着不必担心资源释放的问题,异常处理起来也更简单(请对比Saga的恢复策略和TCC的异常处理)。