分布式事务之seata

分布式事务基础

事务

概念

事务指的就是一个操作单元,在这个操作单元中的所有操作最终要保持一致的行为,要么所有操作都被撤销。

事务可以看做是一次大的操作,它由不同的小操作组成,这些操作要么全部成功,要么全部失败。

例如

转账:包含转出转入操作

网购:包含下单扣减库存支付操作

四个特性

事务特性

原子性(Atomicity):操作这些指令时,要么全部执行成功,要么全部不执行。只要其中一个指令执行失败,所有的指令都执行失败,数据进行回滚,回到执行指令前的数据状态。要么全部执行,要么全部不执行

一致性(Consistency):事务的执行使数据从一个状态转换为另一个状态,数据库的完整性约束没有被破坏。能量守恒,总量不变

eg: 拿转账来说,假设用户A和用户B两者的钱加起来一共是2000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是2000,这就是事务的一致性。

隔离性(Isolation):隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。信息彼此独立,互不干扰

即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。

持久性(Durability):当事务正确完成后,它对于数据的改变是永久性的。不会轻易丢失

eg: 例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,我们事务中提交的数据也必须是已经发生过的而且存储到了磁盘中。

本地事务实现

1
2
3
4
5
begin transaction; 
//1.本地数据库操作:张三减少金额
//2.本地数据库操作:李四增加金额
commit transation;

分布式事务

概念

随着互联网的快速发展,软件系统由原来的单体应用转变为分布式应用,下图描述了单体应用向微服务的演变:分布式系统会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操 作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如用户注册送积分 事务、创建订单减库存事务,银行转账事务等都是分布式事务。

img

典型的场景就是微服务架构 微服务之间通过远程调用完成事务操作。 比如:订单微服务和库存微服务,下单的 同时订单微服务请求库存微服务减库存。 简言之:跨JVM进程产生分布式事务

分布式事务实现

1
2
3
4
5
begin transaction;
//1.本地数据库操作:张三减少金额
//2.远程调用:让李四增加金额
commit transation;

在分布式事务中往往会出现以外的情况:

当远程调用让李四增加金额成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就回滚 了张三减少金额的操作,此时张三和李四的数据就不一致了。

因此在分布式架构的基础上,传统数据库事务就无法使用了,张三和李四的账户不在一个数据库中甚至不在一个应 用系统里,实现转账事务需要通过远程调用,由于网络问题(宕机、cpu卡顿、磁盘损坏等等各种问题)就会导致分布式事务问题。

所以

产生场景

夸JVM进程产生分布式事务

典型的场景就是微服务架构 微服务之间通过远程调用完成事务操作。 比如:订单微服务和库存微服务,下单的 同时订单微服务请求库存微服务减库存。

2.png

跨数据库实例

单体系统访问多个数据库实例 当单体系统需要访问多个数据库(实例)时就会产生分布式事务。 比如:用户信 息和订单信息分别在两个MySQL实例存储,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信 息,由于数据分布在不同的数据实例,需要通过不同的数据库链接去操作数据,此时产生分布式事务。

3.png

我们了解到了分布式事务的基础概念。与本地事务不同的是,分布式系统之所以叫分布式,是因 为提供服务的各个节点分布在不同机器上,相互之间通过网络交互。不能因为有一点网络问题就导致整个系统无法 提供服务,网络因素成为了分布式事务的考量标准之一。

所以我们在分布式情况下,考虑的是系统出现问题应该怎么解决,而不是规避这些问题。例如:我们要考虑的是出现网络卡顿、cpu卡顿、服务宕机后,怎么操作能让用户无感知、或者让够方便快捷的保证数据的一致性,而不是想办法怎么让网络不卡顿、cpu不卡顿、服务不宕机。

CAP

CAP原则又叫CAP定理,同时又被称作布鲁尔定理(Brewer’s theorem),指的是在一个分布式系统中,不可能同时满足以下三点

img

一致性(Consistency)

副本永远是最新数据

强一致性,在写操作完成后开始的任何读操作都必须返回刚刚写入的数据副本永远是最新数据

也就是说,在一致性系统中,一旦客户端将值写入任何一台服务器并获得响应,那么之后client从其他任何服务器读取的都是刚写入的数据

一致性保证了不管向哪台服务器写入数据,其他的服务器能实时同步数据

可用性

高可用

每次向未崩溃的节点发送请求,总能保证收到响应数据(允许不是最新数据)。

分区容忍性(Partition tolerance)

容忍网络分区

分区:在分布式系统中,不同的节点分布在不同的子网络中,由于一些特殊的原因,这些子节点之间出现了网络不通的状态,但他们的内部子网络是正常的。从而导致了整个系统的环境被切分成了若干个孤立的区域。这就是分区。

分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,也就是说,服务器A和B发送给对方的任何消息都是可以放弃的,也就是说A和B可能因为各种意外情况,导致无法成功进行同步,分布式系统要能容忍这种情况。除非整个网络环境都发生了故障。

容许节点 G1/G2 间传递消息的差错(延迟或丢失),而不影响系统继续运行。

以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。

组 合 分析结果
CA 满足原子和可用,放弃分区容错。说白了,就是一个整体的应用。
CP 满足原子和分区容错,也就是说,要放弃可用。当系统被分区,为了保证原子性,必须放弃可用性,让服务停用。
AP 满足可用性和分区容错,当出现分区,同时为了保证可用性,必须让节点继续对外服务,这样必然导致失去原子性。

对于一个分布式系统来说,CAP三者中,

一致性

一致性可以分为强一致性与弱一致性。所谓强一致性,即复制是同步的,弱一致性,即复制是异步的。

CAP回顾

CAP理论告诉我们一个悲惨但不得不接受的事实——我们只能在C、A、P中选择两个条件。而对于业务系统而言,我们往往选择牺牲一致性来换取系统的可用性和分区容错性。不过这里要指出的是,所谓的“牺牲一致性”并不是完全放弃数据一致性,而是牺牲强一致性换取弱一致性

强一致性

系统中的某个数据被成功更新后,后续任何对该数据的读取操作都将得到更新后的值;

  • 一个集群需要对外部提供强一致性,所以只要集群内部某一台服务器的数据发生了改变,那么就需要等待集群内其他服务器的数据同步完成后,才能正常的对外提供服务。
  • 保证了强一致性,务必会损耗可用性

弱一致性

