分布式事务也要让分布式系统下操作满足基本特性:ACID(原子性,一致性,隔离性,持久性)。
理解前提:分布式事务不可能100%解决分布式业务不一致性,只能尽可能提高作为原子执行的功率。
2PC
2pc&3PC: 依赖于数据库层面做回滚等操作,需要控制数据库资源,不和具体业务逻辑挂钩,不太适用于微服务,之前常用于单系统跨库的应用。
准备阶段
询问每个事务参与者是否可以提交事务,并执行事务业务逻辑,每个事务参与者执行Undo和Redo日志操作;
提交阶段
协调者(应用软件组件)向所有事务参与者发送正的commit请求,参与者执行Commit请求提交事务,释放整个事务期间占用的资源。
若所有参与者均反馈Ack完成的消息,则完成事务提交,否则当存在超时或者反馈失败时,协调者向所有参与者发出回滚请求(即Rollback请求)。
2PC缺陷
同步阻塞
1、提交的执行过程中,所有参与该事务操作的逻辑都处于阻塞状态,各个参与者在等待其他参与者响应的过程中,将无法进行其他任何操作。
2、若由于宕机、网络不稳定等导致部分参与者无法回复时,其他参与者也一直阻塞下去;
ps:对于阻塞,貌似3pc也不能避免而只能通过划分更小阶段后最大程度上去避免
单点
在发生协调者宕机后,就算集群中重新选出协调者也无法处理宕机时已有的事务状态(事务状态没有被存储,无法自主也无法使用定时器等做超时处理),所有参与者均处于锁定状态并会一直阻塞下去,尤其在第二阶段;
数据不一致
在阶段2中,如果只有部分参与者接收并执行了Commit请求(协调者commit消息只被部分参与者接收到,如,协调者发送部分commit请求后挂了),会导致节点数据不一致。
3PC
三阶段提交有两个改动点(仍然依赖了数据库本身的特性):
- 引入超时机制。同时在协调者和参与者中都引入超时机制。
- 准备阶段再次一分为二。保证了在最后提交阶段之前各参与节点的状态是一致的,较少了最后提交的可能失败点。
相比较2PC而言,3PC对于协调者(Coordinator)和参与者(Partcipant)都设置了超时时间,而2PC只有协调者才拥有超时机制。这个优化点,主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源(减少卡死)。通过这种机制侧面降低了整个事务的阻塞时间和范围。整体流程示意图如下:
step 1 - CanCommit
事务开启
1、协调者向所有参与者发出包含事务内容的CanCommit请求,询问是否可以开启事务。
2、若参与者可以执行事务操作(在实际的场景中参与者节点会对自身逻辑进行事务尝试,其实说白了就是检查下(链路)健康性(或者锁定资源[中间态]),看有没有能力进行事务操作[参与者不执行事务操作]),则反馈YES并进入预备状态。
step 2 - PreCommit
PreCommit是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的。
事务预提交
1、协调者向所有参与者发出PreCommit请求(开启事务尝试获得锁),每个事务参与者执行Undo和Redo日志操作;
2、参与者发出执行事务操作(uncommit);
事务中断
no反馈:若任何一个参与者反馈NO,则协调者向所有参与者发送ABORT消息,中断事务;
超时:等待超时,则参与者中断事务(避免了协调者的单点问题)。
step 3 - doCommit
提交事务
向所有的参与者发送commit消息,所有参与者均反馈Ack响应,也即执行真正的事务提交。
中断事务
no反馈:任何一个参与者反馈NO,则协调者(在其仍工作情况下)向参与者发送abort中断消息,即根据Undo信息执行回滚操作,并释放整个事务期间占用的资源。
超时:等待超时无doCommit请求或abort请求时,自动继续执行事务提交(避免了协调者的单点问题)。这是与2pc明显的一个不同点,参与者自己也存在超时处理,因为preCommit收到了Ack,认为大概率能够commit
3PC缺陷
数据不一致
参与者收到PreCommit请求后等待最终指令(doCommit指令),如果此时协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。– 是否可借鉴类似RocketMQ事务型消息的回查机制?
参与者如果在不同阶段宕机,我们来看看3PC如何应对:
- 阶段1: 协调者或协调者备份未收到宕机参与者的vote,直接中止事务;宕机的参与者恢复后,读取logging发现未发出赞成vote,自行中止该次事务
- 阶段2: 协调者未收到宕机参与者的precommit ACK,但因为之前已经收到了宕机参与者的赞成反馈(不然也不会进入到阶段2),协调者进行commit;协调者备份可以通过问询其他参与者获得这些信息,过程同理;宕机的参与者恢复后发现收到precommit或已经发出赞成vote,则自行commit该次事务
- 阶段3: 即便协调者或协调者备份未收到宕机参与者t的commit ACK,也结束该次事务;宕机的参与者恢复后发现收到commit或者precommit,也将自行commit该次事务
2PC与3PC总结
- 3pc解决了事务状态不可知的问题。不过其对执行者引入超时机制(超时后根据执行器当前状态canCommit or preCommit回滚或者提交事务,释放事务占用的资源),如果发生网络分区,会导致事务数据不一致(部分节点自主提交),虽然提升了系统可用性,不过牺牲了系统一致性。
- 2pc 3pc归根到底是选择系统可用性还是选择系统一致性(CAP理论中的抉择问题)
- 2pc 一致性好、可用性较低,3pc 一致性较低、可用性高
TCC
TCC事务机制相对于传统事务机制(X/Open XA),其特征在于它不依赖资源管理器(RM)对XA的支持,而是通过对(由业务系统提供的)业务逻辑的调度来实现分布式事务。对于业务系统中一个特定的业务逻辑S,其对外提供服务时,必须接受一些不确定性,即对业务逻辑执行的一次调用仅是一个临时性操作,调用它的消费方服务M保留了后续的取消权。如果M认为全局事务应该rollback,它会要求取消之前的临时性操作,这就对应S的一个取消操作。而当M认为全局事务应该commit时,它会放弃之前临时性操作的取消权,这对应S的一个确认操作。 每一个初步操作,最终都会被确认或取消。因此,针对一个具体的业务服务,TCC事务机制需要业务系统提供三段业务逻辑:初步操作Try、确认操作Confirm、取消操作Cancel。
TCC事务的处理流程与2PC两阶段提交类似,不过2PC通常都是在跨库的DB层面,而TCC本质上就是一个应用层面的2PC,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。
核心思想
“针对每个操作都要注册一个与其对应的确认和补偿(撤销操作)”。
接口需要实现的三个操作
- Try阶段:主要是对业务系统做检测(链接是否正常)及资源预留(尝试预留资源[冻结库存、冻结金额等],若能预留成功则表示系统正常)。
- 该操作时,对于类似订单等数据库记录,将其状态修改为完成支付前的一个中间状态,而对于库存类操作,则总数扣减但处于冻结状态
- 该阶段的所有操作结果都是草稿态或中间态数据,并且当前操作的事务将被直接提交。
- Confirm阶段:确认执行业务操作,将中间状态的数据修改为最终状态。
- 该操作时,对于订单记录,此处才将其状态修改为完成支付状态,若为库存类,则将冻结数量变为实际扣减。
- Cancel阶段:取消执行业务操作。
- 该操作时,将前面冻结或者处于中间状态的数据进行回滚,变为可用状态
异常处理
部分服务挂掉:
TCC 事务框架都是要记录一些分布式事务的活动日志的,可以在磁盘上的日志文件里记录,也可以在数据库里记录。保存下来分布式事务运行的各个阶段和状态。当业务系统恢复正常后,TCC将自动重试。
业务代码不成功:
- 对于try阶段的不成功,将自动执行各个服务的 Cancel 逻辑,把之前的 Try 逻辑都回滚。
- 对于Cancel 或者 Confirm 逻辑执行一直失败,会不停的重试调用它的 Cancel 或者 Confirm 逻辑,务必要它成功!
使用小心得
事务三段式
在例如购票的场景中,可以使用业务三段式,将最核心也最容易失败的部分(锁票/锁座位)提前处理-preAction,将其他的业务操作(落库存、消费积分)作为核心,这样在很大程度上较少失败概率,当后面流程业务中若存在失败,则再进行回滚/业务补偿或者人工介入(核心步骤时效性要求不高)–最终一致性。
关于区别比较(引用记录)
TCC事务的处理流程与2PC两阶段提交类似,不过2PC通常都是在跨库的DB层面(数据库回滚),而TCC本质上就是一个应用层面的2PC,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。
而不足之处则在于TCC对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口还必须实现幂等。