参考资料
非常棒的一篇博客,把2PC和3PC阐述的很清楚
https://blog.csdn.net/gududedabai/article/details/82993594
https://baijiahao.baidu.com/s?id=1607829339793465291&wfr=spider&for=pc
TCC的介绍,一个看得懂的介绍
https://blog.csdn.net/kobejayandy/article/details/54783212
总结
分布式事务解决方案:
-
幂等性+重试+日志:适合一致性要求没有那么高的业务场景,比如RocketMQ的事务消息。每个消息有一个唯一的KeyID,Broker会重试调用Producer的提交回调方法,重试一定次数后会记录日志。缺点是可能不一致,Producer已经提交了,但是Ack一直失败,最终超过重试次数,停止调用,但是Consumer看不到,需要人工干预。
-
2PC + 回滚 可以降低出现不一致的可能性,分布式协调器 分别校验且锁定资源成功后,再发起提交,如果都提交成功则操作成功,如果有异常,则通通回滚。优点是编程简单,事务安全性高,缺点是阻塞严重,容错率差。
-
3PC为了缓解2PC的问题,引入了超时自动处理的机制,以代码的复杂性来换可用性。
-
2PC和3PC都可以引入部分资源锁定的方式,来提高并发度。
幂等性设计方案
幂等性+重试+事务日志+人工干预 实现轻量级的分布式事务。
模拟一个2个参与者的分布式事务操作,A向B转账100万
-
开启一个独立的事务:生成一个唯一性的TXID,记录分布式操作的日志,当前事务状态为已经开启,但是结果为null。同时执行A扣款100 万这个操作。提交事务
-
第一步操作成功以后,开始调用B系统,参数里面有TXID,B用户系统在增款的时候会校验TXID是否已经存在了,然后重试这个操作,直到B用户系统反馈操作成功
-
如果B成功反馈,那么更新分布式事务操作表,设置TXID的状态为成功
-
如果B重试失败次数超过阈值,则更新分布式事务操作表的TXID状态为 重试失败
-
定时的去check 分布式事务操作表,人工排查,TXID状态为空(可能 A、 B都成功了,但是在反馈的时候失败了)和TXID状态为重试失败的交易
2PC
实现原理:调用者通过调用协调器,让协调者来统筹分2阶段来调用所有的参与者,来实现一个分布式的事务的执行
image
模拟一个成功的流程
- prepare 阶段:协调器向执行者发送Prepare消息,执行者:开启事务、校验逻辑能否执行成功、最终反馈ACK(Yes 或者No)
- commit阶段:当协调器收到所有的参与者ACK都是Yes的时候,会广播Commit消息,参与者最终提交事务,释放锁住的资源,并且ACK执行结果(Yes 或者No)
当Commit阶段所有的参与者都反馈Yes的时候,协调者可以反馈给调用者,该分布式操作执行成功。
异常的情况:
- Prepare阶段,如果有参与者反馈ACK=No的时候(比如某个银行账户扣款操作余额不足),协调者就会广播Abort的消息给所有的参与者,参与者结束事务,释放资源,操作失败。
- Commit阶段,如果有参与者ACK=No的时候(理论上这种情况的概率很低,因为Prepare阶段已经锁住了相关的资源)
该方案的缺点:阻塞并发差、容错率低、
-
容错率低:Prepare阶段任何一个参与者宕机,或者消息网络丢失;或者协调器在准备执行Commit时宕机,则所有的参与者都会导致所有的参与者和协调者都盲目等待,并且锁住了所有的事务资源
-
阻塞并发差:容错异常时会盲目阻塞等待,另外锁住资源太过暴力,例如A用户银行余额100万,事务1扣款10万元,事务2扣款20万元,如果事务1暴力的锁住了A用户的全部余额,导致事务2只能等待,实际上可以在业务上优化,做一个锁定部分资源,比如冻结10万元的操作,从而提高并发性
-
实际上该方案还需要添加一个回滚的接口,如果在Commit阶段,如果有参与者执行失败ACK=No,或者超时仍然未反馈结果(例如某个参与者宕机了),那么为了保持一致性,需要回滚所有的执行操作
该方案的优点:编码简单,参与者实现3个接口:Prepare方法、Commit方法、Abort方法即可
3PC
举个栗子:工行A用户 向 建行B用户汇款100万
协调器:
- 第一阶段协调器发起 canCommit:工行check A账户是否正常,然后余额是否够100万,异常或者不够100万直接反馈No即可,建行check B账户是否正常
- 第二阶段协调器发起 preCommit: 工行尝试锁定A账户的所有资金,锁定成功反馈Yes;建行同时也要锁定 B账户,防止2个用户同时向该账户汇款,导致金额异常
- 第三阶段协调器发起 doCommit: 协调器收到 工行、建行都反馈 preCommit执行成功,则告知commit,工行给A账户扣款100万,解锁A账户。建行给B账户增款100万,解锁B账户。并反馈ACK给协调器
协调器收到工行、建行的doCommit的Ack=Yes时,可以反馈调用者:跨行转账成功
https://blog.csdn.net/gududedabai/article/details/82993594
- 3PC为了改善2PC在节点异常情况下的盲目阻塞,当然代价是提升了代码的复杂度。所有的参与者要实现5个接口(canCommit接口、preCommit接口、havaCommit接口、Abort接口、回滚接口)
- 但是3PC仍然存在数据不一致的情况,比如在第二阶段,协调者发起PrepareCommit消息后宕机,因为第一阶段并没有开启事务,没有加锁,所以PrepareCommit阶段是有可能失败的,而协调者挂了,那么参与者在超时后会选择commit,这就导致了部分参与者commit,部分参与者commit失败的情况
- 但是3PC已经大大提升了容错率和阻塞性了,大部分的时候,都可以实现很好的性能
实现的原理参考引用的博客,下面是几点思考:
- 第一阶段的意义何在?
第一阶段其实是为了降低数据不一致的可能性,同时又不锁定任何资源。就好比汇款之前先check一下你的余额是否充足一样,提高转账的成功率。
其实也可以通过 2PC+超时自动处理来解决阻塞问题
比如: Prepare阶段,参与者反馈ACK=Yes,但是迟迟接收不到协调者commit的消息,可以自动提交事务。但是这样做的话,状态不一致的风险就很大,因为可能其它参与者的执行业务复杂耗时较长,但是是不满足执行条件的,引入第一阶段的 canCommit,既可以降低数据不一致的可能性,同时因为没有事务没有锁,不会对当前执行者有并发干扰,以小的成本换来更高的成功率。所以引入了canCommit这个阶段
TCC(事务补偿)
TCC实际上和2PC、3PC并不冲突,只是注重的点不同而已,为了提升2PC、3PC的并发度。
-
实现原理:引入了活动事务管理器,引入了一个try的阶段,只锁定事务本身需要的资源来确保事务正常进行。
TCC(Try-Confirm-Concel) 模型 [7] 同样是一种补偿性事务,主要分为 Try:检查、保留资源,Confirm:执行事务,Concel:释放资源三个阶段。
活动管理器记录了全局事务的推进状态以及各子事务的执行状态,负责推进各个子事务共同进行提交或者回滚。同时负责在子事务处理超时后不停重试,重试不成功后转手工处理,用以保证事务的最终一致性。 -
举例子: A转账给C 100元
- try 阶段:查看 A、C账号可用,且A的余额大于100元,大于的话,冻结100元(这是核心啊,不是冻结A的所有存款,而是通过业务的手段只冻结100元,此时A账户的其它余额仍然处于可用的状态)
- commit阶段:A账号解除冻结且扣款100元,因为之前该现金已经处于冻结状态,所以肯定会扣款成功。C账号增加100元
- Canncel阶段:如果A扣款失败的话,需要回调C的回调接口,如果C增长失败的话,需要回调A账号的扣款成功。如果都成功,就不需要Cannel任何资源
网友评论