系统中的某个数据被更新后,后续对该数据的读取操作可能得到更新后的值,也可能是更改前的值。

但即使过了不一致时间窗口这段时间后,后续对该数据的读取也不一定是最新值所以说,可以理解为数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。

最终一致性

是弱一致性的特殊形式,存储系统保证在没有新的更新的条件下,最终所有的访问都是最后更新的值。

不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。

简单说,就是在一段时间后,节点间的数据会最终达到一致状态。

分布式事务协议

背景

在分布式系统里,每个节点都可以知晓自己操作的成功或者失败,却无法知道其他节点操作的成功或失败。当一个事务跨多个节点时,为了保持事务的原子性与一致性,而引入一个协调者来统一掌控所有参与者的操作结果,并指示它们是否要把操作结果进行真正的提交或者回滚(rollback)。

二阶段提交

二阶段提交协议(Two-phase Commit,即 2PC)是常用的分布式事务解决方案,即将事务的提交过程分为两个阶段来进行处理。

阶段

  • 准备阶段
  • 提交阶段

参与角色

  • 协调者:事务的发起者
  • 参与者:事务的执行者

1. 第一阶段(voting phase)投票阶段:

  1. 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复
  2. 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)
  3. 如参与者执行成功,给协调者反馈同意,否则反馈中止

1596360577153

2. 第二阶段(commit phase)提交执行阶段:

当协调者节点从所有参与者节点获得的相应消息都为同意时:

  1. 协调者节点向所有参与者节点发出正式提交(commit)的请求。
  2. 参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送ack完成消息。
  4. 协调者节点收到所有参与者节点反馈的ack完成消息后,完成事务。

1596376239082

二阶段提交看起来确实能够提供原子性的操作,但是不幸的是,二阶段提交还是有几个缺点的:

  1. 性能问题:执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
  2. 可靠性问题:参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。
  3. 数据一致性问题:二阶段无法解决的问题:协调者在发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

优点

尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)

缺点

实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。

三阶段提交(3PC)

三阶段提交协议,是二阶段提交协议的改进版本,三阶段提交有两个改动点。

  • 在协调者和参与者中都引入超时机制。
  • 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit(询问提交)PreCommit(预提交)DoCommit(提交)三个阶段。处理流程如下:

img

1. 阶段一:CanCommit阶段

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

  1. 事务询问 协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。

  2. 响应反馈 参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。

2. PreCommit阶段

协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能。

  • 假如所有参与者均反馈 yes,协调者预执行事务。
  1. 发送预提交请求 :协调者向参与者发送PreCommit请求,并进入准备阶段
  2. 事务预提交 :参与者接收到PreCommit请求后,会执行事务操作,并将undoredo信息记录到事务日志中(但不提交事务)
  3. 响应反馈 :如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

img

  • 假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
  1. 发送中断请求 :协调者向所有参与者发送abort请求。
  2. 中断事务 :参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

img

3. doCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况。

注意:进入阶段 3 后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的 doCommit 请求或 abort 请求。此时,参与者都会在等待超时之后,继续执行事务提交。

执行提交
  1. 发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
  2. 事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
  3. 响应反馈 事务提交完之后,向协调者发送ack响应。
  4. 完成事务 协调者接收到所有参与者的ack响应之后,完成事务。

img

中断事务

任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务

  1. 发送中断请求 如果协调者处于工作状态,向所有参与者发出 abort 请求
  2. 事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
  3. 反馈结果 参与者完成事务回滚之后,向协调者反馈ACK消息
  4. 中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

img

注意

在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者abort请求时,会在等待超时之后,会继续进行事务的提交。(其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes。(一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。 )

优点:相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。

缺点:数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 doCommit 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。

分布式事务解决方案

事务补偿(TCC)

概念

TCC方案是一种应用层面侵入业务的两阶段提交。是目前最火的一种柔性事务方案,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作

  1. 第一阶段

    Try(尝试):主要是对业务系统做检测及资源预留 (加锁,锁住资源)

  2. 第二阶段

    本阶段根据第一阶段的结果,决定是执行confirm还是cancel

    Confirm(确认):执行真正的业务执行业务,释放锁

    Cancle(取消):是预留资源的取消出问题,释放锁

img

案例

为了方便理解,下面以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤,库存服务和订单服务分别在不同的服务器节点上。

假设商品库存为 100,购买数量为 2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。

①Try 阶段

TCC 机制中的 Try 仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:

  • 完成所有业务检查( 一致性 ) 。
  • 预留必须业务资源( 准隔离性 ) 。
  • Try 尝试执行业务。
img

②Confirm / Cancel 阶段

根据 Try 阶段服务是否全部正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)。

Confirm 和 Cancel 操作满足幂等性,如果 Confirm 或 Cancel 操作执行失败,将会不断重试直到执行完成。

Confirm:当 Try 阶段服务全部正常执行, 执行确认业务逻辑操作

img

Cancel 取消执行,释放 Try 阶段预留的业务资源,上面的例子中,Cancel 操作会把冻结的库存释放,并更新订单状态为取消。

最终一致性保证

  • TCC 事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。
  • Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。也就是说只要Try成功,Confirm一定成功
  • Confirm与Cancel如果失败,由TCC框架进行==重试==补偿
  • 存在极低概率在CC环节彻底失败,则需要定时任务或人工介入

方案总结

TCC 事务机制相对于传统事务机制(X/Open XA),TCC 事务机制相比于上面介绍的 XA 事务机制,有以下优点:

  • 性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
  • 数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
  • 可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。

缺点: TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。

本地消息表

简介

本地消息表的方案最初是由 eBay 提出,核心思路是将分布式事务拆分成本地事务进行处理。

方案通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。

这样设计可以避免”业务处理成功 + 事务消息发送失败”,或”业务处理失败 + 事务消息发送成功”的棘手情况出现,保证 2 个系统事务的数据一致性。

处理流程

下面把分布式事务最先开始处理的事务方称为事务主动方,在事务主动方之后处理的业务内的其他事务称为事务被动方。

为了方便理解,下面继续以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤。

库存服务和订单服务分别在不同的服务器节点上,其中库存服务是事务主动方,订单服务是事务被动方。

事务的主动方需要额外新建事务消息表,用于记录分布式事务的消息的发生、处理状态。

整个业务处理流程如下:

img

步骤1:事务主动方处理本地事务。

事务主动方在本地事务中处理业务更新操作和写消息表操作。上面例子中库存服务阶段在本地事务中完成扣减库存和写消息表(图中 1、2)。

