简介

分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。在分布式系统中,可能涉及到多个参与者,每个参与者负责管理自己的本地数据。分布式事务的目标是确保在整个分布式环境中,所有相关的操作要么全部成功,要么全部失败,以保持数据的一致性。(All or nothing)

基本理论

CAP理论

CAP是 ConsistencyAvailabilityPartition tolerance三个词语的缩写,分别表示一致性、可用性、分区容忍性。

  • 一致性(Consistency) :更新操作成功并返回客户端完成后,所有节点同一时间的数据完全一致,不能存在中间状态。(这里指强一致性
  • 可用性(Availability) : 系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限时间返回结果
  • **分区容错性(Partition tolerance) **:分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。

任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证最终一致性。

CAP理论权衡
由于C、A、P无法同时满足,必须有所舍弃。但如果放弃P(分区容错性),也就放弃了分布式。因此目前常见的分布式系统基于CAP分类主要有:AP和CP两类。CAP理论该怎么理解?为什么是三选二?为什么是CP或者AP?面试题有哪些? - 知乎 (zhihu.com)

Base理论

CAP是分布式系统设计理论,BASE是CAP理论中AP方案的延伸,对于C我们采用的方式和策略就是保证最终一致性;

BASEBasically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写。BASE基于CAP定理演化而来,核心思想是即时无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

  • BA(基本可用):分布式系统在出现不可预知的故障的时候,允许损失部分可用性,但不等于系统不可用。体现在(1)响应时间延长 (2)服务降级
  • S(软状态):允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性。允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。、
  • E(最终一致性):系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。

BASE理论是提出通过牺牲一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。

分布式事务分类

分布式事务实现方案从类型上去分刚性事务、刚性事务

刚性事务

定义:无业务改造,强一致性,原生支持回滚/隔离性,低并发,适合短事务。
原则:遵循CAP理论的CP原则。保证强一致性
实现方式:XA 协议(2PC、JTA、JTS)、3PC,但由于同步阻塞,处理效率低,不适合大型网站分布式场景。

柔性事务

定义:有业务改造,最终一致性,实现补偿接口,实现资源锁定接口,高并发,适合长事务。
原则:遵循Base理论(AP原则),允许中间状态,保证最终一致性。
实现方式:TCC/FMT、Saga(状态机模式、Aop模式)、本地事务消息、消息事务(半消息)

刚性事务解决方案

X/Open DTP模型

X/Open DTP(Distributed Transaction Process) 是一个分布式事务模型。这个模型主要使用了两段提交(2PC - Two-Phase-Commit)来保证分布式事务的完整性。

三大组件:
AP:Application,即应用程序。属于业务层,规定了事务所涉及的操作。
TM:Transaction Manager,事务管理器。是事务调度模型的核心部分,负责协调和管理事务,提供AP事务接口、管理RM事务提交。
RM:Resource Manager,资源管理器。如数据库,消息队列,文件系统等。

事务执行流程:

XA协议

在 X/Open DTP分布式事务模型中,TM 和 多个 RM 的事务控制,都是基于 XA 协议来完成的。XA 协议也是 X/Open 提出的分布式事务处理规范(基于2PC),也是分布式事务处理的工业标准,它定义了一组标准的函数接口,称为 xa 函数,用于管理和控制分布式事务的执行。

目前,主流数据库都实现了 XA 接口,如 MySQL、Oracle、DB2、PGSQL等,它们都可以作为RM。

常见的xa函数:
xa_open/xa_close:建立和关闭与资源管理器的连接。
xa_start:启动一个xa事务。示例(mysql):xa start xid ,xid必须全局唯一
xa_end:结束一个xa事务。示例(mysql):xa end xid
xa_perpare:准备。示例(mysql):xa perpare xid
xa_commit:提交xa事务。示例(mysql):xa commit xid
xa_rollback:回滚xa事务。示例(mysql):xa rollback xid

XA各个阶段的处理流程

2PC

2PC即Two-Phase Commit,二阶段提交。(标准的XA规范)

一阶段(Prepare):TM向各RM发送事务内容,RM执行事务(写入undo/redo日志),但不提交。并向TM发送响应(成功或失败)。
二阶段(Commit):TM根据各RM返回的结果,决定是否最终执行事务提价或回滚,并广播给RM执行Commit/Rollback。然后各RM向TM返回响应结果(成功或失败)。注意这里RM如超时未收到TM的请求,则默认执行回滚操作。

优点:

  • 实现简单

缺点:

  • 同步阻塞,所有参与的RM都是事务阻塞,并且是锁定资源的,意味着不能进行其他操作;同时各个参与者还必须等待其他参与者完成响应。
  • 单点故障:由于TM是单点的,如果出现故障,所有RM都无法完成事务操作(第二阶段不能提交或回滚)
  • 数据不一致:在第二阶段TM发送Commit的时候,如果发送网络波动或TM崩溃,导致部分RM没有接收到请求,那么整个系统会出现数据不一致问题。
  • 缺少快速失败机制:在第一阶段,如果参与者出现故障,TM只能等待超时机制来得知。(3PC进行改进)

3PC

针对2PC的缺点,提出了3PC。在2PC的基础上添加了CanCommit阶段。

一阶段(CanCommit):TM询问RM是否可以提交事务,RM此时只检查是否因为部分因素导致事务无法成功提交,并加入了超时机制。RM返回响应(如果某个RM不能正常提交,后面就不执行了,相当于预检机制)
二阶段(PreCommit):当所有RM返回YES时,TM向各RM发送事务内容,RM执行事务(写入undo/redo日志),但不提交。
三阶段(DoCommit):TM 根据 RM 在 PreCommit 阶段的响应,决定是否执行最终提交操作。如果所有 RM 都响应 PreCommit 成功,TM 发送 DoCommit 请求给所有 RM 执行 提交操作。否则,如果有任何一个 RM 响应 PreCommit 失败或超时,TM 发送 DoAbort 请求给所有 RM 执行回滚操作。注意这里RM如超时未收到TM的请求,则默认执行提交操作。

从上述过程看出,3PC主要添加了预检机制以及修改了超时时的默认行为。改善了单点问题和回滚时的性能。但依旧存在数据不一致、阻塞等问题。

柔性事务解决方案

在电商领域等互联网场景下,刚性事务在数据库性能和处理能力上都暴露出了瓶颈。
柔性事务有两个特性:基本可用和柔性状态。

  • 基本可用是指分布式系统出现故障的时候允许损失一部分的可用性。
  • 柔性状态是指允许系统存在中间状态,这个中间状态不会影响系统整体的可用性,比如数据库读写分离的主从同步延迟等。柔性事务的一致性指的是最终一致性

柔性事务主要分为补偿性通知型

  • 补偿型事务:TCC、SAGA。是同步的
  • 通知型事务:MQ事务消息、最大努力通知型。是异步的

异步确保型

指将一系列同步的事务操作修改为基于消息队列异步执行的操作,来避免分布式事务中同步阻塞带来的数据操作性能的下降。

MQ事务消息方案

基于MQ的事务消息方案主要依靠MQ的半消息机制来实现投递消息和参与者自身本地事务的一致性保障。半消息机制实现原理其实借鉴的2PC的思路,是二阶段提交的广义拓展。

半消息机制
通常包括两个步骤:发送半消息和确认半消息。

  1. 发送半消息(Send Half Message):
    • 发送半消息是指在事务开始时,将消息发送到消息队列,但消息处于未确认的状态。
    • 在这个阶段,消息队列会持有这个消息,但不会将其投递给订阅者或消费者。
  2. 确认半消息(Confirm Half Message):
    • 确认半消息是指在事务执行成功时,发送确认请求给消息队列,让消息队列将消息标记为可投递状态。
    • 如果事务执行失败,发送取消请求给消息队列,让消息队列将消息标记为不可投递状态。

流程:

  1. 事务发起方首先发送半消息到MQ;
  2. MQ通知发送方消息发送成功;
  3. 在发送半消息成功后执行本地事务;
  4. 根据本地事务执行结果返回commit或者是rollback;
  5. 如果消息是rollback, MQ将丢弃该消息不投递;如果是commit,MQ将会消息发送给消息订阅方;
  6. 订阅方根据消息执行本地事务;
  7. 订阅方执行本地事务成功后再从MQ中将该消息标记为已消费;
  8. 如果执行本地事务过程中,执行端挂掉,或者超时,MQ服务器端将不停的询问producer来获取事务状态;
  9. Consumer端的消费成功机制有MQ保证;

案例:用户下单后,增加用户账户积分。在这个业务中涉及到两个具有先后顺序关系的操作。只有订单服务成功创建订单后,才可以增加积分(保证原子性)

  1. 订单服务创建订单
  2. 订单服务向MQ发送半消息
  3. 积分服务增加积分

如果积分服务的事务失败,则需要回滚订单服务或重试。
如果涉及到更多的服务,比如创建订单,然后扣除库存,最后增加用户积分。则涉及到三类服务,订单服务、库存服务、积分服务。库存服务订阅订单服务,积分服务订阅库存服务。

  1. 订单服务创建订单
  2. 订单服务向MQ发送半消息
  3. 订单服务本地提交事务,并向MQ确认。
  4. 库存服务收到MQ消息,向MQ发送半消息,并执行扣减库存事务。
  5. 库存服务本地提交事务,并向MQ确认。
  6. 积分服务收到MQ消息,执行增加积分事务。
    如果某一服务执行失败,可能需要全部回滚或重试。

RocketMQ实现异步确保型事务

有一些第三方的MQ是支持事务消息的,这些消息队列,支持半消息机制,比如RocketMQ,ActiveMQ。但是有一些常用的MQ也不支持事务消息,比如 RabbitMQ 和 Kafka 都不支持。

以阿里的 RocketMQ 中间件为例,其思路大致为:

  1. producer(本例中指A系统)发送半消息到broker,这个半消息不是说消息内容不完整, 它包含完整的消息内容, 在producer端和普通消息的发送逻辑一致
  2. broker存储半消息,半消息存储逻辑与普通消息一致,只是属性有所不同,topic是固定的RMQ_SYS_TRANS_HALF_TOPIC,queueId也是固定为0,这个tiopic中的消息对消费者是不可见的,所以里面的消息永远不会被消费。这就保证了在半消息提交成功之前,消费者是消费不到这个半消息的
  3. broker端半消息存储成功并返回后,A系统执行本地事务,并根据本地事务的执行结果来决定半消息的提交状态为提交或者回滚
  4. A系统发送结束半消息的请求,并带上提交状态(提交 or 回滚)
  5. broker端收到请求后,首先从RMQ_SYS_TRANS_HALF_TOPIC的queue中查出该消息,设置为完成状态。如果消息状态为提交,则把半消息从RMQ_SYS_TRANS_HALF_TOPIC队列中复制到这个消息原始topic的queue中去(之后这条消息就能被正常消费了);如果消息状态为回滚,则什么也不做。
  6. producer发送的半消息结束请求是 oneway 的,也就是发送后就不管了,只靠这个是无法保证半消息一定被提交的,rocketMq提供了一个兜底方案,这个方案叫消息反查机制,Broker启动时,会启动一个TransactionalMessageCheckService 任务,该任务会定时从半消息队列中读出所有超时未完成的半消息,针对每条未完成的消息,Broker会给对应的Producer发送一个消息反查请求,根据反查结果来决定这个半消息是需要提交还是回滚,或者后面继续来反查
  7. consumer(本例中指B系统)消费消息,执行本地数据变更(至于B是否能消费成功,消费失败是否重试,这属于正常消息消费需要考虑的问题)


本地消息表方案

有时候我们目前的MQ组件并不支持事务消息,或者我们想尽量少的侵入业务方。这时我们需要另外一种方案“基于DB本地消息表“。

本地消息表最初由eBay 提出来解决分布式事务的问题。是目前业界使用的比较多的方案之一,它的核心思想就是将分布式事务拆分成本地事务进行处理。


消息发送方:

  • 需要有一个消息表,记录着消息状态相关信息。
  • 业务数据和消息表在同一个数据库,要保证它俩在同一个本地事务。直接利用本地事务,将业务数据和事务消息直接写入数据库。
  • 在本地事务中处理完业务数据和写消息表操作后,通过写消息到 MQ 消息队列。使用专门的投递工作线程进行事务消息投递到MQ,根据投递ACK去删除事务消息表记录
  • 消息会发到消息消费方,如果发送失败,即进行重试。

消息消费方:

  • 处理消息队列中的消息,完成自己的业务逻辑。
  • 如果本地事务处理成功,则表明已经处理成功了,修改发送方消息表消息状态。
  • 如果本地事务处理失败,那么就会重试执行。
  • 如果是业务层面的失败,给消息生产方发送一个业务补偿消息,通知进行回滚等操作。

生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。

优缺点:
优点:

  • 本地消息表建设成本比较低,实现了可靠消息的传递确保了分布式事务的最终一致性。
    无需提供回查方法,进一步减少的业务的侵入。
  • 在某些场景下,还可以进一步利用注解等形式进行解耦,有可能实现无业务代码侵入式的实现。
    缺点:
  • 本地消息表与业务耦合在一起,难于做成通用性,不可独立伸缩。
  • 本地消息表是基于数据库来做的,而数据库是要读写磁盘IO的,因此在高并发下是有性能瓶颈的

MQ事务消息 VS 本地消息表

相同点:

  • 事务消息都依赖于消息队列MQ,因此都是异步的(回调机制实现)。
  • 都存在重复投递的可能,需要去重机制或幂等设计。
  • 都需要实现业务补偿逻辑

不同点:

  • MQ事务消息需要MQ的半消息特性支持,而本地消息表使用数据库记录消息状态
  • MQ事务具有比较大的业务侵入性,需要业务方进行改造,提供对应的本地操作成功的回查功能
  • MQ事务效率高,但具有业务耦合性。本地事务表的消息表也具有耦合性。

最大努力通知型

最大努力通知方案的目标,就是发起通知方通过一定的机制,最大努力将业务处理结果通知到接收方。一般使用衰减重试机制达到事务的最终一致性。

最大努力通知事务主要用于外部系统,因为外部的网络环境更加复杂和不可信,所以只能尽最大努力去通知实现数据最终一致性,比如充值平台与运营商、支付对接、商户通知等等跨平台、跨企业的系统间业务交互场景

异步确保型事务主要适用于内部系统的数据最终一致性保障,因为内部相对比较可控,比如订单和购物车、收货与清算、支付与结算等等场景。

MQ事务消息方案

要实现最大努力通知,可以采用 MQ 的 ACK 机制。

最大努力通知事务在投递之前,跟异步确保型流程都差不多,关键在于投递后的处理。因为异步确保型在于内部的事务处理,所以MQ和系统是直连并且无需严格的权限、安全等方面的思路设计。最大努力通知事务在于第三方系统的对接,所以最大努力通知事务有几个特性:

  • 业务主动方在完成业务处理后,向业务被动方(第三方系统)发送通知消息,允许存在消息丢失。
  • 业务主动方提供递增多挡位时间间隔(5min、10min、30min、1h、24h),用于失败重试调用业务被动方的接口;在通知N次之后就不再通知,报警+记日志+人工介入。
  • 业务被动方提供幂等的服务接口,防止通知重复消费。
  • 业务主动方需要有定期校验机制,对业务数据进行兜底;防止业务被动方无法履行责任时进行业务回滚,确保数据最终一致性。
  • 消息校对机制:在重复通知仍然没有通知到对方,可由接收通知方主动查询信息。

  1. 业务活动的主动方,在完成业务处理之后,向业务活动的被动方发送消息,允许消息丢失。
  2. 主动方可以设置时间阶梯型通知规则,在通知失败后按规则重复通知,直到通知N次后不再通知。
  3. 主动方提供校对查询接口给被动方按需校对查询,用于恢复丢失的业务消息。
  4. 业务活动的被动方如果正常接收了数据,就正常返回响应,并结束事务。
  5. 如果被动方没有正常接收,根据定时策略,向业务活动主动方查询,恢复丢失的业务消息。

场景:充值业务

  1. 账户系统调用充值系统接口
  2. 充值系统完成支付处理向账户系统发起充值结果通知
    若通知失败,则充值系统按策略进行重复通知
  3. 账户系统接收到充值结果通知修改充值状态
  4. 账户系统未接收到通知会主动调用充值系统的接口查询充值结果

特点

  1. 用到的服务模式:可查询操作、幂等操作;
  2. 被动方的处理结果不影响主动方的处理结果;
  3. 适用于对业务最终一致性的时间敏感度低的系统;
  4. 适合跨企业的系统间的操作,或者企业内部比较独立的系统间的操作,比如银行通知、商户通知等;

本地消息表方案

要实现最大努力通知,可以采用定期检查本地消息表的机制 。

发送消息方:

  • 需要有一个消息表,记录着消息状态相关信息。
  • 业务数据和消息表在同一个数据库,要保证它俩在同一个本地事务。直接利用本地事务,将业务数据和事务消息直接写入数据库。
  • 在本地事务中处理完业务数据和写消息表操作后,通过写消息到 MQ 消息队列。使用专门的投递工作线程进行事务消息投递到MQ,根据投递ACK去删除事务消息表记录
  • 消息会发到消息消费方,如果发送失败,即进行重试。
  • 生产方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。

最大努力通知事务 VS 异步确保型事务

最大努力通知事务其实是基于异步确保型事务发展而来适用于外部对接的一种业务实现。他们主要有的是业务差别,如下:
• 从参与者来说:最大努力通知事务适用于跨平台、跨企业的系统间业务交互;异步确保型事务更适用于同网络体系的内部服务交付。
• 从消息层面说:最大努力通知事务需要主动推送并提供多档次时间的重试机制来保证数据的通知;而异步确保型事务只需要消息消费者主动去消费。
• 从数据层面说:最大努力通知事务还需额外的定期校验机制对数据进行兜底,保证数据的最终一致性;而异步确保型事务只需保证消息的可靠投递即可,自身无需对数据进行兜底处理。

隔离性问题

考虑电商系统中经典的“超售问题”。
商品下单一般会涉及两个操作,首先读商品库存,然后扣除库存。

1
2
3
4
5
begin transaction
read(stock)
if stock > 0:
write(stock-1)
commit

为了确保不出现”超售“,上述事务必须要保障隔离等级为“可重复读”及以上,否则会出现用户A读取库存的时候,用户B下单成功,扣除了库存。这时用户A如果再次读取库存,会发现两次读取的数据不一致(不可重复读),那么用户A在后续扣除库存的时候,可能就会出现”超售“问题(如果用户B买走了最后一件商品的话,此时库存应该为0)。

由于异步确保型最大努力通知型都是异步的,有一定的延迟性,无法提供实时的隔离性保证。

补偿型

补偿模式使用一个额外的协调服务来协调各个需要保证一致性的业务服务,协调服务按顺序调用各个业务微服务,如果某个业务服务调用异常(包括业务异常和技术异常)就取消之前所有已经调用成功的业务服务。

TCC事务

TCC 是另一种常见的分布式事务机制,它是“Try-Confirm-Cancel”三个单词的缩写,是由数据库专家 Pat Helland 在 2007 年撰写的论文《Life beyond Distributed Transactions: An Apostate’s Opinion (opens new window)》中提出。

TCC 分布式事务模型包括三部分:

  1. 主业务服务:主业务服务为整个业务活动的发起方,服务的编排者,负责发起并完成整个业务活动。

  2. 从业务服务:从业务服务是整个业务活动的参与方,负责提供 TCC 业务操作,实现初步操作(Try)、确认操作(Confirm)、取消操作(Cancel)三个接口,供主业务服务调用。

  3. 业务活动管理器:业务活动管理器管理控制整个业务活动,包括记录维护 TCC 全局事务的事务状态和每个从业务服务的子事务状态,并在业务活动提交时调用所有从业务服务的 Confirm 操作,在业务活动取消时调用所有从业务服务的 Cancel 操作。

  • Try:尝试执行阶段,完成所有业务可执行性的检查(保障一致性),并且预留好全部需用到的业务资源(保障隔离性)。
  • Confirm:确认执行阶段,真正执行的业务逻辑,不进行任何业务检查,直接使用 Try 阶段准备的资源来完成业务处理。Try成功,Confirm一定成功。Confirm 阶段可能会重复执行,因此本阶段所执行的操作需要具备幂等性
  • Cancel:取消执行阶段,释放 Try 阶段预留的业务资源。Cancel 阶段可能会重复执行,也需要满足幂等性。

Confirm 或 Cancel 阶段: 两者是互斥的,只能进入其中一个,并且都满足幂等性,允许失败重试。

TCC事务模型的要求

  • 可查询操作:服务操作具有全局唯一的标识,操作唯一的确定的时间。
  • 幂等操作:重复调用多次产生的业务结果与调用一次产生的结果相同。一是通过业务操作实现幂等性,二是系统缓存所有请求与处理的结果,最后是检测到重复请求之后,自动返回之前的处理结果。
  • TCC操作:Try阶段,尝试执行业务,完成所有业务的检查,实现一致性;预留必须的业务资源,实现准隔离性。Confirm阶段:真正的去执行业务,不做任何检查,仅适用Try阶段预留的业务资源,Confirm操作还要满足幂等性。Cancel阶段:取消执行业务,释放Try阶段预留的业务资源,Cancel操作要满足幂等性。TCC与2PC(两阶段提交)协议的区别:TCC位于业务服务层而不是资源层,TCC没有单独准备阶段,Try操作兼备资源操作与准备的能力,TCC中Try操作可以灵活的选择业务资源,锁定粒度。TCC的开发成本比2PC高。实际上TCC也属于两阶段操作,但是TCC不等同于2PC操作。
  • 可补偿操作:Do阶段:真正的执行业务处理,业务处理结果外部可见。Compensate阶段:抵消或者部分撤销正向业务操作的业务结果,补偿操作满足幂等性。约束:补偿操作在业务上可行,由于业务执行结果未隔离或者补偿不完整带来的风险与成本可控。实际上,TCC的Confirm和Cancel操作可以看做是补偿操作。

TCC事务模型 VS DTP事务模型

相似点:

  • TCC事务的主业务服务相当于DTP模型中的AP;TCC事务的从业务服务相当于 DTP模型中的RM
    • 在DTP模型中,应用AP操作多个资源管理器RM上的资源;而在TCC模型中,是主业务服务操作多个从业务服务上的资源。例如航班预定案例中,美团App就是主业务服务,而川航和东航就是从业务服务,主业务服务需要使用从业务服务上的机票资源。不同的是DTP模型中的资源提供者是类似于Mysql这种关系型数据库,而TCC模型中资源的提供者是其他业务服务。
  • TCC中从业务服务TryConfirmCancel与DTP模型中RM提供的PrepareCommitRollback接口类似。
  • 事务管理器
    • 在DTP模型中,阶段1的(prepare)和阶段2的(commit、rollback),都是由TM进行调用的。
    • TCC中阶段1的try接口是主业务服务调用(绿色箭头),阶段2的(confirm、cancel接口)是事务管理器TM调用(红色箭头)。这就是 TCC 分布式事务模型的二阶段异步化功能,从业务服务的第一阶段执行成功,主业务服务就可以提交完成,然后再由事务管理器框架异步的执行各从业务服务的第二阶段。这里牺牲了一定的隔离性和一致性的,但是提高了长事务的可用性。

TCC事务模型 VS 2PC

阶段1:

  • 在XA中,各个RM准备提交各自的事务分支,事实上就是准备提交资源的更新操作(insert、delete、update等);执行事务(写入undo/redo log),但不提交。
  • 在TCC中,是主业务活动请求(try)各个从业务服务预留资源。

阶段2:

  • XA根据第一阶段每个RM是否都prepare成功,判断是要提交还是回滚。如果都prepare成功,那么就commit每个事务分支,反之则rollback每个事务分支。
  • TCC中,如果在第一阶段所有业务资源都预留成功,那么confirm各个从业务服务,否则取消(cancel)所有从业务服务的资源预留请求。

TCC和2PC不同点

  • XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。基于数据库锁实现,需要数据库支持XA协议,由于在执行事务的全程都需要对相关数据加锁,一般高并发性能会比较差
  • TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁,性能较好。但是对微服务的侵入性强,微服务的每个事务都必须实现try、confirm、cancel等3个方法,开发成本高,今后维护改造的成本也高为了达到事务的一致性要求,try、confirm、cancel接口必须实现幂等性操作由于事务管理器要记录事务日志,必定会损耗一定的性能,并使得整个TCC事务时间拉长
  • 2PC的撤销是通过回滚事务实现,而TCC的撤销是基于补偿性事务,而不是简单的回滚。补偿是一个独立的支持ACID特性的本地事务,用于在逻辑上取消服务提供者上一个ACID事务造成的影响,对于一个长事务(long-running transaction),与其实现一个巨大的分布式ACID事务,不如使用基于补偿性的方案,把每一次服务调用当做一个较短的本地ACID事务来处理,执行完就立即提交

TCC的最终一致性要求弱化了对资源的锁定条件,进而提高了分布式下的并发性能

使用场景案例

TCC是可以解决部分场景下的分布式事务的,但是,它的一个问题在于,需要每个参与者都分别实现Try,Confirm和Cancel接口及逻辑,这对于业务的侵入性是巨大的。

TCC 方案严重依赖回滚和补偿代码,最终的结果是:回滚代码逻辑复杂,业务代码很难维护。所以,TCC 方案的使用场景较少,但是也有使用的场景。

比如说跟钱打交道的,支付、交易相关的场景,大家会用 TCC方案,严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性,保证在资金上不会出现问题。

以下是一个网上书店购买书本的案例:

  1. 用户向 Fenix’s Bookstore 发送交易请求:购买一本价值 100 元的《深入理解 Java 虚拟机》。
  2. 创建事务,生成事务 ID,记录在活动日志中,进入 Try 阶段:
    • 用户服务:检查业务可行性,可行的话,将该用户的 100 元设置为“冻结”状态,通知下一步进入 Confirm 阶段;不可行的话,通知下一步进入 Cancel 阶段。
    • 仓库服务:检查业务可行性,可行的话,将该仓库的 1 本《深入理解 Java 虚拟机》设置为“冻结”状态,通知下一步进入 Confirm 阶段;不可行的话,通知下一步进入 Cancel 阶段。
    • 商家服务:检查业务可行性,不需要冻结资源。
  3. 如果第 2 步所有业务均反馈业务可行,将活动日志中的状态记录为 Confirm,进入 Confirm 阶段:
    • 用户服务:完成业务操作(扣减那被冻结的 100 元)。
    • 仓库服务:完成业务操作(标记那 1 本冻结的书为出库状态,扣减相应库存)。
    • 商家服务:完成业务操作(收款 100 元)。
  4. 第 3 步如果全部完成,事务宣告正常结束,如果第 3 步中任何一方出现异常,不论是业务异常或者网络异常,都将根据活动日志中的记录,重复执行该服务的 Confirm 操作,即进行最大努力交付。
  5. 如果第 2 步有任意一方反馈业务不可行,或任意一方超时,将活动日志的状态记录为 Cancel,进入 Cancel 阶段:
    • 用户服务:取消业务操作(释放被冻结的 100 元)。
    • 仓库服务:取消业务操作(释放被冻结的 1 本书)。
    • 商家服务:取消业务操作(大哭一场后安慰商家谋生不易)。
  6. 第 5 步如果全部完成,事务宣告以失败回滚结束,如果第 5 步中任何一方出现异常,不论是业务异常或者网络异常,都将根据活动日志中的记录,重复执行该服务的 Cancel 操作,即进行最大努力交付。
    如果上述重复执行失败,则可以进行告警,人工介入来处理。

SAGA事务

SAGA可以看做一个异步的、利用队列实现的补偿事务。

Saga模型是把一个分布式事务拆分为多个本地事务,每个本地事务都有相应的执行模块和补偿模块(对应TCC中的Confirm和Cancel),当Saga事务中任意一个本地事务出错时,可以通过调用相关的补偿方法恢复之前的事务,达到事务最终一致性。

这样的SAGA事务模型,是牺牲了一定的隔离性和一致性的,但是提高了long-running事务的可用性。

Saga事务组成
  • LLT(Long Live Transaction):由一个个本地事务组成的事务链。
  • 本地事务:事务链由一个个子事务(原子事务)组成,$LLT = T1+T2+T3+…+Ti$。
  • 补偿:每个本地事务 $Ti$有对应的补偿 $Ci$。

要求

  • $Ti$与 $Ci$都具备幂等性。
  • $Ti$与 $Ci$满足交换律(Commutative),即先执行 $Ti$还是先执行 $Ci$,其效果都是一样的。
  • $Ci$必须能成功提交,即不考虑 $Ci$本身提交失败被回滚的情形,如出现就必须持续重试直至成功,或者要人工介入。

如果 $T1$到 $Tn$均成功提交,那事务顺利完成,否则,要采取以下两种恢复策略之一:

  • 向后恢复(Backward Recovery):撤销掉之前所有成功子事务。如果任意本地子事务失败,则补偿已完成的事务。如异常情况的执行顺序$T1,T2,T3,…Ti,Ci,…C3,C2,C1$。

  • 向前恢复(Forward Recovery):即重试失败的事务(最大努力交付),适用于必须要成功的场景,该情况下不需要执行补偿Ci。执行顺序:$T1,T2,…,Tj(失败),Tj(重试),…,Ti$。

显然,向前恢复没有必要提供补偿事务,如果你的业务中,子事务(最终)总会成功,或补偿事务难以定义或不可能,向前恢复更符合你的需求。

使用限制

  • 不是所有的事务都能被补偿。补偿事务从语义角度撤消了事务$Ti$的行为,但未必能将数据库返回到执行$Ti$时的状态。
  • Saga不提供ACID保证,因为原子性和隔离性不能得到满足。
    • 原子性(Atomicity):正常情况下保证,但不能完全保证。
    • 一致性(Consistency):在某个时间点,会出现A库和B库的数据违反一致性要求的情况,但是最终是一致的。
    • 隔离性(Isolation):在某个时间点,A事务能够读到B事务部分提交的结果。
    • 持久性(Durability):和本地事务一样,只要commit则数据被持久。

SAGA事务解决方案

考虑业务逻辑如下:

  1. 向DB中插入一条数据
  2. 向MQ中发送一条消息
方案一:半消息模式

半消息的完整事务逻辑如下:

  1. 向MQ发送半消息。
  2. 向DB插入数据。
  3. 向MQ发送确认消息。

    这样,即使第二步执行失败,那么MQ中的半消息也无法被消费者消费(相当于执行了补偿)

为了解决确认消息丢失的问题,MQ引入了一个反查的机制。即MQ会每隔一段时间,对所有的半消息进行扫描,并就扫描到的存在时间过长的半消息,向发送者进行询问,询问如果得到确认回复,则将消息改为确认状态,如得到失败回复,则将消息删除。

方案二:本地消息表

在DB中,新增一个消息表,用于存放消息。如下:

  1. 在DB业务表中插入数据。
  2. 在DB消息表中插入数据。
  3. 异步将消息表中的消息发送到MQ,收到ack后,删除消息表中的消息。

如上,通过上述逻辑,将一个分布式的事务,拆分成两大步。第1和第2,构成了一个本地的事务,从而解决了分布式事务的问题。
这种解决方案,不需要业务端提供消息查询接口,只需要稍微修改业务逻辑,侵入性是最小的。

SAGA的案例

SAGA适用于无需马上返回业务发起方最终状态的场景,例如:你的请求已提交,请稍后查询或留意通知 之类。

将上述补偿事务的场景用SAGA改写,其流程如下:

  1. 订单服务创建最终状态未知的订单记录,并提交事务T1
  2. 现金服务扣除所需的金额,并提交事务T2
  3. 订单服务更新订单状态为成功,并提交事务T3

以上为成功的流程,若现金服务扣除金额失败,那么,最后一步订单服务将会更新订单状态为失败(补偿T1)。

其业务编码工作量比补偿事务多一点,包括以下内容:

  • 订单服务创建初始订单的逻辑
  • 订单服务确认订单成功的逻辑
  • 订单服务确认订单失败的逻辑
  • 现金服务扣除现金的逻辑
  • 现金服务补偿返回现金的逻辑

但其相对于补偿事务形态有性能上的优势,所有的本地子事务执行过程中,都无需等待其调用的子事务执行,减少了加锁的时间,这在事务流程较多较长的业务中性能优势更为明显。同时,其利用队列进行进行通讯,具有削峰填谷的作用。

因此该形式适用于不需要同步返回发起方执行最终结果、可以进行补偿、对性能要求较高、不介意额外编码的业务场景。

但当然SAGA也可以进行稍微改造,变成与TCC类似、可以进行资源预留的形态。

TCC vs SAGA

相同点:

  • 都是补偿性事务

不同点:

  • TCC可以保障隔离性,SAGA无法保障
  • SAGA业务侵入小,TCC需要全局改造
  • SAGA事件驱动模式,参与者可异步执行,高吞吐;

总体方案对比

属性 2PC TCC Saga 异步确保型事务 尽最大努力通知
事务一致性
复杂性
业务侵入性
使用局限性
性能
维护成本

Seata框架

快速启动 | Apache Seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

领域模型

  • TC :事务协调者。负责我们的事务ID的生成,事务决议,注册、提交、回滚等。
  • TM:事务管理者。定义事务的边界,负责告知 TC,分布式事务的开始,提交,回滚。
  • RM:资源管理者。管理每个分支事务的资源,每一个 RM 都会作为一个分支事务注册在 TC。

Seata事务的执行流程

  1. 事务管理者(TM)通过RPC至事务协调者(TC)注册全局事务(Global Transaction)
  2. 将TC生成的XID传递至其TM所调用的任意资源管理者(RM)中
  3. RM通过其接收到的XID,将其所管理的资源且被该调用锁使用到的资源注册为一个事务分支(Branch Transaction)
  4. 当该请求的调用链全部结束时TM将事务的决议结果(Commit/Rollback)通知TC
  5. TC将协调所有RM进行事务的二阶段动作(回滚/提交)

AT模式

AT 模式是 Seata 创新的一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层,我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等。

AT模式是改进版的2PC模式,或XA模型。它不会一直锁定资源。

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

使用前提

  • 基于支持本地 ACID 事务的关系型数据库。
  • Java 应用,通过 JDBC 访问数据库。

AT模型图

使用案例

有个充值业务,现在有两个服务,一个负责管理用户的余额,另外一个负责管理用户的积分。

当用户充值的时候,首先增加用户账户上的余额,然后增加用户的积分。

Seata AT分为两阶段,主要逻辑全部在第一阶段,第二阶段主要做回滚或日志清理的工作。

第一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

  1. 余额服务中的TM,向TC申请开启一个全局事务,TC会返回一个全局的事务ID。
  2. 余额服务在执行本地业务之前,RM会先向TC注册分支事务。
  3. 余额服务依次生成undo log、执行本地事务、生成redo log,最后直接提交本地事务。
  4. 余额服务的RM向TC汇报,事务状态是成功的。
  5. 余额服务发起远程调用,把事务ID传给积分服务。
  6. 积分服务在执行本地业务之前,也会先向TC注册分支事务。
  7. 积分服务次生成undo log、执行本地事务、生成redo log,最后直接提交本地事务。
  8. 积分服务的RM向TC汇报,事务状态是成功的。
  9. 积分服务返回远程调用成功给余额服务。
  10. 余额服务的TM向TC申请全局事务的提交/回滚。

第二阶段:异步提交或回滚

  • 提交:TC通知多个RM异步清理掉本地的redo和undo log
  • 回滚:TC通知每个RM回滚数据

读写隔离性

写隔离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
分支事务1-开始
|
V 获取 本地锁
|
V 获取 全局锁 分支事务2-开始
| |
V 释放 本地锁 V 获取 本地锁
| |
V 释放 全局锁 V 获取 全局锁
|
V 释放 本地锁
|
V 释放 全局锁

如上所示,一个分布式事务的锁获取流程是这样的
1)先获取到本地锁,这样你已经可以修改本地数据了,只是还不能本地事务提交
2)而后,能否提交就是看能否获得全局锁(拿到全局锁才可以提交
3)获得了全局锁,意味着可以修改了,那么可以提交本地事务,然后释放本地锁
4)当分布式事务全局提交,释放全局锁。这样就可以让其它事务获取全局锁,并提交它们对本地数据的修改了。

