目录
三、分布式事务的6个协议---2PC、3PC、XTS、TCC、查询补偿、消息事务与消息重试
(2PC、3PC、XTS、TCC、查询补偿、消息事务与消息重试)
一、转账举例与事务对比
1、转账举例
分布式事务对于一致性的要求是强一致性,因此对于我们后续讨论有一定的借鉴意义。如:bob给smith转账,强一致性的要求一定是需要对外来说bob减钱的同时smith加钱。
图1:单机环境下的一致性 |
图2:分布式环境下的一致性 |
单机环境见图1:bob的减钱和smith的加钱都转同一个库来做,可以采用数据库的事务特性轻松支持。保证bob给smith转账的安全性。而分布式环境见图2:假设应用服务器是A,bob端的数据库是B,smith端的数据是C,那么A做成一个转账,需要B事务成功提交,并且C事务成功提交。然而因为网络的影响,可能出现两种情况
1. 如果bob扣款成功,而网络通知smith失败了,则会出现bob的钱减了,smith的钱没加
2. 如果bob扣款不成功,而smith加钱成功了,则会出现smith钱增加了,但是bob的钱也没减少
2、事务对比
1、本地事务(数据库事务/传统事务) & 分布式事务
本地事务(数据库事务/传统事务): | 优:支持严格的ACID特性。缺:不具备分布式事务处理能力;隔离最小单位受限于资源管理器。 |
分布式事务: | 一次大操作欧不同小操作组成,小操作分布在不同的服务器上,保证这些小操作要么全部成功,要么全部失败。 |
2、刚性事务 & 柔性事务
刚性事务: | 完全遵照ACID规范的义务,最常见的是:本地事务(数据库事务/传统事务)mysql。 |
柔性事务: | C(一致性)A(可用性)P(分区容错性)中,为了满足A,降低了C(一致性)和隔离性的要求。 |
二、分布式理论(CAP, BASE)
1、分布式的CAP理论---一致性+可用性+分区容错性
C(一致性):每次读取要么获得最近写入的数据,要么获得一个错误。?这个解释怪怪的?
A(可用性):每次请求都能获得一个(非错误)响应,但不保障返回的是最新写入的数据。
P(分区容错性):尽管任意数量的消息被节点间的网络丢失(或延迟),系统仍继续运行。说人话,就是部分节点挂了,还能正常提供服务。
注意:CAP中的A与ACID中的C的区别:ACID的C保证事务一致性,着重强调单数据库事务操作时,要保证数据的完整和正确性,,而CAP理论中的C强调的是对一个数据多个备份的读写一致性。
- ACID的C指的是事务中的一致性。在一系列对数据修改的操作中,保证数据的正确性。即数据在事务期间的多个操作中,数据不会凭空的消失或增加,数据的每一个增删改操作都是有因果关系的。比如用户A向用户B转了200块钱,不会出现用户A扣了款,而用户B没有收到的情况。
- CAP中的C就是为了做到在分布式环境中读取的数据一致。在分布式环境中,多服务之间的复制是异步,需要一定耗时,不会瞬间完成。在某个服务节点的数据修改之后,到同步到其它服务节点之间存在一定的时间间隔,如果在这个间隔内有并发读请求过来,而这些请求又负载均衡到多个节点,可能会出现从多个节点数据不一致的情况,因为请求有可能会落到还没完成数据同步的节点上。CAP中的C就是为了做到在分布式环境中读取的数据是一致的。
2、分布式的BASE理论---基本可用+软状态+最终一致性
(论文详见:https://dl.acm.org/doi/10.1145/1394127.1394128)
BASE理论本质:对CAP理论的延伸,是对CAP中AP方案的一个补充。提升AP,降低对C的要求。
BASE理论的核心思想:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当方式来使系统达到最终一致性。
BASE理论的基本原则有3个:Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)。
主要目的是为了提升分布式系统的可伸缩性。
(1)基本可用
基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。不过,这绝不等价于系统不可用。比如:
响应时间上的损失:正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障,查询结果的响应时间增加了1~2秒。
系统功能上的损失:正常情况下,在一个电子商务网站上进行购物的时候,消费者几乎能够顺利完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。
(2)软状态
软状态,也被称之为柔性状态,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
(3)最终一致性
最终一致性强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
三、分布式事务的6个协议---2PC、3PC、XTS、TCC、查询补偿、消息事务与消息重试
(2PC、3PC、XTS、TCC、查询补偿、消息事务与消息重试)
1、2PC(二阶段提交协议-阻塞协议)
这种不一致的问题困扰着大家。任意一边出错想要回滚另一边都不是简单的数据库回滚的事情( 因为此时已经成功提交),而是需要做业务的逆向操作,而不同业务的逆操作都不同,导致复杂性增加。考虑数据库事务的执行实际上是先将执行操作写入binlog,等到最后通过一个commit指令将binlog的内容一次更新到表中,或者写到一半通过一个rollback指令将binlog中的内容回滚。于是乎,可以想到使用2个阶段来执行这个过程,第一阶段,写入binlog;第二阶段执行commit或者rollback。这就是著名的两阶段提交协议(2PC)。如果仔细考虑,会发现两阶段协议并没有解决问题,只不过降低了出错的概率而已,因为第二阶段同样存在上面的两种情况。注意最终状态是多台机器的状态&&的 结果。上图是两阶段协议的时序图。
![]() | 1. 考虑prepare阶段的响应(因为请求阶段和执行阶段都可以在最后响应中体现出来),对于分布式环境中,任意时刻考虑3种状态:成功、失败、超时。 2. 考虑commit阶段,同样考虑成功失败超时3种状态。 |
3.1.1、算法思路:参与者RM将操作成败通知协调者TM,再由协调者TM根据所有参与者RM的反馈情报决定各参与者是否要提交操作还是中止操作。 3.1.2、实现步骤:在此协议中,一个事务管理器(Transaction Manager,简称 TM,也被称之为“协调者”)协调 1 个或多个资源管理器(Resource Manager,简称 RM,也被称之为“参与者”)的活动,所有资源管理器(参与者)向事务管理器(协调者)汇报自身活动状态,由事务管理器(协调者)根据各资源管理器(协调者)汇报的状态(完成准备或准备失败)来决定各资源管理器(协调者)是“提交”事务还是进行“回滚”操作。 3.1.3、具体实现: 第一阶段(准备阶段) 协调者通知各个参与者准备提交它们的事务分支。如果参与者判断自己进行的工作可以被提交,那就对工作内容进行持久化,再给协调者肯定答复;要是发生了其他情况,那给协调者的都是否定答复。在发送了否定答复并回滚了已经的工作后,参与者就可以丢弃这个事务分支信息。 以MySQL数据库为例,在第一阶段,事务管理器(协调者)向所有涉及到的数据库服务器(参与者)发出Prepare "准备提交"请求,数据库(参与者)收到请求后执行数据修改和日志记录等处理,处理完成后只是把事务的状态改成"可以提交",然后把结果返回给事务管理器(协调者)。 第二阶段(执行阶段) 协调者根据第一阶段中各个参与者 Prepare的结果,决定是提交还是回滚事务。如果所有的参与者都Prepare成功,那么协调者通知所有的参与者进行提交;如果有参与者Prepare失败的话,则协调者通知所有参与者回滚自己的事务分支。 还是以MySQL数据库为例,如果第一阶段中所有数据库(参与者)都Prepare成功,那么事务管理器(协调者)向数据库服务器(参与者)发出"确认提交"请求,数据库服务器(参与者)把事务的"可以提交"状态改为"提交完成"状态,然后返回应答。如果在第一阶段内有任何一个数据库(参与者)的操作发生了错误,或者事务管理器(协调者)收不到某个数据库(参与者)的回应,则认为事务失败,回撤所有数据库(参与者)的事务。数据库服务器(参与者)收不到第二阶段的确认提交请求,也会把"可以提交"的事务回撤。 3.1.4、缺陷: 2PC提交的优点是尽量保证了数据的强一致,但不是 100% 一致。但是2PC也有明显的缺点: a.单点故障(TM故障则所有RM事务锁定):由于协调者的重要性,一旦协调者发生故障,参与者会一直阻塞,尤其是在第二阶段,协调者发生故障,那么所有的参与者都处于锁定事务资源的状态中,而无法继续完成事务操作。 b.同步阻塞(参与者占有公共资源则其他节点阻塞):由于所有节点在执行操作时都是同步阻塞的,当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。 c.数据不一致(TM提交后故障,部分RM收到提交请求执行后故障):在第二阶段中,当协调者向参与者发送提交事务请求之后,发生了局部网络异常或者在发送提交事务请求过程中协调者发生了故障,这会导致只有一部分参与者接收到了提交事务请求。而在这部分参与者接到提交事务请求之后就会执行提交事务操作。但是其他部分未接收到提交事务请求的参与者则无法提交事务。从而导致分布式系统中的数据不一致。 |
2、3PC(三阶段提交协议-非阻塞协议)
(论文详见:https://en.wikipedia.org/wiki/Three-phasecommitprotocol)
![]() | 1. 考虑cancommit阶段的响应。 a.成功。不必处理,执行后续行为precommit。 b.失败。说明无法执行,无须后续提交或回滚行为。 c.超时。保守处理,超时可以当做失败。
2. 考虑precommit阶段的响应。 a.成功。不必处理,执行后续行为docommit。 b.失败。执行阶段出错,执行后续行为rollback。 c.超时。执行阶段太慢,也可能是网络阶段太慢或丢包,但是保守处理,超时可以当做出错。
3. 考虑cancommit阶段的响应。 a.成功。整个事务成功执行。 b.失败。提交出错,假设此时前面的B已经提交成功了,则同样面临无法回滚的问题。 c.超时。保守处理,超时可以当做失败。 例外情况,即自cancommit返回成功后的任意阶段A挂掉了,那么BC同样能够知道这个事务正在发生(因为cancommit已经提交了足够信息让BC知晓此事),于是BC可以在无A的情况下继续执行后续的阶段(比如BC投票启动新的A’,并提供A’足够信息)。于是3PC正好解决了2PC的例外情况。 但是3PC仍然存在类似2PC的问题,即最后阶段失败或超时同样有可能出现数据不一致的问题。所以3PC仍然只是降低了发生概率,并没有真正解决问题。 |
3.2.1、相比2PC的改动
3.2.2、缺点 中断事务请求的参与者存在数据不一致的问题(TM提交中断事务请求但RM没收到,RM等待超时后提交了事务):在提交阶段如果发送的是中断事务请求,但是由于网络问题,导致部分参与者没有接到请求。那么参与者会在等待超时之后执行提交事务操作,这样这些由于网络问题导致提交事务的参与者的数据就与接受到中断事务请求的参与者存在数据不一致的问题。所以无论是 2PC 还是 3PC 都不能保证分布式系统中的数据 100% 一致。 3.2.3、具体实现 第一阶段CanCommit(事务询问+响应反馈) 3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送Commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
第二阶段PreCommit(发送预提交请求+事务预提交+响应反馈 / 发送中断请求+中断事务) 协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能。
第三阶段DoCommit(发送提交请求+事务提交+响应反馈+完成事务 / 发送中断请求+事务回滚+反馈结果+中断事务) 该阶段进行真正的事务提交,也可以分为以下两种情况。
|
3、XTS
XTS本质上是2PC(实际上如果引入3PC会多2n次网络交互,在量大时反而更加不安全)。XTS引入协调者A的server部分,实际上是一个大集群,以配置的方式接入各种需要分布式事务的业务,集群由专门的团队维护,保证其可用性和性能;而协调者A的client部分则通过发起方调用,prepare阶段时,先通过client将本次事务信息发送到server,落库,然后即时推送prepare请求到B和C,当收到B,C的响应时把他们状态入库,如果正常,则做commit提交;否则会用定时任务去推送未完成的状态直到完成。上文提到的prepare之后协调者A挂了这种情况,在server集群的保证下,几乎很少会发生。而上文提到的所有超时的情况,都可以通过定时任务推送拿到一个确定的状态而不是盲目的选择回滚或者提交。另外由于B和C都是集群,很少会发生多次请求过去无响应的情况。直到最后一种情况就是commit时B成功了C失败了,或者反过来B失败C成功,这种情况成为悬挂事务,最终等待人工来解决,据说每天都有几笔到几十笔。无疑XTS作为2PC在工业界的应用,是相当了不起的设计,通过各种方式规避了各种可能的不一致性,在性能,效率等方面做到了平衡。
4、TCC(Try/Confirm/Cancel)
业务补偿类型,其基本思想是对每一个业务操作做一个逆操作,一旦成功了,就做正向业务,一旦失败了就做业务的逆操作。通常在业务逻辑简单并且正逆操作清晰的时候用比较好。
5、查询补偿
典型的场景是向银行发送了转账请求未得到明确的成功失败返回码,此时先做业务结果的查询,根据结果做相应处理,比如查询结果成功,则置状态为成功,查询结果失败,则做相应的业务补偿,查询结果为未知,则继续查询。
6、消息事务及消息重试
事务消息及消息重试本质上都是将一些通用的事务交给消息中间件,通过消息中间件来保证消息的最终一致性。
事实上,消息事务解决了这类问题,即本地事务和消息应当有一致性,解决这个一致性比较麻烦,比如消息中间件和业务同时实现XA;或者采用一些更加复杂的方式,比如将消息表与业务表放同库,利用数据库的事务来保证一致性,而消息系统只需要轮训该消息表即可;当然,也有消息的二阶段提交+补偿的方式。消息事务解决了消息发起方,即生产者与消息中间件之间的一致性问题。
try {
//数据库操作;消息投递
} catch(Exception e) {
//回滚
}
消息中间件与消费者之间的一致性问题则需要通过重试+幂等来解决。消息重试中主要考虑重试次数以及重试时间的阈值变化。
转自:https://mp.weixin.qq.com/s/csw6s7HdNZv7-Mefk9wKzg(zookeeper分布式事务)