步骤 2:事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息。

消息中间件可以基于 Kafka、RocketMQ 消息队列,事务主动方主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。

上面例子中,库存服务把事务待处理消息写到消息中间件,订单服务消费消息中间件的消息,完成新增订单(图中 3 - 5)。

步骤 3:事务被动方通过消息中间件,通知事务主动方事务已处理的消息。

上面例子中,订单服务把事务已处理消息写到消息中间件,库存服务消费中间件的消息,并将事务消息的状态更新为已完成(图中 6 - 8)。

为了数据的一致性,当处理错误需要重试,事务发送方和事务接收方相关业务处理需要支持幂等。

具体最终一致性的容错处理如下:

  • 当步骤 1 处理出错,事务回滚,相当于什么都没发生。
  • 当步骤 2、步骤 3 处理出错,由于未处理的事务消息还是保存在事务发送方,事务发送方可以定时轮询为超时消息数据,再次发送到消息中间件进行处理。事务被动方消费事务消息重试处理。
  • 如果是业务上的失败,事务被动方可以发消息给事务主动方进行回滚。
  • 如果多个事务被动方已经消费消息,事务主动方需要回滚事务时需要通知事务被动方回滚。

方案总结

优点:

  • 从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
  • 方案轻量,容易实现。

缺点:

  • 与具体的业务场景绑定,耦合性强,不可公用。
  • 消息数据与业务数据同库,占用业务系统资源。
  • 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。

MQ 事务方案

方案简介

基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。

处理流程

下面主要基于 RocketMQ 4.3 之后的版本介绍 MQ 的分布式事务方案。

在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ,相对于提供了 2PC 的提交接口,方案如下:

正常情况:事务主动方发消息

img

这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下:

  • 图中 1:发送方向 MQ 服务端(MQ Server)发送 half 消息。

  • 图中 2:MQ Server 将消息持久化成功之后,向发送方 ack 确认消息已经发送成功。

  • 图中 3:发送方开始执行本地事务逻辑。

  • 图中 4:发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。

  • 图中 5:MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。

异常情况:事务主动方消息恢复

img

在断网或者应用重启等异常情况下,图中 4 提交的二次确认超时未到达 MQ Server,此时处理逻辑如下:

  • 图中 5:MQ Server 对该消息发起消息回查。
  • 图中 6:发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
  • 图中 7:发送方根据检查得到的本地事务的最终状态再次提交二次确认。
  • 图中 8:MQ Server基于 commit/rollback 对消息进行投递或者删除。

介绍完 RocketMQ 的事务消息方案后,由于前面已经介绍过本地消息表方案,这里就简单介绍 RocketMQ 分布式事务:

img

事务主动方基于 MQ 通信通知事务被动方处理事务,事务被动方基于 MQ 返回处理结果。

如果事务被动方消费消息异常,需要不断重试,业务处理逻辑需要保证幂等。

如果是事务被动方业务上的处理失败,可以通过 MQ 通知事务主动方进行补偿或者事务回滚。

方案总结

相比本地消息表方案,MQ 事务方案优点是:

  • 消息数据独立存储 ,降低业务系统与消息系统之间的耦合。

  • 吞吐量由于使用本地消息表方案。

缺点是:

  • 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。
  • 业务处理服务需要实现消息状态回查接口。

Saga 事务:最终一致性

方案简介

Saga 事务源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的如何处理 long lived transaction(长活事务)论文。

Saga 事务核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。

处理流程

Saga 事务基本协议如下

  • 每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。
  • 每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。

可以看到,和 TCC 相比,Saga 没有“预留”动作,它的 Ti 就是直接提交到库。

下面以下单流程为例,整个操作包括:创建订单、扣减库存、支付、增加积分。

img

Saga 的执行顺序有两种,如上图:

  • 事务正常执行完成:T1, T2, T3, …, Tn,例如:扣减库存(T1),创建订单(T2),支付(T3),依次有序完成整个事务。
  • 事务回滚:T1, T2, …, Tj, Cj,…, C2, C1,其中 0 < j < n,例如:扣减库存(T1),创建订单(T2),支付(T3,支付失败),支付回滚(C3),订单回滚(C2),恢复库存(C1)。

Saga 定义了两种恢复策略:

img

向前恢复(forward recovery):对应于上面第一种执行顺序,适用于必须要成功的场景,发生失败进行重试,执行顺序是类似于这样的:T1, T2, …, Tj(失败), Tj(重试),…, Tn,其中j是发生错误的子事务(sub-transaction)。该情况下不需要Ci。

向后恢复(backward recovery):对应于上面提到的第二种执行顺序,其中 j 是发生错误的子事务(sub-transaction),这种做法的效果是撤销掉之前所有成功的子事务,使得整个 Saga 的执行结果撤销。

Saga 事务常见的有两种不同的实现方式:

①命令协调(Order Orchestrator):中央协调器负责集中处理事件的决策和业务逻辑排序。