写隔离原则

  • 一阶段本地事务提交前,需要确保先拿到 全局锁 。
  • 拿不到 全局锁 ,不能提交本地事务。
  • 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

以一个示例来说明:

两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。

tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。(在此之前tx2一直在等待本地锁)

tx2 后开始,开启本地事务,拿到本地锁(tx2可以拿到被tx1修改后的最新数据m=900),更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。


tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。

考虑如果tx1的第二阶段需要全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。(但此时本地锁被tx2持有,tx2在等待全局锁,tx1在等待本地锁,如果不做处理,就形成了循环等待死锁的局面)。

此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。

读隔离
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。(因为其他事务可能会全局回滚)

如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。

出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。

TCC模式

简介

TCC 与 Seata AT 事务一样都是两阶段事务,它与 AT 事务的主要区别为:

  • TCC 对业务代码侵入严重
    每个阶段的数据操作都要自己进行编码来实现,事务框架无法自动处理。
  • TCC 性能更高
    不必对数据加全局锁,允许多个事务同时操作数据。

Seata TCC 整体是 两阶段提交 的模型。一个分布式的全局事务,全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:

  • 一阶段 prepare 行为
  • 二阶段 commit 或 rollback 行为

根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 TCC (Branch) Transaction Mode.

AT 模式基于 支持本地 ACID 事务 的 关系型数据库

  • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
  • 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
  • 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。

