主要包括:
- 分布式基础
- 分布式事务概念介绍
- 2PC 和 3PC
- 分布式事务的具体实现方案
分布式一致性基础
数据库的强一致性事务 ACID 特性满足了本地单机事务的一致性,但是无法满足分布式的一致性。
因此就产生了 CAP理论 和 BASE 的概念。
先复习一下强一致性事务的 ACID 特性:
原子性(Atomicity)
一致性(Consistency)
隔离性(Isolation)
持久性(Durability)
CAP定理
对于设计分布式系统来说,CAP定理是一个入门理论,主要包括:
一致性(Consistency):在分布式系统中的所有数据备份,在同一时刻是否是同样的值。(等同于所有节点访问同一份最新的副本)
可用性(Availability):在集群中的一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据的更新具备高可用性)
分区容错性(Partition tolerance):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在一定的时间限制内达成数据一致性,就意味着发生了分区的状况。必须就当前状况在A和C之间做出选择。
以下是分别拥有CA、CP、AP的情况:
CA with P:如果不要求P(不允许分区),则C和A是可以保证的(强一致性并且具备可用性)。但其实分区问题是始终存在的,因此CA的系统更多的是允许分区后,各个子系统依然保持CA。
CP with A:如果不要求A(不要求完全高可用),相当于每个请求都要在Server之前强一致,而P(分区)会导致同步时间无限延长,如此CP也是可以保证的。很多传统数据库分布式事务都属于此种模式。
AP with C:要高可用并且分区,那就必须放弃强一致性。一旦分区发生,节点之间可能会失去联系,为了保持高可用,每个节点只能使用本地数据提供服务,而这样会导致全局上面数据的不一致性。现在众多的NoSQL都属于此类。
MySQL主从异步复制 是 AP系统。
MySQL主从半同步复制 是 CP系统。
Zookeeper 是 CP系统。
Redis主从同步 是 AP 系统。
Eureka主从同步 是 AP 系统。
BASE理论
BASE 是 对CAP理论的一个扩展,和ACID是相反的。
不同于ACID的强一致性模型,BASE通过牺牲强一致性,从而获得可用性,并允许数据在一段时间内不一致,但最终达到一致状态。
BASE主要包括:
Basically Avaliable(基本可用)
Soft state(软状态)
Eventually consistent(最终一致性)
1、BA 基本可用:分布式系统在出现故障时,允许损失部分可用功能,但是保证核心功能可用。
2、S 软状态:允许系统中存在中间状态,这个状态不影响系统的可用性,这里是指CAP中的不一致状态。
3、E 最终一致:是指经过一段时间之后,分布式系统中所有节点的数据最终会达到一致性。
对于大部分的分布式应用而言,只要数据在规定的时间内达到最终一致性即可。我们可以把符合传统的 ACID 叫做刚性事务,把满足 BASE 理论的最终一致性事务叫做柔性事务。
一味的追求强一致性,并非最佳方案。对于分布式应用来说,刚柔并济是更加合理的设计方案,即在本地服务中采用强一致事务,在跨系统调用中采用最终一致性。如何权衡系统的性能与一致性,是十分考验架构师与开发者的设计功力的。
2PC 与 3PC
两阶段提交(Two Parse Commit)
是一种使分布式系统中所有节点在进行事务提交时保持一致性的一种协议。
在一个跨多系统的分布式事务中,需要引入一个协调者的组件,来统一掌控全部节点并指示这些节点是否把操作结果进行真正的提交,想要在分布式系统中实现一致性的其他协议,都是在两节点提交的基础上做的改进。
1)投票阶段
- 协调者(事务管理器)向所有参与者询问是否可以执行提交操作 vote,并等待各个参与者的相应。
- 参与者执行所有事务操作,并写入 redo log 和 undo log。
- 参与者相应询问:
- 若实际执行成功,返回 “同意”
- 若实际执行失败,返回 “中止”
2)提交阶段
-
若所有参与者都返回 “同意”:
- 协调者(事务管理器)向所有参与者发出 “正式提交” 的请求。
- 参与者收到请求后,正式提交。
- 参与者返回 “完成”。
- 协调者(事务管理器)收到所有的 “完成” 消息后,完成事务。
-
若有任何一个参与者返回 “中止”,或询问超时(即有部分参与者没有响应):
- 协调者(事务管理器)向所有参与者发出 “回滚” 的请求。
- 参与者收到请求后,利用 undo log 进行回滚。
- 参与者返回 “回滚完成”。
- 协调者(事务管理器)收到所有的 “回滚完成” 消息后,取消事务。
缺点:
1、同步阻塞。2PC是一个阻塞协议,必须等到事务完全执行完成才能释放资源。
2、单点问题。协调者的角色很关键,如果协调者宕机,那么资源管理器会一直阻塞,无法使用。
3、数据不一致。虽然2PC是为分布式数据强一致性设计的,但是仍然存在数据不一致的可能,如果在commit阶段因为网络问题,参与者没收到Commit消息,那么其余参与者就会一直处于阻塞状态,这时候就产生了数据不一致。
三阶段提交(Three Parse Commit)
3PC引入了 超时机制 和 准备阶段
使得在参与者收不到确认时,依然可以从容的 commit 或者 rollback,避免资源锁定太久导致浪费。
但是3PC同样存在着很多问题,实现起来非常复杂,因为很难通过多次询问来解决系统分歧问题,尤其是在超时状态互不信任的分布式网络中,这也就是著名的拜占庭将军问题。
分布式事务的实现方案
目前实现方式主要有以下6种:
- XA(横跨多个数据库的一致性方案,不建议使用)
- 可靠消息最终一致性(使用MQ来实现事务)
- TCC (Try-Confirm-Cancel)
- 本地消息表(严重依赖消息表来管理事务)
- 最大努力通知
- Saga
XA方案
XA是 X/Open CAE Specification(Distributed Transaction Processing)模型,它定义的 TM(Transaction Manager)与(Resource Manager)之前新选哪个通信的接口。
Java中 的 javax.transaction.xa.XAResource
定义了 XA 接口,它依赖数据库厂商对 jdbc-driver 的具体实现。
目前XA有两种实现:
- 基于一阶段提交(1PC)的弱XA。
- 基于二阶段提交(2PC)的强XA。
弱XA
- 弱XA通过去掉XA阶段的 prepar 阶段,以达到减少资源锁定范围而提升并发性能的效果。典型实现是在一个业务线程中,遍历所有的数据库连接,依次做 commit 或者 rollback。
- 弱XA和本地事务相比,性能损耗低,但在事务提交的执行过程中,弱出现网络故障、数据库宕机等预期之外的异常,想回造成数据不一致,且无法回滚。
强XA
强XA也就是典型的二阶段提交,这里就不展开说了。
XA的应用场景
这种分布式事务的方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发场景。
一般来说,某个系统内部如果出现跨多个库 的操作,是 不合规 的。
现在微服务,打个大的系统分成几百、几十个服务。一般来说,我们的规定和规范,是要求 每个服务职能操作自己对应的一个数据库
如果你要操作其他服务的库,必须是通过 调用其他服务的接口 来实现,绝对不允许交叉访问数据库。
否则交叉访问,全体乱套,根本无法管理也无法治理,而且可能出现多个服务修改数据导致数据错误的情况。
TCC方案
TCC模型,是把锁的粒度交给业务处理,它需要每个子业务都实现 Try-Confirm / Cancel 接口。
TCC 模式本质上是应用层面的2PC
-
Try:
- 尝试执行业务
- 完成所有业务检查(一致性)
- 预留必须业务资源(准隔离性)
-
Confirm:
- 确认执行业务
- 真正执行业务,不做任何业务检查
- 只使用Try阶段预留的业务资源
- Confirm 操作满足幂等性
-
Cancel:
- 取消执行业务
- 释放Try 阶段预留的业务资源
- Cancel 操作满足幂等性
这三个阶段都会按照本地事务的方式执行,不同于XA的prepar,TCC无需将XA的投票期间所有资源挂起,因此极大提高了吞吐量。
TCC的缺点在于,需要编写回滚逻辑的代码,这个回滚逻辑可能会比较恶心。
本地消息表方案
其实是Ebay公司搞出来的一个思路,具体参考:本地消息表
大概思路是:
1、A系统 在自己本地一个事务里操作同时,插入一条数据到消息表。
2、接着 A系统 讲这个消息发送到MQ中去。
3、B系统 接收到消息之后,在一个事务里,往自己本地消息列表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会被回滚,这样 保证不会重复处理消息。
4、B系统 执行成功之后,就会更新自己的本地消息表,同时也会更新 A系统对应消息表的状态。
5、如果 B系统处理失败了,那么就不会更新消息表状态,此时 A系统会定时扫描自己的消息表,如果有未处理成功的消息,就会再次发送到MQ中去,让B再次处理。
6、这个方案保证了最终一致性,哪怕 B事务失败了,但是 A 会不断重发消息,直到 B 成功为止。
这就要求 B系统 的消息消息,需要做到幂等性。
该方案存在的最大问题在于,严重依赖于数据库的消息表来管理事务
,对于高并发 和 可扩展性 支持较差。
适用于对一致性哟求不高的,实现该模型方案时,一定注意,要求 消费消息端,一定要做到消息重试的幂等性。
可靠消息最终一致性方案
直接基于MQ来实现事务。比如阿里的 RocketMQ 就支持消息事务。比较建议使用该方案。
大概思路:
1、A系统 发送一个 prepared 消息到MQ,如果这个 prepared 消息发送失败,那么该事务直接快速失败,取消执行。
2、如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉MQ发送确认消息,如果失败就告诉MQ发送回滚消息。
3、如果发送了确认消息,那么此时 B系统会接收到确认消息,然后执行本地的事务。
4、MQ会 自动轮询 所有prepared 消息,回调 A系统 的接口。询问该消息本地处理的状态,是成功还是失败。所有没有发送确认的消息,是继续重试还是回滚?阴暗来说这里可以查询DB,看之前本地事务是否执行,如果回滚了,那么MQ会发送回滚消息给B系统。这里就是避免本地事务执行成功而消息发送失败
5、如果 B系统失败了,那就会不断重试,直到成功。
如果实在无法执行成功,有两种方案:
1)要么对重要业务进行回滚操作(B回滚之后,想办法通知A系统也会滚)
2)发送告警,由人工手动来进行回滚补偿。
尽最大努力提交方案
有两种不同的解决方案:
方案一
最大努力送达,是对于弱XA的一种补偿,采用事务表记录所有的事务操作SQL。
- 如果子事务提交成功,将会删除事务日志。
- 如果子事务失败,则会按照配置的重试次数,尝试重新提交,尽量保证数据的一致性。这里可以根据不同场景,在 C 和 A 之间做平衡,采用同步或者异步方式重试。
优点:无锁资源,性能损耗小。
缺点:尝试多次提交失败后,无法回滚。仅适用于事务最终一定能够成功的场景。
解决方案: Sharding-JDBC。
方案二
大概思路:
1、系统A 本地事务执行完之后,发送消息到MQ
2、设立一个服务消费MQ,该服务的任务就是 最大努力的通知,该服务会消费MQ然后写入DB中流下来,或者放入一个内存度咧,接着小勇系统B的接口。
3、如果系统B执行成功,那么事务结束。如果执行失败,那么就尽最大努力的定时尝试重新调用系统B,反复N次直到成功,如果调用次数超过设定的重复次数阈值,那么最终事务失败。
比较常见的场景:支付成功之后,多次回调。
Saga 方案
相比于本地的数据库事务来说,长事务(Long Lived Transaction)会对一些数据库资源持有相对较长的一段时间,这会严重地影响其他正常数据库事务的执行。
Saga的核心思想,就是将长事务,拆分为多个本地短事务,可以非常明显地降低事务被回滚的风险
当我们使用 Saga 模式开发分布式事务时,有两种协调不同服务的方式:
- 协同(Chroeography,去中心化)
- 编排(Orchestration,中心化)
优缺点:
其实就是 中心化 和 去中心化 的优缺点。
中心化:往往会造就一个【上帝服务】,其中包含了非常多的组织与集成其他节点的工作,存在单点故障问题。
去中心化:管理和调整不方便,追踪一个事务需要跨越多个服务,增加了维护成本。
下游约束:
使用Saga作为分布式事务方案时,需要对分布式事务的参与者有一定的约束,需要保证:
1)提供接口和补偿副作用的接口。
2)接口支持重入,并保证全局唯一的幂等性。(可以使用全局唯一的业务ID)
Saga协同
使用协同的方式,每一个本地的事务的完成,都会触发一个其他服务中的本地事务执行,也就是说,事务的执行过程是流式的。(类似于订单的状态单向流转)
当我们选择协同方式处理事务时,服务之间的通信其实就是通过事件进行,每一个本地事务最终弄都会向服务下游发送新事件。(这个事件可以是MQ,也可以是RPC请求,但是要求下游接口提供幂等和重入)
Saga 编排
编排的方式,引入了一个中心化的节点: 事务协调器
我们通过一个 Saga 对象来追踪所有的子任务调用情况,根据任务的调用情况,决定是否需要调用对应的补偿方案,并在网络请求出现超时时重试。
事务协调器会保存当前进行中的分布式事务状态,并根据情况对事务进行提交或者回滚。在服务编排过程中,我们是从协调者本身出发,去考虑整个事务的执行过程。相对于协同,编排的方式实现更为简单。
(如果有什么错误或者建议,欢迎留言指出)
(本文内容是对各个知识点的转载整理,用于个人技术沉淀,以及大家学习交流用)
参考资料:
芋道源码
CAP原理,强一致性弱一致性
分布式系统弱一致性模型
Paxos、Raft分布式一致性算法应用场景
掘金: 有人再问你分布式事务,把这篇扔给他
简书:数据库 分布式事务 2PC和3PC