分布式事务的思考


  今天思考了一下分布式事务里边常见的两阶段和三阶段算法,这里稍微记录一下自己的一点点思考。进入分布式系统之后,系统需要在多个节点上进行数据的事务操作,这就面临着常见的所谓的网络故障,机器故障等等一系列挑战。分布式事务有一种特殊的场景,就是一致性kv场景。但是一致性kv的场景可以根据自己的业务特点有特殊的解法,例如一致性读之类的算法,同时也演化出了如raft,paxos,zab之类的算法,算是解决了一种特殊的分布式事务场景,在这里就不描述了。

问题场景

  通常的分布式事务,在提交时,主要面临的是两个方面的挑战:

  1. 业务资源不足

  2. 机器故障/并发问题

  举最简单的例子,一个应用程序,需要扣除数据库A中AA表10元钱,同时也要增加数据库B中BB表10元钱。不做任何分布式事务的管控,就是依次(并发)向两个db分别发送两个单机事务:

  A: begin

   select balance from AA where id=xxxxxx for update

   /*增加业务代码逻辑判断,判断balance里有超过10元钱,根据结果进行commit或者rollback

    */

     commit/rollback

      B: begin

          update BB set balance=balance+10 where id=xxxxxx

          commit

      这里最常见的到是AA表里的账户余额不足问题,进而引起回滚操作。在整个过程中还会伴随数据不一致的情况。这个就是不引入分布式事务的场景下,同时修改多个库会遇到的问题。

两阶段

       两阶段算法,我个人理解解决的主要是业务资源不足,以及由业务资源不足产生的数据不一致问题。主要就是通过引入一个“准备阶段”来解决的,在“准备阶段”会判断所谓的业务资源是否充足,如果不充足就不会进行commit,这样就减少了上文描述的部分数据库需要回滚的场景,也降低了数据不一致的问题。解决了业务资源不足的问题,就要处理机器故障/并发场景带来的问题,注意我这里用的是“处理”,而不是解决,因为例如短时间内的数据不一致是不可解决的,只要存在最终的一个“commit”,就会有数据不一致的窗口。并发带来的影响,只能降低,不能解决。一般认为两阶段算法会遗留三个问题:

      1. 阻塞场景,这个问题其实是无解的。整个提交只要会分阶段,那么就会阻塞到“commit”请求到。这是个伪问题,分布式事务就是会减低系统整体的吞吐。

      2.单点故障,这里主要说的是协调者会在commit命令过程中发生了故障。导致部分节点commit,部分阻塞。这是一个主要问题,也是三阶段较之两阶段的主要改进点。至于在commit命令前故障,也只是会造成参与者阻塞,降低系统吞吐,引起回滚,不算问题。

      3. 数据不一致问题,就是commit报文到达各参与者时间有先后,这个也不算问题。因为这个是单纯靠分布式事务无法解决的。

三阶段

      三阶段较之两阶段基本没解决什么问题场景,唯一解决的就是协调者在commit命令过程中故障,引起部分节点commit,部分超时rollback。也就是这里会引起状态不一致,事务部分成功/部分失败。所以就引入了三阶段,但是三阶段是怎么解决的呢,就是在can和commit中间,再引入了一个中间操作,姑且叫他prepare操作。以参与者收到prepare为界,prepare之前协调者单点故障了,操作超时回滚,收到prepare之后,操作超时提交(这里翻了很多资料,没看到明确的说明在收到prepare会改变超时默认操作(提交或回滚),但总不能不改变吧,不然要这个操作干啥呢,又不能降低阻塞时间。)。算法设计者认为这样可以降低单点故障。。。。我个人觉得是纯属扯淡,因为在发送prepare操作过程中,也是可能会故障的,这个估计也是为什么三阶段协议不流行的原因。分布式一致性,靠增加确认阶段来解决问题,不是正确的方法,这也是为什么说拜占庭问题是无解问题。

TCC

      不管对于两阶段/三阶段来说,真正的吐槽,或者需要解决的问题主要其实就两个:

      1. 算法阻塞时间过长,因为主要就是调用for update/update加记录锁/间隙锁,阻塞了操作,降低吞吐

      2. 协调者的故障会引起“确认”操作分割,部分收到/部分没收到,引起状态不一致(转账:接收方收到钱,转出方没扣除)。

  分析到这里,可以看到,单纯依靠“改进消息确认”可靠性的方式或者说单纯在系统层面进行改进,是没什么效果空间的了。需要通过一些业务方面的配合才能更好的处理分布式事务(注意:也不是解决,只是处理,只能提升,不能解决)。tcc呢,可以认为是一个变种的两阶段,差别主要是try阶段,是一些业务自定义的操作。业务需要向外暴露出try/commit/rollback接口给tm使用。tcc期望这些接口可以降低对数据库的阻塞时间,注意这个“期望”二字,这个是依赖业务本身模型和程序员功底的。它没有提供一种通用的解决方式来处理分布式事务,tcc只是一种解决问题思路,但不是完整方案。把它和2pc/3pc放在一起是不合适的,但它确实是一种思路,就是不要在系统层面,而是在业务层面去解决分布式事务。在实现tcc时候,要注意降低对数据库的阻塞,不然和2pc就没什么区别了。

总结

  分布式事务首先解决的是业务资源不足导致的问题,在引入了2pc之后,业务资源不足的问题解决了,但是遗留了:1.阻塞时间过长,减低了系统吞吐;2. 单点故障,导致系统状态不一致两个问题。为了解决这两个问题,引入了类似3pc的算法,但是3pc其实也并没有改善上边两个场景(据说可以降低阻塞时间,但我没看出来),所以这个算法并不流行。在通用系统层面解决分布式事务的尝试就到头了,业界的同学尝试结合业务层面的操作来解决上述两个场景,进而就引入了TCC,系统开发者可以在业务层面进行操作,降低“阻塞时间”,思路还是不错的,就只是增加了业务层面开发人员的代码工作量。当然在解决单点故障导致的状态割裂方面,tcc也没有提供统一的解决方案。    

  那么如何进行选择呢,为了提高系统整体的吞吐量,在业务层面可以优化的情景下,就尽量使用tcc。如果使用tcc已经没什么太大意义了,不如直接使用两阶段。至于三阶段,我个人是不推荐使用的,因为看不出三阶段解决了什么问题(没有提高系统吞吐)。那么如何解决单点故障造成的状态不一致呢,这个是分布式事务最不应该出现的问题,也是会影响业务的问题,2pc/3pc/tcc都没给出足够的答案。这里唯一能想到的答案就是数据库扫描进行一致性检查的补救措施了,但也会衍生出很大的不一致时间。后边,我们再看看基于消息中间件,tidb的percolate,还有seata都能不能解决或者优化系统不一致的状态窗口

   

  

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/245706.html

(0)
上一篇 2022年4月18日 17:57
下一篇 2022年4月18日 17:57

相关推荐

发表回复

登录后才能评论