相应的,TCC 模式,不依赖于底层数据资源的事务支持:

  • 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
  • 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
  • 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。

所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。

TCC 的 Try 操作作为一阶段,负责资源的检查和预留;Confirm 操作作为二阶段提交操作,执行真正的业务;Cancel 是二阶段回滚操作,执行预留资源的取消,使资源回到初始状态。

案例

第一阶段 Try
以账户服务为例,当下订单时要扣减用户账户金额:

假如用户购买 100 元商品,要扣减 100 元。
TCC 事务首先对这100元的扣减金额进行预留,或者说是先冻结这100元(如果账户没有100元,该分支事务就直接失败了)

第二阶段 Confirm
如果第一阶段能够顺利完成,那么说明“扣减金额”业务(分支事务)最终肯定是可以成功的。当全局事务提交时, TC会控制当前分支事务进行提交,如果提交失败,TC 会反复尝试,直到提交成功为止。
当全局事务提交时,就可以使用冻结的金额来最终实现业务数据操作:

第二阶段 Cancel
如果全局事务回滚,就把冻结的金额进行解冻,恢复到以前的状态,TC 会控制当前分支事务回滚,如果回滚失败,TC 会反复尝试,直到回滚完成为止。

多个事务并发的情况
多个TCC全局事务允许并发,它们执行扣减金额时,只需要冻结各自的金额即可:

SAGA模式

简介

Saga 模式是 SEATA 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

适用场景:

  • 业务流程长、业务流程多
  • 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口

优势:

  • 一阶段提交本地事务,无锁,高性能
  • 事件驱动架构,参与者可异步执行,高吞吐
  • 补偿服务易于实现

缺点:

  • 不保证隔离性

原理

目前 SEATA 提供的 Saga 模式是基于状态机引擎来实现的,机制是:

  1. 通过状态图来定义服务调用的流程并生成 json 状态语言定义文件
  2. 状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点
  3. 状态图 json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚

    注意: 异常发生时是否进行补偿也可由用户自定义决定

  4. 可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能

XA模式

使用前提

  • 支持XA 事务的数据库。
  • Java 应用,通过 JDBC 访问数据库。

在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。

工作机制

整体运行机制
XA 模式 运行在 Seata 定义的事务框架内:

  • 执行阶段
    • XA start + XA end + XA prepare + SQL + 注册分支
  • 完成阶段
    • XA commit/XA rollback