中央协调器(Orchestrator,简称 OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。

以电商订单的例子为例:

  • 事务发起方的主业务逻辑请求 OSO 服务开启订单事务
  • OSO 向库存服务请求扣减库存,库存服务回复处理结果。
  • OSO 向订单服务请求创建订单,订单服务回复创建结果。
  • OSO 向支付服务请求支付,支付服务回复处理结果。
  • 主业务逻辑接收并处理 OSO 事务处理结果回复。

中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。

基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。

②事件编排(Event Choreography0):没有中央协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动。

在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。

当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。

img

以电商订单的例子为例:

  • 事务发起方的主业务逻辑发布开始订单事件。
  • 库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件。
  • 订单服务监听库存已扣减事件,创建订单,并发布订单已创建事件。
  • 支付服务监听订单已创建事件,进行支付,并发布订单已支付事件。
  • 主业务逻辑监听订单已支付事件并处理。

事件/编排是实现 Saga 模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。如果事务涉及 2 至 4 个步骤,则可能是非常合适的。

方案总结

命令协调设计的优点如下:

  • 服务之间关系简单,避免服务之间的循环依赖关系,因为 Saga 协调器会调用 Saga 参与者,但参与者不会调用协调器。
  • 程序开发简单,只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。
  • 易维护扩展,在添加新步骤时,事务复杂性保持线性,回滚更容易管理,更容易实施和测试。

命令协调设计缺点如下:

  • 中央协调器容易处理逻辑容易过于复杂,导致难以维护。
  • 存在协调器单点故障风险。

事件/编排设计优点如下:

  • 避免中央协调器单点故障风险。
  • 当涉及的步骤较少服务开发简单,容易实现。

事件/编排设计缺点如下:

  • 服务之间存在循环依赖的风险。
  • 当涉及的步骤较多,服务间关系混乱,难以追踪调测。

值得补充的是,由于 Saga 模型中没有 Prepare 阶段,因此事务间不能保证隔离性。

当多个 Saga 事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发,例如:在应用层面加锁,或者应用层面预先冻结资源。

总结

img

介绍完分布式事务相关理论和常见解决方案后,最终的目的在实际项目中运用,因此,总结一下各个方案的常见的使用场景:

  • 2PC/3PC:依赖于数据库,能够很好的提供强一致性和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。
  • TCC:适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。
  • 本地消息表/MQ 事务:都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。
  • Saga 事务:由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。 Saga 相比缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga 事务较适用于补偿动作容易处理的场景。

分布式事务方案设计

本地事务 两阶段提交 柔性事务
业务改造
一致性 不支持 支持
隔离性 不支持 支持
并发性能 无影响 严重衰退
适合场景 业务方处理不一致 短事务 & 低并发

seata

简介

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

  • 对业务无侵入:即减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入
  • 高性能:减少分布式事务解决方案所带来的性能消耗

官方站点:

1600073526306

术语

TC (Transaction Coordinator) 事务协调者 :维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM (Transaction Manager) 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM (Resource Manager) 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

image-20210215174627345

UNDO_LOG表: 回滚日志

  1. UNDO_LOG必须在每个业务数据库中创建,用于保存回滚操作数据
  2. 当全局提交时,UNDO_LOG记录直接删除
  3. 当全局回滚时,将现有数据撤销,还原至操作前的状态,详见beforeImage和afterImage
1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

1
2
3
4
5
6
id:自增id
branch_id:分支事务id
xid: 全局事务id,可以保证幂等性
context:
rollback_info:分支事务操作的记录在事务前后的记录镜像,即beforeImage和afterImage
log_status:
属性 解释
id 自增id
branch_id 分支事务id,RM的事务id
xid 全局事务id
context info序列化格式
rollback_info 分支事务操作的记录在事务前后的记录镜像,即beforeImage和afterImage
log_status 日志状态
log_created 日志创建时间
log_modified 日志修改时间
ext 扩展内容

AT模式(默认)

AT模式运行机制

AT模式的特点就是对业务无入侵式,整体机制分二阶段提交

两阶段提交协议的演变:

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

在 AT 模式下,用户只需关注自己的业务SQL,用户的业务SQL 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。

image-20210215181643407

实现步骤

  1. TM端使用@GlobalTransaction进行全局事务开启、提交、回滚(默认的事务传播机制是Threadlocal,线程上下文中)
  2. TM开始RPC调用远程服务
  3. RM端seata-client通过扩展DataSourceProxy,实现自动生成UNDO_LOGTC上报
  4. TM告知TC提交/回滚全局事务
  5. TC通知RM各自执行commit/rollback操作,同时清除undo_log

官方图

安装

下载(这里使用的0.9),并解压缩

http://seata.io/zh-cn/blog/download.html

修改conf/file.conf文件

  • mode="file"改为mode="db"

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    store {
    ## store mode: file、db、redis
    ## 这里直接改为db,默认是file
    mode = "db"

    ## file store property
    file {
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    maxBranchSessionSize = 16384
    # globe session size , if exceeded throws exceptions
    maxGlobalSessionSize = 512
    # file buffer size , if exceeded allocate new buffer
    fileWriteBufferCacheSize = 16384
    # when recover batch read size
    sessionReloadReadSize = 100
    # async, sync
    flushDiskMode = async
    }
  • db部分配置mysql相关信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "root"
    password = "123456"
    minConn = 5
    maxConn = 30
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
    }

  • 创建seata数据库

    导入conifg文件中的db_store.sql文件,这个文件在1.2.0一下的版本上有,高版本需要到低版本下载

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    -- the table to store GlobalSession data
    drop table if exists `global_table`;
    create table `global_table` (
    `xid` varchar(128) not null,
    `transaction_id` bigint,
    `status` tinyint not null,
    `application_id` varchar(32),
    `transaction_service_group` varchar(32),
    `transaction_name` varchar(128),
    `timeout` int,
    `begin_time` bigint,
    `application_data` varchar(2000),
    `gmt_create` datetime,
    `gmt_modified` datetime,
    primary key (`xid`),
    key `idx_gmt_modified_status` (`gmt_modified`, `status`),
    key `idx_transaction_id` (`transaction_id`)
    );

    -- the table to store BranchSession data
    drop table if exists `branch_table`;
    create table `branch_table` (
    `branch_id` bigint not null,
    `xid` varchar(128) not null,
    `transaction_id` bigint ,
    `resource_group_id` varchar(32),
    `resource_id` varchar(256) ,
    `lock_key` varchar(128) ,
    `branch_type` varchar(8) ,
    `status` tinyint,
    `client_id` varchar(64),
    `application_data` varchar(2000),
    `gmt_create` datetime,
    `gmt_modified` datetime,
    primary key (`branch_id`),
    key `idx_xid` (`xid`)
    );

    -- the table to store lock data
    drop table if exists `lock_table`;
    create table `lock_table` (
    `row_key` varchar(128) not null,
    `xid` varchar(96),
    `transaction_id` long ,
    `branch_id` long,
    `resource_id` varchar(256) ,
    `table_name` varchar(32) ,
    `pk` varchar(36) ,
    `gmt_create` datetime ,
    `gmt_modified` datetime,
    primary key(`row_key`)
    );

修改registry.conf文件

这里采用注册到nacos,nacos在springcloud-alibaba中比较流行

  • registry部分的由type="file"都换成type="nacos"

  • 将2处的nacos配置根据实际情况需要调整参数,本机启动nacos则不需要调整

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"

nacos {
serverAddr = "localhost:8848"
# 这里可以在nacos中创建命名空间
namespace = ""
cluster = "default"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}

config {
# file、nacos 、apollo、zk、consul、etcd3
# 这里可以选择使用nacos的配置中心,这里是个demo,这里不在使用,但是方法在下面
type = "file"

nacos {
serverAddr = "localhost:8848"
# 这里可以在nacos中创建命名空间
namespace = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
app.id = "seata-server"
apollo.meta = "http://192.168.1.204:8801"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}

修改conf/logback.xml

${user.home}改为具体的seata目录,我这里是D:\soft\seata-0.9,那么配置如下(在第19行),需要自己创建logs文件夹

1
<property name="LOG_HOME" value="D://soft//seata-0.9//logs"/>

启动bin/seata-server.bat

双击启动bin/seata-server.bat文件启动服务

  1. 为了方便查看更加清晰的日志,建议使用cmd的方式启动
    • 打开CMD窗口,使用cd命令切换到seatabin目录
    • 输入seata-server.bat回车

启动成功的结果如下

image-20210926180109514

使用Nacos作为seata的配置中心(这里没有使用)

如果不用Nacos,那seata的默认配置中心就是file.conf文件,接下来我们不再依靠该文件而是nacos,要让seata使用nacos作为配置中心,我们需要按照以下步骤进行操作。

  1. 保证你的Nacos服务已经启动,且可正常连接,

  2. 添加命名空间seata,获取到namespace的id,我这里是9e16e498-22a3-4c0d-b606-a754a78ce13a

  3. 修改registry.conf文件中的registry部分type = “nacos”,同时修改nacos部分的配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    nacos {
    application = "seata-server"
    serverAddr = "localhost:8848"
    group = "SEATA_GROUP"
    namespace = "9e16e498-22a3-4c0d-b606-a754a78ce13a"
    cluster = "default"
    username = "nacos"
    password = "nacos"
    }

  4. 修改registry.conf文件中的config部分type = “nacos”,同时修改nacos部分的配置

    1
    2
    3
    4
    5
    6
    7
    8
    nacos {
    serverAddr = "localhost:8848"
    namespace = "9e16e498-22a3-4c0d-b606-a754a78ce13a"
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    }

  5. 下载nacos-config.脚本config.txt

    这个也是在低版本有,高版本可以在这里下载点击进入下载页

  • nacos-config.shnacos-config.py选择一个:在seata目录下新建 script (非必须)目录,将 nacos-config.sh 放入script 目录下
  • config.txt:该文件存放在将seata目录下,与conf、lib目录同级,seata的非常全的配置内容,可通过nacos-config.sh脚本推送到nacos配置中心
  • 修改config.txt的内容,下述1处暂时无需修改,2处需要换成你的DB连接路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.thread-factory.boss-thread-prefix=NettyBoss
transport.thread-factory.worker-thread-prefix=NettyServerNIOWorker
transport.thread-factory.server-executor-thread-prefix=NettyServerBizHandler
transport.thread-factory.share-boss-worker=false
transport.thread-factory.client-selector-thread-prefix=NettyClientSelector
transport.thread-factory.client-selector-thread-size=1
transport.thread-factory.client-worker-thread-prefix=NettyClientWorkerThread
transport.thread-factory.boss-thread-size=1
transport.thread-factory.worker-thread-size=8
transport.shutdown.wait=3
service.vgroup_mapping.fsp_tx_group=default
service.enableDegrade=false
service.disable=false
service.max.commit.retry.timeout=-1
service.max.rollback.retry.timeout=-1
client.async.commit.buffer.limit=10000
client.lock.retry.internal=10
client.lock.retry.times=30
client.lock.retry.policy.branch-rollback-on-conflict=true
client.table.meta.check.enable=true
client.report.retry.count=5
client.tm.commit.retry.count=1
client.tm.rollback.retry.count=1
store.mode=db
store.file.dir=file_store/data
store.file.max-branch-session-size=16384
store.file.max-global-session-size=512
store.file.file-write-buffer-cache-size=16384
store.file.flush-disk-mode=async
store.file.session.reload.read_size=100
store.db.datasource=dbcp
store.db.db-type=mysql
store.db.driver-class-name=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true
store.db.user=root
store.db.password=123456
store.db.min-conn=1
store.db.max-conn=3
store.db.global.table=global_table
store.db.branch.table=branch_table
store.db.query-limit=100
store.db.lock-table=lock_table
recovery.committing-retry-period=1000
recovery.asyn-committing-retry-period=1000
recovery.rollbacking-retry-period=1000
recovery.timeout-retry-period=1000
transaction.undo.data.validation=true
transaction.undo.log.serialization=jackson
transaction.undo.log.save.days=7
transaction.undo.log.delete.period=86400000
transaction.undo.log.table=undo_log
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registry-type=compact
metrics.exporter-list=prometheus
metrics.exporter-prometheus-port=9898
support.spring.datasource.autoproxy=false

image-20210926181019792
  1. 打开git bash命令行,执行sh脚本
1
2
3
4
$ cd script
# -h 主机,你可以使用localhost,-p 端口号 你可以使用8848,-t 命名空间ID,-u 用户名,-p 密码
$ sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t 9e16e498-22a3-4c0d-b606-a754a78ce13a -u nacos -w nacos

这个过程比较慢

image-20210926181235706 image-20210926181210589

代码实现:

这里参考尚硅谷的代码实现

https://blog.csdn.net/u011863024/article/details/114298288

TCC模式

概念

TCC 模式需要用户根据自己的业务场景实现 Try、Confirm 和 Cancel 三个操作;事务发起方在一阶段执行 Try 方式,在二阶段提交执行 Confirm 方法,二阶段回滚执行 Cancel 方法。

img

TCC 三个方法描述:

  • Try:资源的检测和预留;
  • Confirm:执行的业务操作提交;要求 Try 成功 Confirm 一定要能成功;
  • Cancel:预留资源释放;

TCC 的实践经验

蚂蚁金服TCC实践,总结以下注意事项:

➢业务模型分2阶段设计 ➢并发控制 ➢允许空回滚 ➢防悬挂控制 ➢幂等控制

1 TCC 设计 - 业务模型分 2 阶段设计:

用户接入 TCC ,最重要的是考虑如何将自己的业务模型拆成两阶段来实现。

以“扣钱”场景为例,在接入 TCC 前,对 A 账户的扣钱,只需一条更新账户余额的 SQL 便能完成;但是在接入 TCC 之后,用户就需要考虑如何将原来一步就能完成的扣钱操作,拆成两阶段,实现成三个方法,并且保证一阶段 Try 成功的话 二阶段 Confirm 一定能成功。

img

如上图所示,Try 方法作为一阶段准备方法,需要做资源的检查和预留。在扣钱场景下,Try 要做的事情是就是检查账户余额是否充足,预留转账资金,预留的方式就是冻结 A 账户的 转账资金。Try 方法执行之后,账号 A 余额虽然还是 100,但是其中 30 元已经被冻结了,不能被其他事务使用。

二阶段 Confirm 方法执行真正的扣钱操作。Confirm 会使用 Try 阶段冻结的资金,执行账号扣款。Confirm 方法执行之后,账号 A 在一阶段中冻结的 30 元已经被扣除,账号 A 余额变成 70 元 。

如果二阶段是回滚的话,就需要在 Cancel 方法内释放一阶段 Try 冻结的 30 元,使账号 A 的回到初始状态,100 元全部可用。

用户接入 TCC 模式,最重要的事情就是考虑如何将业务模型拆成 2 阶段,实现成 TCC 的 3 个方法,并且保证 Try 成功 Confirm 一定能成功。相对于 AT 模式,TCC 模式对业务代码有一定的侵入性,但是 TCC 模式无 AT 模式的全局行锁,TCC 性能会比 AT 模式高很多。

2 TCC 设计 - 允许空回滚:

img

Cancel 接口设计时需要允许空回滚。在 Try 接口因为丢包时没有收到,事务管理器会触发回滚,这时会触发 Cancel 接口,这时 Cancel 执行时发现没有对应的事务 xid 或主键时,需要返回 回滚成功。让事务服务管理器认为已回滚,否则会不断重试,而 Cancel 又没有对应的业务数据可以进行回滚。

3 TCC 设计 - 防悬挂控制:

悬挂的意思是:Cancel 比 Try 接口先执行,出现的原因是 Try 由于网络拥堵而超时,事务管理器生成回滚,触发 Cancel 接口,而最终又收到了 Try 接口调用,但是 Cancel 比 Try 先到。按照前面允许空回滚的逻辑,回滚会返回成功,事务管理器认为事务已回滚成功,则此时的 Try 接口不应该执行,否则会产生数据不一致,所以我们在 Cancel 空回滚返回成功之前先记录该条事务 xid 或业务主键,标识这条记录已经回滚过,Try 接口先检查这条事务xid或业务主键如果已经标记为回滚成功过,则不执行 Try 的业务操作。

4 TCC 设计 - 幂等控制:

img

幂等性的意思是:对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的。因为网络抖动或拥堵可能会超时,事务管理器会对资源进行重试操作,所以很可能一个业务操作会被重复调用,为了不因为重复调用而多次占用资源,需要对服务设计时进行幂等控制,通常我们可以用事务 xid 或业务主键判重来控制。

使用

使用@LocalTCC注解,对代码有严重的侵入性,但是效率较高

需要自己实现这三个阶段的内容,Seata 实现 TCC 操作需要定义一个接口,添加@LocalTCC注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package cn.tedu.order.tcc;

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

import java.math.BigDecimal;

@LocalTCC
public interface OrderTccAction {

/*
第一阶段的方法
通过注解指定第二阶段的两个方法名

BusinessActionContext 上下文对象,用来在两个阶段之间传递数据
@BusinessActionContextParameter 注解的参数数据会被存入 BusinessActionContext
这里@TwoPhaseBusinessAction的注解,谢了你自己定义的提交和回滚方法,如果出异常主动调用你的实现方法
*/
@TwoPhaseBusinessAction(name = "orderTccAction", commitMethod = "commit", rollbackMethod = "rollback")
boolean prepareCreateOrder(BusinessActionContext businessActionContext,
@BusinessActionContextParameter(paramName = "orderId") Long orderId,
@BusinessActionContextParameter(paramName = "userId") Long userId,
@BusinessActionContextParameter(paramName = "productId") Long productId,
@BusinessActionContextParameter(paramName = "count") Integer count,
@BusinessActionContextParameter(paramName = "money") BigDecimal money);

// 第二阶段 - 提交
boolean commit(BusinessActionContext businessActionContext);

// 第二阶段 - 回滚
boolean rollback(BusinessActionContext businessActionContext);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package cn.tedu.order.tcc;

import cn.tedu.order.entity.Order;
import cn.tedu.order.mapper.OrderMapper;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Component
@Slf4j
public class OrderTccActionImpl implements OrderTccAction {
@Autowired
private OrderMapper orderMapper;

@Transactional
@Override
public boolean prepareCreateOrder(BusinessActionContext businessActionContext, Long orderId, Long userId, Long productId, Integer count, BigDecimal money) {
log.info("创建 order 第一阶段,预留资源 - "+businessActionContext.getXid());

Order order = new Order(orderId, userId, productId, count, money, 0);
orderMapper.create(order);

//事务成功,保存一个标识,供第二阶段进行判断
ResultHolder.setResult(getClass(), businessActionContext.getXid(), "p");
return true;
}

@Transactional
@Override
public boolean commit(BusinessActionContext businessActionContext) {
log.info("创建 order 第二阶段提交,修改订单状态1 - "+businessActionContext.getXid());

// 防止幂等性,如果commit阶段重复执行则直接返回
if (ResultHolder.getResult(getClass(), businessActionContext.getXid()) == null) {
return true;
}

//Long orderId = (Long) businessActionContext.getActionContext("orderId");
long orderId = Long.parseLong(businessActionContext.getActionContext("orderId").toString());
orderMapper.updateStatus(orderId, 1);

//提交成功是删除标识
ResultHolder.removeResult(getClass(), businessActionContext.getXid());
return true;
}

@Transactional
@Override
public boolean rollback(BusinessActionContext businessActionContext) {
log.info("创建 order 第二阶段回滚,删除订单 - "+businessActionContext.getXid());

//第一阶段没有完成的情况下,不必执行回滚
//因为第一阶段有本地事务,事务失败时已经进行了回滚。
//如果这里第一阶段成功,而其他全局事务参与者失败,这里会执行回滚
//幂等性控制:如果重复执行回滚则直接返回
if (ResultHolder.getResult(getClass(), businessActionContext.getXid()) == null) {
return true;
}

//Long orderId = (Long) businessActionContext.getActionContext("orderId");
long orderId = Long.parseLong(businessActionContext.getActionContext("orderId").toString());
orderMapper.deleteById(orderId);

//回滚结束时,删除标识
ResultHolder.removeResult(getClass(), businessActionContext.getXid());
return true;
}
}

https://blog.csdn.net/qq_39554452/article/details/109988012

Saga模式

概念

img

saga模式的实现,是长事务解决方案。

Saga 是一种补偿协议,在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。

img

如图:T1T3都是正向的业务流程,都对应着一个冲正逆向操作C1C3

分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。

Saga 正向服务与补偿服务也需要业务开发者实现。因此是业务入侵的。Saga 模式下分布式事务通常是由事件驱动的,各个参与者之间是异步执行的,Saga 模式是一种长事务解决方案。

Saga 模式使用场景

Saga 模式适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式一阶段就会提交本地事务,无锁、长流程情况下可以保证性能。

事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供 TCC 要求的接口,可以使用 Saga 模式。

Saga模式的优势是:

  • 一阶段提交本地数据库事务,无锁,高性能;

  • 参与者可以采用事务驱动异步执行,高吞吐;

  • 补偿服务即正向服务的“反向”,易于理解,易于实现;

缺点:Saga 模式由于一阶段已经提交本地数据库事务,且没有进行“预留”动作,所以不能保证隔离性。后续会讲到对于缺乏隔离性的应对措施。

与TCC实践经验相同的是,Saga 模式中,每个事务参与者的冲正、逆向操作,需要支持:

  • 空补偿:逆向操作早于正向操作时;
  • 防悬挂控制:空补偿后要拒绝正向操作
  • 幂等

XA模式

概念

XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准。

XA 规范 描述了全局的事务管理器与局部的资源管理器之间的接口。 XA规范 的目的是允许的多个资源(如数据库,应用服务器,消息队列等)在同一事务中访问,这样可以使 ACID 属性跨越应用程序而保持有效。

XA 规范 使用两阶段提交(2PC,Two-Phase Commit)来保证所有资源同时提交或回滚任何特定的事务。

XA 规范 在上世纪 90 年代初就被提出。目前,几乎所有主流的数据库都对 XA 规范 提供了支持。

施行

img

  • 执行阶段:

    • 可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚
    • 持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化(即,之后任何意外都不会造成无法回滚的情况)
  • 完成阶段:

    • 分支提交:执行 XA 分支的 commit
    • 分支回滚:执行 XA 分支的 rollback

XA 的价值

为什么要在 Seata 中增加 XA 模式呢?支持 XA 的意义在哪里呢?

本质上,Seata 已经支持的 3 大事务模式:AT、TCC、Saga 都是 补偿型 的。

补偿型 事务处理机制构建在 事务资源 之上(要么在中间件层面,要么在应用层面),事务资源 本身对分布式事务是无感知的。

aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy84WGt2Tm5UaWFwT09EWGhNcTdsU1c0ZkhSUmEwRXZJTkMyS1plWGJSeWQ2clBWNndVelFmYWtMSW85V0Q0d1R1Z05tYlNXcWlhZ3NGSVNzR2ljTDBSNEdlUS82NDA

事务资源 对分布式事务的无感知存在一个根本性的问题:无法做到真正的 全局一致性 。

比如,一条库存记录,处在 补偿型 事务处理过程中,由 100 扣减为 50。此时,仓库管理员连接数据库,查询统计库存,就看到当前的 50。之后,事务因为异外回滚,库存会被补偿回滚为 100(隔离级别在读已提交 以上不会出现)。显然,仓库管理员查询统计到的 50 就是 脏 数据。

可以看到,补偿型 分布式事务机制因为不要求 事务资源 本身(如数据库)的机制参与,所以无法保证从事务框架之外的全局视角的数据一致性。XA 的价值

与 补偿型 不同,XA 协议 要求 事务资源 本身提供对规范和协议的支持。

因为 事务资源 感知并参与分布式事务处理过程,所以 事务资源(如数据库)可以保障从任意视角对数据的访问有效隔离,满足全局数据一致性。

除了 全局一致性 这个根本性的价值外,支持 XA 还有如下几个方面的好处:

  • 业务无侵入:和 AT 一样,XA 模式将是业务无侵入的,不给应用设计和开发带来额外负担。

  • 数据库的支持广泛:XA 协议被主流关系型数据库广泛支持,不需要额外的适配即可使用。

  • 多语言支持容易:因为不涉及 SQL 解析,XA 模式对 Seata 的 RM 的要求比较少,为不同语言开发 SDK 较之 AT 模式将更 薄,更容易。

  • 传统基于 XA 应用的迁移:传统的,基于 XA 协议的应用,迁移到 Seata 平台,使用 XA 模式将更平滑。

XA 广泛被质疑的问题

不存在某一种分布式事务机制可以完美适应所有场景,满足所有需求。

XA 规范早在上世纪 90 年代初就被提出,用以解决分布式事务处理这个领域的问题。

现在,无论 AT 模式、TCC 模式还是 Saga 模式,这些模式的提出,本质上都源自 XA 规范对某些场景需求的无法满足。

XA 规范定义的分布式事务处理机制存在一些被广泛质疑的问题,针对这些问题,我们是如何思考的呢?

  1. 数据锁定:数据在整个事务处理过程结束前,都被锁定,读写都按隔离级别的定义约束起来。

思考:

数据锁定是获得更高隔离性和全局一致性所要付出的代价。

补偿型 的事务处理机制,在 执行阶段 即完成分支(本地)事务的提交,(资源层面)不锁定数据。而这是以牺牲 隔离性 为代价的。

另外,AT 模式使用 全局锁 保障基本的 写隔离,实际上也是锁定数据的,只不过锁在 TC 侧集中管理,解锁效率高且没有阻塞的问题。

  1. 协议阻塞:XA prepare 后,分支事务进入阻塞阶段,收到 XA commit 或 XA rollback 前必须阻塞等待。

思考:

协议的阻塞机制本身并不是问题,关键问题在于 协议阻塞 遇上 数据锁定。

如果一个参与全局事务的资源 “失联” 了(收不到分支事务结束的命令),那么它锁定的数据,将一直被锁定。进而,甚至可能因此产生死锁。

这是 XA 协议的核心痛点,也是 Seata 引入 XA 模式要重点解决的问题。

基本思路是两个方面:避免 “失联” 和 增加 “自解锁” 机制。(这里涉及非常多技术细节,暂时不展开,在后续 XA 模式演进过程中,会专门拿出来讨论)

  1. 性能差:性能的损耗主要来自两个方面:一方面,事务协调过程,增加单个事务的 RT;另一方面,并发事务数据的锁冲突,降低吞吐。

思考:

和不使用分布式事务支持的运行场景比较,性能肯定是下降的,这点毫无疑问。

本质上,事务(无论是本地事务还是分布式事务)机制就是拿部分 性能的牺牲 ,换来 编程模型的简单 。

与同为 业务无侵入 的 AT 模式比较:

首先,因为同样运行在 Seata 定义的分布式事务框架下,XA 模式并没有产生更多事务协调的通信开销。

其次,并发事务间,如果数据存在热点,产生锁冲突,这种情况,在 AT 模式(默认使用全局锁)下同样存在的。

所以,在影响性能的两个主要方面,XA 模式并不比 AT 模式有非常明显的劣势。

AT 模式性能优势主要在于:集中管理全局数据锁,锁的释放不需要 RM 参与,释放锁非常快;另外,全局提交的事务,完成阶段 异步化。

实现和使用

设计

设计目标

XA 模式的基本设计目标,两个主要方面:

  1. 从 场景 上,满足 全局一致性 的需求。
  2. 从 应用上,保持与 AT 模式一致的无侵入。
  3. 从 机制 上,适应分布式微服务架构的特点。

整体思路:

与 AT 模式相同的:以应用程序中 本地事务 的粒度,构建到 XA 模式的 分支事务。

通过数据源代理,在应用程序本地事务范围外,在框架层面包装 XA 协议的交互机制,把 XA 编程模型 透明化。

把 XA 的 2PC 拆开,在分支事务 执行阶段 的末尾就进行 XA prepare,把 XA 协议完美融合到 Seata 的事务框架,减少一轮 RPC 交互。

核心设计
整体运行机制

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

img

  • 执行阶段(E xecute):
    • XA start/XA end/XA prepare + SQL + 注册分支
  • 完成阶段(F inish):
    • XA commit/XA rollback
数据源代理

XA 模式需要 XAConnection。

获取 XAConnection 两种方式:

  • 方式一:要求开发者配置 XADataSource

  • 方式二:根据开发者的普通 DataSource 来创建

第一种方式,给开发者增加了认知负担,需要为 XA 模式专门去学习和使用 XA 数据源,与 透明化 XA 编程模型的设计目标相违背。

第二种方式,对开发者比较友好,和 AT 模式使用一样,开发者完全不必关心 XA 层面的任何问题,保持本地编程模型即可。

我们优先设计实现第二种方式:数据源代理根据普通数据源中获取的普通 JDBC 连接创建出相应的 XAConnection。

类比 AT 模式的数据源代理机制,如下:
img

但是,第二种方法有局限:无法保证兼容的正确性。

实际上,这种方法是在做数据库驱动程序要做的事情。不同的厂商、不同版本的数据库驱动实现机制是厂商私有的,我们只能保证在充分测试过的驱动程序上是正确的,开发者使用的驱动程序版本差异很可能造成机制的失效。

这点在 Oracle 上体现非常明显。参见 Druid issue:https://github.com/alibaba/druid/issues/3707

综合考虑,XA 模式的数据源代理设计需要同时支持第一种方式:基于 XA 数据源进行代理。

类比 AT 模式的数据源代理机制,如下:

img

分支注册

XA start 需要 Xid 参数。

这个 Xid 需要和 Seata 全局事务的 XID 和 BranchId 关联起来,以便由 TC 驱动 XA 分支的提交或回滚。

目前 Seata 的 BranchId 是在分支注册过程,由 TC 统一生成的,所以 XA 模式分支注册的时机需要在 XA start 之前。

将来一个可能的优化方向:

把分支注册尽量延后。类似 AT 模式在本地事务提交之前才注册分支,避免分支执行失败情况下,没有意义的分支注册。

这个优化方向需要 BranchId 生成机制的变化来配合。BranchId 不通过分支注册过程生成,而是生成后再带着 BranchId 去注册分支。

小结

这里只通过几个重要的核心设计,说明 XA 模式的基本工作机制。

此外,还有包括 连接保持、异常处理 等重要方面,有兴趣可以从项目代码中进一步了解。

以后会陆续写出来和大家交流。

演进规划

XA 模式总体的演进规划如下:

第 1 步(已经完成):首个版本(1.2.0),把 XA 模式原型机制跑通。确保只增加,不修改,不给其他模式引入的新问题。

第 2 步(计划 5 月完成):与 AT 模式必要的融合、重构。

第 3 步(计划 7 月完成):完善异常处理机制,进行上生产所必需的打磨。

第 4 步(计划 8 月完成):性能优化。

第 5 步(计划 2020 年内完成):结合 Seata 项目正在进行的面向云原生的 Transaction Mesh 设计,打造云原生能力。

XA 模式的使用

从编程模型上,XA 模式与 AT 模式保持完全一致。

可以参考 Seata 官网的样例:seata-xa

样例场景是 Seata 经典的,涉及库存、订单、账户 3 个微服务的商品订购业务。

在样例中,上层编程模型与 AT 模式完全相同。只需要修改数据源代理,即可实现 XA 模式与 AT 模式之间的切换。

1
2
3
4
5
6
7
8
@Bean("dataSourceProxy")
public DataSource dataSource(DruidDataSource druidDataSource) {
// DataSourceProxy for AT mode
// return new DataSourceProxy(druidDataSource);

// DataSourceProxyXA for XA mode
return new DataSourceProxyXA(druidDataSource);
}

总结

在当前的技术发展阶段,不存一个分布式事务处理机制可以完美满足所有场景的需求。

一致性、可靠性、易用性、性能等诸多方面的系统设计约束,需要用不同的事务处理机制去满足。

Seata 项目最核心的价值在于:构建一个全面解决分布式事务问题的 标准化 平台。

基于 Seata,上层应用架构可以根据实际场景的需求,灵活选择合适的分布式事务解决方案。

XA 模式的加入,补齐了 Seata 在 全局一致性 场景下的缺口,形成 AT、TCC、Saga、XA 四大 事务模式 的版图,基本可以满足所有场景的分布式事务处理诉求。

参考:

https://www.it235.com/%E9%AB%98%E7%BA%A7%E6%A1%86%E6%9E%B6/SpringCloudAlibaba/seata.html#%E7%AC%AC%E4%B8%80%E7%AB%A0%EF%BC%9A%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1%E5%9F%BA%E7%A1%80

https://seata.io/zh-cn/docs/dev/mode/xa-mode.html