简介
我们在过去总是使用本地事务,也就是数据库提供的事务操作,其中具有ACID的特性,但在如今我们的各个模块儿被拆分,服务与服务间相互调用,简单来说就是需要跨进程的事务,我们来想一下现有的本地事务是否能解决分布式事务。
情况1:用户直接调用订单模块儿,开启事务,然后在订单表中存入数据,然后再远程调用物流模块儿,去操作物流模块儿,我们可以想到如果物流模块儿出现问题,订单模块儿远程调用发生错误,是会进行事务回滚的。应该是没问题的。那如果物流模块儿确实修改成功了,但网络传输出现了问题,订单模块儿就进行回滚了,则就导致了物流模块儿有数据,订单模块儿没数据的问题。我们再考虑下面的问题:
情况二:
情况三:
前提理论
CAP理论
CAP表示一致性,可用性,分区容忍性。下面我们用数据库读写分离来演示
整体执行流程如下:
1、商品服务向主数据库写入商品信息(添加商品、修改商品、删除商品)
2、主数据库向商品服务响应写入成功。
3、商品服务请求从数据库读取商品信息。
一致性
表示写操作如果成功,各个节点上的读操作,应能读到最新的数据。
那么应如何保证?
- 在写入主数据库后应立刻将数据同步到从数据库中
- 在同步期间应对从数据库加锁,以防止读取到过期的数据
那这就存在一定的问题:比如加锁后的性能损耗。
可用性
指的是任何事务操作都能得到响应结果,不会出现响应超时,响应错误的问题。也就是说我允许我读到之前的数据,但不允许接收不到数据
如何保证?
- 数据还是应从主数据库同步到从数据库
- 不能将资源锁定
- 可以返回旧数据,甚至是默认数据,但不能返回错误数据或响应超时
分区容忍性
在微服务中数据分布到各个节点中,在网络分区中,允许因为网络问题导致的节点通信失败,但该节点应能继续对外提供服务,举例:1. 主数据库向从数据库同步失败,不能影响节点的读写操作 2. 其中一个节点挂了不能影响另一个。
如何操作
- 尽量使用异步操作同步数据,让节点松耦合
- 多添加从节点,保证备份节点
分区容忍性应该是分布式下最重要的特性。
在CAP理论中,是否三个特性能同时满足?
不能,在分区容忍性必备的情况下,一致性与可用性间存在矛盾,所以应看情况保证CP或者AP的特性。
AP特性:放弃一致性,比如订单退款,并不是瞬间让钱到账,而是允许二十四小时内到账。
CP特性:保证一致性,数据必须同步到最新状态,比如跨行转账,必须双方都完后事务才算完成。
一般情况下,使用的是AP特性,保证服务的可用。
BASE理论
BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。BASE理论是对CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障,允许部分功能不可用,但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终要达到一致状态
。满足BASE理论的事务,我们称之为“柔性事务”。
- 基本可用:分布式系统中允许损失部分可用功能,来保证核心功能的可用
- 软状态:因为不要求强一致性,所以允许存在一个中间状态,比如订单的支付中,数据同步中等状态,最后改为成功状态。
- 最终一致性:就是从中间状态最终一定达到数据一致。
解决方案
分布式事务解决方案---2PC(两阶段提交)
2PC也称为两阶段提交,顾名思义是将事务分为两个阶段,准备阶段(Prepare),提交阶段(Commit)。举个栗子:
张三和李四好久不见,老友约起聚餐,饭店老板要求先买单,才能出票。这时张三和李四分别抱怨近况不如 意,囊中羞涩,都不愿意请客,这时只能AA。只有张三和李四都付款,老板才能出票安排就餐。但由于张三和李四 都是铁公鸡,形成了尴尬的一幕:
- 准备阶段:老板要求张三付款,张三付款。老板要求李四付款,李四付款。
- 提交阶段:老板出票,两人拿票纷纷落座就餐。
例子中形成了一个事务,若张三或李四其中一人拒绝付款,或钱不够,店老板都不会给出票,并且会把已收款退回。
整个事务过程由事务管理器和参与者组成,店老板就是事务管理器,张三、李四就是事务参与者,事务管理器负责决策整个分布式事务的提交和回滚,事务参与者负责自己本地事务的提交和回滚。
而在一些关系型数据库中(Oracle,Mysql)都是支持两阶段提交协议
- 准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。(Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件),这时候资源是被锁定的。
- 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。
成功情况:
失败情况:
为了统一标准减少行业内不必要的对接成本,需要制定标准化的处理模型及接口标准,国际开放标准组织Open Group定义了分布式事务处理模型DTP(Distributed Transaction Processing Reference Model)。
XA方案
DTP模型定义如下角色: AP(Application Program):即应用程序,可以理解为使用DTP分布式事务的程序。 RM(Resource Manager):即资源管理器,可以理解为事务的参与者,一般情况下是指一个数据库实例,通过资源管理器对该数据库进行控制,资源管理器控制着分支事务。 TM(Transaction Manager):事务管理器,负责协调和管理事务,事务管理器控制着全局事务,管理事务生命周期,并协调各个RM。全局事务是指分布式事务处理环境中,需要操作多个数据库共同完成一个工作,这个工作即是一个全局事务。 DTP模型定义TM和RM之间通讯的接口规范叫XA,简单理解为数据库提供的2PC接口协议,基于数据库的XA 协议来实现2PC又称为XA方案。 以上三个角色之间的交互方式如下:
- TM向AP提供 应用程序编程接口,AP通过TM提交及回滚事务。
- TM交易中间件通过XA接口来通知RM数据库事务的开始、结束以及提交、回滚等。
总结: 整个2PC的事务流程涉及到三个角色AP、RM、TM。AP指的是使用2PC分布式事务的应用程序;RM指的是资源管理器,它控制着分支事务;TM指的是事务管理器,它控制着整个全局事务。
XA方案的问题:
- 需要数据库支持XA协议
- 资源锁需要等两个阶段结束才释放,性能差
Seata
阿里开源的分布式事务框架,传统2PC的问题在Seata中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。主要优点是性能较好,且不长时间占用连接资源,它以高效并且对业务0侵入的方式解决微服务场景下面临的分布式事务问题,它目前提供AT(2PC)、TCC、SAGA 和 XA 事务模式。第一阶段就将本地事务提交了(提交前需要获取全局事务锁),全局事务管理器统计所有分支事务的结果,如果某个分支出现了异常,在本地事务中有一个UNDO_LOG Table
的数据表,记录了之前的数据,修改SQL,修改后的数据。可通过它进行事务的回滚,这里不会产生脏数据就是因为本地事务的提交需要先获取全局事务锁。 第二阶段,如果分支事务都成功,则根据事务ID删除undo_log
的记录,如果失败,则找到记录进行回滚。 这里业务说明官网已经写的很详细了,这里我不再赘述。 Seata的核心就是通过undo log文件让已经提交的事务,仍能回滚。
小结:
这里我们介绍了传统的2PC(基于数据库XA协议)和Seata实现2PC的方案。 Seata实现的要点:
- 全局事务开始使用 @GlobalTransactional标识 。
- 每个本地事务方案仍然使用@Transactional标识。
- 每个数据都需要创建undo_log表,此表是seata保证本地事务一致性的关键。
- TM获取到的XID会通过远程调用时传入。
分布式事务解决方案---TCC
TCC与2PC有啥区别? 为啥使用?
2PC是定义在数据层
的,而且有全局锁的存在,也会有一定的性能消耗。TCC是在业务层定义的,更加灵活,但复杂度也会上升,TCC是try,conform,console
三个方法的简写。也就是想实现分布式事务,需要实现这三个方法。下面我们使用框架进行演示
hmily
TCC需要注意三种异常处理分别是空回滚、幂等、悬挂:** **
这里是指TCC中的try,conform,console
是三个独立的线程去完成的,且分布式调用具体网络延迟的可能性。
- 空回滚:try还没执行时,执行console。
- 解决思路:我们记录TM生成的全局事务ID,来判断try是否已经执行,如果未执行则不执行console。
- 悬挂:conform或console已经执行了,才开始执行try
- 解决思路:在conform和console执行时将执行记录插入数据,当执行try时进行判断
- 幂等性:conform和console都是默认成功的,当执行失败时,会不断重试,这就需要保证代码代码幂等性
- 解决思路:执行前,先通过全局唯一事务ID,查看自己之前是否执行过。
这里有一个拓宽的思路,就是在本地事务中,记录一张记录表,来判断操作是否执行过,由于是在同一本地事务内,所以可以保证该记录的准确性。
Try,Console,Conform需要严格处理上面的三个问题,下面我们用业务来说明:场景为 A 转账 30 元给 B,A和B账户在不同的服务。
// 账户A try:检查余额是否够30元 扣减30元 (操作是直接提交本地事务的) confirm: 空 cancel:增加30元 // 账户B try:增加30元 confirm: 空 cancel:减少30元
存在问题:
1)如果账户A的try没有执行,执行cancel则就多加了30元。 2)由于try,cancel、confifirm都是由单独的线程去调用,且会出现重复调用,所以都需要实现幂等。 3)账号B在try中增加30元,当try执行完成后可能会其它线程给消费了。 4)如果账户B的try没有执行在cancel则就多减了30元。 问题解决:
1)账户A的cancel方法需要判断try方法是否执行,正常执行try后方可执行cancel。 2)try,cancel、confifirm方法实现幂等。 3)账号B在try方法中不允许更新账户金额,在confirm中更新账户金额。 4)账户B的cancel方法需要判断try方法是否执行,正常执行try后方可执行cancel。
// 账户A try: try幂等校验,判断之前执行过没 try悬挂处理,判断conform和cancel执行过没 检查余额是否够30元 扣减30元 confirm: 空 cancel: cancel幂等校验 cancel空回滚处理 增加可用余额30元 // 账户B try:空 confirm: confirm幂等校验 正式增加30元 cancel:空
分布式事务解决方案---可靠消息最终一致性
也算是Base理论的实现 可靠消息:张三给李四发钱,张三的账户先减少钱,然后发送一条消息到消息队列,李四进行接收消息增加钱,保证整个过程的可靠性。最终一致性:张三发钱后,是不能再回滚的,李四就必须获取并消费消息,保证的最终数据是一致的。
这中间有什么问题?
张三向消息队列发送消息就一定会出现网络问题,比如:事务内,张三钱减少了,然后发送数据到消息队列,如果发送错误张三的钱也可以回滚,但是可能消息已经到队列了,但返回时网络延迟发生错误,导致数据回滚,则队列中数据是仍然存在的,所以一定要保证数据操作与消息发送保证原子性
。 李四需要必须接收并完成消息,但可能出现问题导致不断的重试,所以有幂等性
的问题
RocketMq
这是大概的一个流程,我们现在分析可能存在哪些问题: 张三向消息队列成功发送事务消息后,有一个rockmq监听本地事务方法的返回。这时候事务消息并不能被消费,如果张三正常提交本地事务,则该事务消息可被消费,如果本地事务没有提交成功,返回Rollback则事务消息删除,如果返回UnKnown(比如出现某些异常),则过一段时间,消息队列会再次监听本地事务执行情况。从而保证张三本地事务与事务消息的原子性。 李四从消息队列获取消息后,应该进行本地事务并确认该事务消息,如果没有确认,消息队列会不断发给消费者,最终也可人工处理。 可靠消息最终一致性的优点就是如果李四那边要执行很久,张三这边是不用等的,只要最终确认该事务消息即可。进行了异步解耦。
分布式事务解决方案---最大努力通知
RocketMq
我们以支付业务为例 这里的账户系统就是等待通知的 充值系统当充值完毕后发起通知
这里我们看出最大努力通知的目标:发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。
具体包括:
- 有一定的消息重复通知机制
- 因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知
- 消息校验机制
- 如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询信息来满足需求。
最大努力通知与可靠消息一致性有什么不同?
- 解决思想不同
可靠消息一致性主要保证消息发送方保证把消息发送出去,并消息接收方成功获取并消费该消息,关键在发起方。 最大努力通知则侧重消息能被消息接受者收到,或者可以消息接收方主动来查询消息状态,关键在于接收方。
- 业务应用场景不同
可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易。 最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。
- 技术解决方向不同
可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。 最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消 息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)。
解决方案
消息接收方直接监听消息队列由消息通知系统监听消息队列,然后再通知消息接收方
这两者有啥不同?
第一种只适用于内部网络系统,也就是发起方和接收方都归你管理。但如果说消息发送方是第三方系统,人家就不能让你监听人家的消息队列吧,只能说加入一个中间通知系统,然后通过网络请求来通知你。
分布式事务综合案例分析
在上面我们已经学习了四种不同的分布式事务解决方案,我们需要进行区分,哪种业务场景需要哪种实现,各个方案有什么区别。
场景一
用户向用户中心发起注册请求,用户中心保存用户业务信息,然后通知统一账号服务新建该用户所对应登录账号。 针对注册业务,如果用户与账号信息不一致,则会导致严重问题,因此该业务对一致性要求较为严格,即当用户服务和账号服务任意一方出现问题都需要回滚事务。
业务分析
- 使用最大努力通知
- 不可以,用户服务注册好后,通知账户服务,这里只能说不断通知,没法做到回滚
- 使用可靠消息一致性
- 不可以,用户发送事务消息,然后进行本地事务,如果成功了,也无法保证账户服务成功消费,无法回滚
- TCC方案
- 可行,支持事务回滚,性能好,但实现复杂
- 2PC方案
场景二
用户向用户中心提交开户资料,用户中心生成开户请求号并重定向至银行存管系统开户页面。用户设置存管密码并确认开户后,银行存管立即返回“请求已受理”。在某一时刻,银行存管系统处理完该开户请求后,将调用回调地址通知处理结果,若通知失败,则按一定策略重试通知。同时,银行存管系统应提供开户结果查询的接口,供用户中心校对结果。
业务分析
- Seata方案
- 不可行,银行存管系统不能让你去编写业务代码吧
- TCC方案
- 不可行,银行存管系统不会让你写try,conform,console吧
- 可靠消息最终一致性
- 不可行,银行系统不会和用户系统直接通过MQ交互
- 最大努力通知
场景三
管理员对某标的满标审批通过,交易中心修改标的状态为“还款中”,同时要通知还款服务生成还款计划。
业务分析
- 使用Seata
- 不行,生成计划如果过久,Seata锁定资源
- 使用TCC
- 本需求对业务一致性要求较低,因为生成还款计划的时长较长,所以不要求交易中心修改标的状态为“还款中”就立 即生成还款计划 ,所以本方案不适用。
- 使用努力通知
- 满标审批通过后由交易中心向还款服务发送通知要求生成还款计划,还款服务并且对外提供还款计划生成结果校对接口供其它服务查询,最大努力 通知方案也适用本场景 。
- 使用消息一致性
- 满标审批通过后由交易中心修改标的状态为“还款中”并且向还款服务发送消息,还款服务接收到消息开始生成还款计划,基本于MQ的可靠消息一致性方案适用此场景 。
总结
分布式事务对比分析: 2PC 最大的诟病是一个阻塞协议。RM在执行分支事务后需要等待TM的决定,此时服务会阻塞并锁定资源。由于其 阻塞机制和最差时间复杂度高, 因此,这种设计不能适应随着事务涉及的服务数量增加而扩展的需要,很难用于并 发较高以及子事务生命周期较长 (long-running transactions) 的分布式服务中。
如果拿TCC事务的处理流程与2PC两阶段提交做比较,2PC通常都是在跨库的DB层面,而TCC则在应用层面的处 理,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使 得降低锁冲突、提高吞吐量成为可能。而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现 try、confirm、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实 现不同的回滚策略。典型的使用场景:满,登录送优惠券等。
可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消 息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。典型的使用场景:注 册送积分,登录送优惠券等。
最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务;允许发起通知方处理业 务失败,在接收通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都会不影响到接收通知方的后 续处理;发起通知方需提供查询执行情况接口,用于接收通知方校对结果。典型的使用场景:银行通知、支付结果 通知等。总结: 在条件允许的情况下,我们尽可能选择本地事务单数据源,因为它减少了网络交互带来的性能损耗,且避免了数据 弱一致性带来的种种问题。若某系统频繁且不合理的使用分布式事务,应首先从整体设计角度观察服务的拆分是否 合理,是否高内聚低耦合?是否粒度太小?
分布式事务一直是业界难题,因为网络的不确定性,而且我们习惯于拿 分布式事务与单机事务ACID做对比。无论是数据库层的XA、还是应用层TCC、可靠消息、最大努力通知等方案,都没有完美解决分布式事务问题,它们 不过是各自在性能、一致性、可用性等方面做取舍,寻求某些场景偏好下的权衡。