在讲述分布式事务的概念之前,我们先来回顾下事务相关的一些概念。
就是一个程序执行单元,里面的操作要么全部执行成功,要么全部执行失败,不允许只成功一半另外一半执行失败的事情发生。例如一段事务代码做了两次数据库更新操作,那么这两次数据库操作要么全部执行成功,要么全部回滚。
我们知道事务有 4 个非常重要的特性,即我们常说的(ACID)。
其实分布式事务从实质上看与数据库事务的概念是一致的,既然是事务也就需要满足事务的基本特性(ACID),只是分布式事务相对于本地事务而言其表现形式有很大的不同。
本地事务的时代,如果需要同时操作数据库的多条记录,而这些操作可以放到一个事务中,那么我们可以通过数据库提供的事务机制就可以实现。
而随着微服务架构的推进,原本一个本地逻辑执行单元,被拆分到了多个独立的微服务中,这些微服务又分别操作了不同的数据库和表。
比如下个指派个体的运输任务,下运输任务的同时要生成需求、计划、任务,还要去调用询价服务,投保服务,那么,一旦产生任何一个服务异常,都会产生事务性的问题。虽然对于我们现有逻辑来说,可以由运营作废,但未来自动化之后呢?
分布式事务是为了解决微服务架构(形式都是分布式系统)中不同节点之间的数据一致性问题。这个一致性问题本质上解决的也是传统事务需要解决的问题,即一个请求在多个微服务调用链中,所有服务的数据处理要么全部成功,要么全部回滚。当然分布式事务问题的形式可能与传统事务会有比较大的差异,但是问题本质是一致的,都是要求解决数据的一致性问题。
而分布式事务的实现方式有很多种,最具有代表性的是由 Oracle Tuxedo 系统提出的 XA 分布式事务协议。XA 协议包括两阶段提交(2PC)和三阶段提交(3PC)两种实现,接下来我们分别来介绍下这两种实现方式的原理。
两阶段提交又称 2PC(two-phase commit protocol),2PC 是一个非常经典的强一致、中心化的原子提交协议。这里所说的中心化是指协议中有两个角色:一个是分布式事务协调者(coordinator)和 N 个参与者(participant)。
两阶段提交,顾名思义就是要进行两个阶段的提交:第一阶段,准备阶段(投票阶段);第二阶段,提交阶段(执行阶段)。
1)如果所有参与者均反馈的是成功,协调者就会向所有参与者发送 “全局提交确认通知(global_commit)”,参与者 Participant 就会完成自身本地数据库事务的提交,并将提交结果回复 “ack” 消息给协调者 Coordinator,然后协调者 Coordinator 就会向调用方返回分布式事务处理完成的结果。如果有任何一个参与者返回失败,则回滚事务。
2)如果参与者向协调者反馈 “Vote_Abort” 消息,即返回了失败的消息。此时分布式事务协调者 Coordinator 就会向所有的参与者 Participant 发起事务回滚的消息(“global_rollback”),此时各个参与者就会回滚本地事务,释放资源,并且向协调者发送 “ack” 确认消息,协调者就会向调用方返回分布式事务处理失败的结果。
以上就是两阶段提交的基本过程了,那么按照这个两阶段提交协议,分布式系统的数据一致性问题就能解决么?
其实,2PC 只是通过增加了事务协调者(Coordinator)的角色来通过 2 个阶段的处理流程来解决分布式系统中一个事务需要跨多个服务的数据一致性问题。
以下几点是 XA-两阶段提交协议中会遇到的一些问题:
三阶段提交又称 3PC,在 2PC 的基础上增加了 CanCommit 阶段,并引入了超时机制。一旦事务参与者迟迟没有收到协调者的 Commit 请求,就会自动进行本地 commit,这样相对有效地解决了协调者单点故障的问题。
相比较 2PC 而言,3PC 对于协调者(Coordinator)和参与者(Participant)都设置了超时时间,解决了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地 commit 从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
另外,通过 CanCommit、PreCommit、DoCommit 三个阶段的设计,相较于 2PC 而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。
3PC 的缺点:
3PC 在去除阻塞的同时也引入了新的问题,那就是参与者接收到 precommit 消息后,如果出现网络分区,此时协调者所在的节点和参与者无法进行正常的网络通信,在这种情况下,该参与者依然会进行事务的提交,这必然出现数据的不一致性。
TCC 与 2PC、3PC 一样,只是分布式事务的一种实现方案。
TCC(Try-Confirm-Cancel)又称补偿事务。其核心思想是:” 针对每个操作都要注册一个与其对应的确认和补偿(撤销操作)”。它分为三个操作:
TCC 事务的处理流程与 2PC 两阶段提交类似,不过 2PC 通常都是在跨库的 DB 层面,而 TCC 本质上就是一个应用层面的 2PC,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。
而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现 try、confirm、cancel 三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm 和 cancel 接口还必须实现幂等。
TCC 的具体原理图如下:
接入 TCC 前,业务操作只需要一步就能完成,但是在接入 TCC 之后,需要考虑如何将其分成 2 阶段完成,把资源的检查和预留放在一阶段的 Try 操作中进行,把真正的业务操作的执行放在二阶段的 Confirm 操作中进行;
TCC 服务要保证第一阶段 Try 操作成功之后,二阶段 Confirm 操作一定能成功;
事务协调器在调用 TCC 服务的一阶段 Try 操作时,可能会出现因为丢包而导致的网络超时,此时事务协调器会触发二阶段回滚,调用 TCC 服务的 Cancel 操作;
TCC 服务在未收到 Try 请求的情况下收到 Cancel 请求,这种场景被称为空回滚;TCC 服务在实现时应当允许空回滚的执行;
事务协调器在调用 TCC 服务的一阶段 Try 操作时,可能会出现因网络拥堵而导致的超时,此时事务协调器会触发二阶段回滚,调用 TCC 服务的 Cancel 操作;在此之后,拥堵在网络上的一阶段 Try 数据包被 TCC 服务收到,出现了二阶段 Cancel 请求比一阶段 Try 请求先执行的情况;
用户在实现 TCC 服务时,应当允许空回滚,但是要拒绝执行空回滚之后到来的一阶段 Try 请求;
无论是网络数据包重传,还是异常事务的补偿执行,都会导致 TCC 服务的 Try、Confirm 或者 Cancel 操作被重复执行;用户在实现 TCC 服务时,需要考虑幂等控制,即 Try、Confirm、Cancel 执行次和执行多次的业务结果是一样的;
TCC 服务的一阶段 Try 操作会做资源的预留,在二阶段操作执行之前,如果其他事务需要读取被预留的资源数据,那么处于中间状态的业务数据该如何向用户展示,需要业务在实现时考虑清楚;通常的设计原则是 “宁可不展示、少展示,也不多展示、错展示”;
TCC 服务的一阶段 Try 操作预留资源之后,在二阶段操作执行之前,预留的资源都不会被释放;如果此时其他分布式事务修改这些业务资源,会出现分布式事务的并发问题;
用户在实现 TCC 服务时,需要考虑业务数据的并发控制,尽量将逻辑锁粒度降到最低,以最大限度的提高分布式事务的并发性;
Hmily (How much I love you)
高性能分布式事务 tcc 开源框架。基于 java 语言来开发(JDK1.8),支持 dubbo,springcloud,motan 等 rpc 框架进行分布式事务。
框架特性
原理图:
流程图:
作者:京东物流 宋乐
来源:京东云开发者社区 自猿其说 Tech 转载请注明来源