读TiDB原理部分,知道其分布式事务是参考的Google percolator。而percolator是一种2PC的优化。
分布式事务解决的是什么问题呢?
假设一个场景,一个电商网站,用户在购买商品时,需要两步操作1)创建订单,2)扣减库存。我们通常希望这两步是事务的,要么同时成功,要么同时失败。如果订单创建成功,库存扣减失败,会导致超卖。如果订单创建失败但扣减了库存呢,会导致少卖。
怎么解决这个问题呢?
如果订单表和商品表在MySQL同一个逻辑DB里面,可以使用MySQL的单机事务来保证。
如果在不同DB呢?
一种折中的做法,分别在两张表上开事务A、B,两事务都执行完后,同时commit,我们将重要的事务放在前面commit;这样如果A事务失败,则AB同时回滚。但如果A成功commit,B失败,就存在数据不一致的问题。
上面我们让AB等待同时完成后提高,一定程度降低了这种不一致发生的概率。但不是个完善的解决方案。
下面介绍的2PC,为解决这个问题。
首先2PC中有一个协调者节点,多个参与者节点(不同的数据库表)。
【一】协调者协议流程如下:
1. 开启一个事务,写本地日志begin_transaction,进行wait状态
2. 向所有参与者节点发送prepare消息,并等待参与者节点对prepare消息的响应
a. 若任何一个节点返回vote-abort消息,则写本地日志global-abort, 行向所有参与者节点发送global-abort消息,进入ABORT状态
b.若收到所有参与者节点返回的vote-commit消息,本地日志写global-commit日志,并向所有参与者节点发送golobal-commit消息,进入COMMIT状态】
3.等待参与者节点的global-commit或global-abort消息的回复,若所有节点都回复,则写日志end_transication,并完成事务。
【二】参与者协议流程如下:
1.开启事物,写本地日志init,进入INIT状态
2.等待协调者的prepare消息
a.若可以进行本次事务,则写本地日志ready,进入READY状态,并向协调者节点发送vote-commit消息等待回复
b.若收到回复global-abort,写本地日志abort,进入ABORT状态,并向协调者节点发送回复
c.若收到global-commit消息,写本地日志commit,进入COMMIT状态,向协调者发送回复
3.若参与者无法进行本次事务
a.写本地日志abort,进入abort状态,并向协调节点发送vote-abort消息。可以对后序收到的global-abort消息进行响应
4.即使流程结束,但任何时候收到协调者发送的global-abort或global-commit消息,也发送一个相应的回复。
** 以上操作都是先写日志,再进行处理
接下来讨论一下异常处理:
【协调者节点宕机恢复】
先看一下协调节点几种可能日志记录:begin_transiaction, global-commit或global-abort, end_transication
协调者宕机恢复后,先让到事务其最新日志,若是begin_transiaction,表示协调者处于WAIT状态,此时或者已经发送过prepare消息,也可能没有发过。但可以确认,一定没有发送过global-commit或global-abort消息。此时只需要重发prepare消息,即使参与者已经收到并回复过prepare消息,此时只需重新发一条即可。不影响一致性。
如果日志中最后是global-commit或global-abort日志。说明宕机前处于COMMIT或ABORT状态,此时协调者只需向参与者再发一次global-commit或global-abort消息,继续2PC流程。
【参与者节点宕机恢复】
如果日志处于init状态,表示还未对本事务做出选择,继续等待prepare消息即可。
如果处于ready状态,说明已经收到了prepare消息,但是否已经做出回复 不消息可知;所以重发vote-commit消息即可。注意这里是发送的vote-commit而不是vote-abort,因为只有本次事务可以提交,才会到ready状态。
如果日志最后是commit或abort状态,则表示已经收到了global-commit或global-abort消息,但不能确定是否已经发送过了确认消息。这时候只需要等待新的 global-commit或global-abort消息,并进行回复即可。因为协调者节点会不断重发消息。
分布式系统中,错误一般分为两块,超时和其它错误,其中超时是最难处理的错误。接下来讨论超时的问题。
协议的异常,体现在等待消息的超时上面。
【一,协调者在WAIT状态超时】
一般有两种原因,1.协调者与某个参与者之间的网络断开。2.某个参与者宕机,这种超时,可以选择放弃整个事务。因为WAIT状态下,协调者一定未发送来global-abort或global-commit消息,因此只要向所有参与者发送global-abort停止事务就可以,不影响协议正确性。
【二,协调者在COMMIT或ABORT状态超时】
此时,等待参与者对global-commit或global-abort的响应消息超时。这种情况下协调者只能不断重发global-commit或global-abort消息,直到所有参与者都响应。
2PC对这种情况没有很好的容错,只能阻塞在这里不断重试。其中任何结点的超时,或者协调者本身的网络问题,都会导致2PC完成不了。
【三,参与者INIT状态超时】
此时还没收到prepare消息,直接abort即可。但可能导致原先可以提交的事务不能成功完成。
【三,参与者READY状态超时】
READY状态,代表参与者收到prepare消息,并回复了vote-commit消息。此时参与者不能再改变自己的选择,只能不断重发vote-commit,直到收到global-abort或global-commit消息,继续下面流程。这里可以对应到协调者不断重发global-commit或global-abort消息,同样没有很好的容错机制。整个流程阻塞在这里,对于参与者而言,协议状态处于未知,即不能提交本节点事务,也不能放弃本节点事务。(如果提交了,实际协调者发送了global-abort,则在本节点提交,其它节点未提交,导致数据不一致。如果放弃了,则实际协调者发送了global-commit,则本节点放弃,其它节点提交了,同样导致数据不一致。)
实际上,2PC的有很多缺点:
1)容错很差,以上超时分析可知。
2)性能很差,一次通信涉及到4次消息交互,慢节点对整个协议性能影响很大。
3)可用性差,协调者单点