distribute

什么是事务?

事务从本质上讲就是:逻辑上的一组操作,组成这组操作的各个逻辑单元在不同的服务甚至服务器上,保证它们要成功就都成功,要失败就都失败。

事务的四大特性

提到事务就不得不提事务的四大特性(基本特征) ACID:

  • 原子性(atomicity):“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。
  • 一致性(consistency):“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。
  • 隔离性(isolation):在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。
  • 持久性(durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中。

隔离级别和常见问题

一、四种事务隔离级别

1.1 read uncommitted 读未提交

即:事务A可以读取到事务B已修改但未提交的数据。

除非是文章阅读量,每次+1这种无关痛痒的场景,一般业务系统没有人会使用该事务隔离级别,标准实在太宽松了。

1.2 read committed 读已提交(简称RC)

即:事务A只能读取到事务B修改并已提交的数据。

这个级别相对要严格一些,至少是要等其它事务把变更提交到db,才能读取到,听上去蛮靠谱的。但是有些业务场景,比如会员系统中,如果要在一个事务中,多次读取用户身份,判断是否会员,如果刚开始读取到该用户是会员,做了一些逻辑处理,后面又读到用户不是会员了,这就有点崩溃,不知道如何继续。这种希望同1个事务中,关键数据不管读取多次次,结果都一样,RC级别就不行了。

1.3 repeatable read 可重复读

即:同一个事务中,多次读取某一行记录,始终是一样的值,不管在此期间,其它事务有没有修改过该数据(不论是否提交)。该级别解决了RC不可重复读的问题,但是存在幻读问题(幻读后面会详解)。

1.4 serializable 串行化

即:一个事务在修改其它数据时,如果有其它事务也想改,必须等前面的事务提交或回滚后,才能继续。最严格的级别,但是性能最低,也几乎没人用。

二、脏读/不可重复读/幻读

2.1 脏读

495

验证:

a. 找一个mysql环境,建一个测试表t_people,就2列 id ,name

b. 开二个mysql终端,连到db上,为方便讲解,这2个终端称为“终端1”、“终端2”,终端1里输入:

set session transaction isolation level read uncommitted;
start transaction;

即:设置当前会话的隔离级别为”读未提交”。

535

终端2里,输入:

start transaction;
update t_people set name='xxx' where id=1;

525

然后再回到“终端1”,执行 

select id,name from t_people where id=1;

480

可以看到,读取到了未提交的脏数据 。 终端2里,此时如果执行rollback回滚 

520

终端1里,继续执行

select id,name from t_people where id=1;

525

可以发现最新结果,已经是回滚后的数据。很显然:如果有脏读问题出现,就更加保证不了“可重复读”。

2.2 不可重复读

465

将事务隔离级别设置成 read committed(即:读已提交),可解决脏读问题,但满足不了“可重复读需求”。

验证方法跟刚才类似,终端1里输入:

set session transaction isolation level read committed;

将级别设置成RC,然后2个终端里都开启事务,终端2中,修改一行数据,但是不提交,此时终端1里应该是读不到终端2修改的数据。然后终端2提交,终端1才能读到修改后的数据。终端2如果继续修改、提交,终端1里再读取这1行,将是最新的值。(也就是只说,只要终端2不断修改,不断提交,终端1里就能读到这行不同的新值,即:保证不了同1个事务中,同一行数据,多次重复读取的值不变)

2.3 幻读

将隔离级别继续调整至Repeatable Read,还是刚才的场景,变成这样:

事务A对于同一行数据,不管读多少次,始终是相同的值,完全不理会有没有其它事务在修改它。有点:“两耳不闻窗外事,一心只读圣贤书”的味道。但是这也有问题,比如秒杀订单系统中,事务A第1次读取商品库存,发现还有1个,可以下单,赶紧继续,但是此时,可能有另一个事务,也在下单,已经提交了订单,把库存减为0了,事务A并不知道,因为多次读取库存的值是一样的,还是1,最后仍然把订单创建了,形成超卖。

验证方法:

set session transaction isolation level repeatable read;

剩下的步骤跟前面类似,就不重复赘述了。 

2.4 串行化

从db层面,要想同时解决脏读、不可重复读、幻读,只有串行化这个级别可以做到。

set session transaction isolation level serializable;

如下图:终端1设置串行化后,紧接着select xxx where id=1这条语句后,id=1的这行记录,就被锁了。

505

在终端2里,更新其它记录(即:id不等于1)可以正常成功,但是更新id=1 时,就会卡住,除非终端1把事务提交或回滚,否则将一直卡着,直到超时失败。

520

三、总结

并发事务可能会带来的问题

  • 脏读:一个事务可以读取另一个事务未提交的数据
  • 不可重复读:一个事务可以读取另一个事务已提交的数据单条记录前后不匹配
  • 虚读(幻读):一个事务可以读取另一个事务已提交的数据读取的数据前后多了或者少了

我们解决并发读的问题可以设置隔离级别解决问题:

隔离级别脏读不可重复读幻读
读未提交 (Read uncommitted)
读已提交 (Read committed)×
可重复读 (Repeatable read)××
可串行化 (Serializable )×××

Spring传播行为

Spring传播行为介绍
REQUIRED支持当前事务,如果不存在,就新建一个
SUPPORTS支持当前事务,如果不存在,就不使用事务
MANDATORY支持当前事务,如果不存在,抛出异常
REQUIRES_NEW如果有事务存在,挂起当前事务,创建一个新的事务
NOT_SUPPORTED以非事务方式运行,如果有事务存在,挂起当前事务
NEVER以非事务方式运行,如果有事务存在,抛出异常
NESTED如果当前事务存在,则嵌套事务执行(嵌套式事务)

分布式事务

背景

1.1 分布式架构演进之 - 数据库的水平拆分

如下图所示,分库分表之后,原来在一个数据库上就能完成的写操作,可能就会跨多个数据库,这就产生了跨数据库事务问题。

500

1.2 分布式架构演进之 - 业务服务化拆分

将单业务系统拆分成多个业务系统,降低了各系统之间的耦合度,使不同的业务系统专注于自身业务,更有利于业务的发展和系统容量的伸缩。

500

业务系统按照服务拆分之后,一个完整的业务往往需要调用多个服务,如何保证多个服务间的数据一致性成为一个难题。

总结

不同的服务,不同数据库
相同的服务,不同数据库
不同的服务,相同数据库

CAP 理论

CAP:

分布式存储系统的CAP原理(分布式系统的三个指标):

Consistency(一致性):在分布式系统中的所有数据备份,在同一时刻是否同样的值。
对于数据分布在不同节点上的数据来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。

  • 对于一致性,可以分为从客户端和服务端两个不同的视角。
    • 客户端
    • 从客户端来看,一致性主要指的是多并发访问时更新过的数据如何获取的问题。
    • 服务端
    • 从服务端来看,则是更新如何分布到整个系统,以保证数据最终一致。
  • 对于一致性,可以分为强/弱/最终一致性三类
    • 从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。
    • 强一致性
      • 对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。
    • 弱一致性
      • 如果能容忍后续的部分或者全部访问不到,则是弱一致性。
    • 最终一致性
      • 如果经过一段时间后要求能访问到更新后的数据,则是最终一致性。

Availability(可用性):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(要求数据需要备份)
Partition tolerance(分区容忍性):大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。

权衡

CAP 理论就是说在分布式存储系统中,最多只能实现上面的两点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们无法避免的。所以我们只能在一致性和可用性之间进行权衡,没有系统能同时保证这三点。要么选择 CP、要么选择 AP。

证明

495

假设在 N1和 N2之间网络断开的时候,有用户向 N1发送数据更新请求,那 N1中的数据 V0将被更新为 V1,由于网络是断开的,所以分布式系统同步操作 M,所以 N2中的数据依旧是 V0;这个时候,有用户向 N2发送数据读取请求,由于数据还没有进行同步,应用程序没办法立即给用户返回最新的数据 V1,怎么办呢?

有二种选择,第一,牺牲数据一致性,响应旧的数据V0给用户;第二,牺牲可用性,阻塞等待,直到网络连接恢复,数据更新操作M完成之后,再给用户响应最新的数据V1。

这个过程,证明了要满足分区容错性的分布式系统,只能在一致性和可用性两者中,选择其中一个。

BASE 理论

BASE 是对 CAP 中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于 CAP 定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。接下来看看 BASE 中的三要素:

Basically Available(基本可用)
基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。 电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。
Soft state(软状态)
软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现。
Eventually consistent(最终一致性)
最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
BASE 模型是传统 ACID 模型的反面,不同于 ACID,BASE 强调牺牲高一致性,从而获得可用性,数据允许在一段时间内的不一致,只要保证最终一致就可以了。

解决方案

分布式事务模型

解决分布式事务,各个子系统之间必须能感知到彼此的事务状态,才能保证状态一致,因此需要一个事务协调者来协调每一个事务的参与者(子系统事务)。这里的子系统事务,称为分支事务;有关联的各个分支事务在一起称为全局事务

505

  • 全局事务:整个分布式事务
  • 分支事务:分布式事务中包含的每个子系统的事务

方案

  • 两阶段提交(2PC)
    • XA
    • AT
    • 2PC 即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit phase),2是指两个阶段,P 是指准备阶段,C 是指提交阶段。
  • TCC (Try, Confirm, Cancel)
  • 消息队列
    • saga

Seata 分布式事务架构

  • TC(Transaction Coordinator)-事务协调者: 维护全局和分支事务的状态,协调全局事务提交或回滚。
  • TM(Transaction Manager)-事务管理器: 定义全局事务的范围、开始全局事务、提交或回滚全局事务。
  • RM(Resource Manager)-资源管理器: 管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

在 Seata 中,分布式事务的执行流程:

  • TM 开启分布式事务(TM 向 TC 注册全局事务记录);
  • 按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态 );
  • TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务);
  • TC 汇总事务信息,决定分布式事务是提交还是回滚;
  • TC 通知所有 RM 提交/回滚 资源,事务二阶段结束;

Seata 提供了 4 中不同的分布式事务解决方案:

  • XA 模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
  • TCC 模式:最终一致的分阶段事务模式,有业务侵入
  • AT 模式:最终一致的分阶段事务模式,无业务侵入,也是 Seata 的默认模式
  • SAGA 模式:长事务模式,有业务侵入

XA 模式原理

XA 规范是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范描述了全局的 TM 与局部的 RM 之间的接口,几乎所有主流的数据库都对 XA 规范提供了支持。
标准的 XA 模式为两阶段提交:

  • 第一阶段由事务协调者向 RM(XA 模式下一般由数据库实现)发起事务准备请求,RM 执行完毕之后,并不直接提交事务,而是将执行的结果告知事务协调者。
  • 第二阶段由事务协调者判断 RM 的返回结果,如果分支事务都成功了,向 RM 发起提交请求,RM 执行事务提交并返回已提交请求

具体过程如下图所示

但是,如果在事务执行过程中有一个失败了,事务协调者则会回滚所有已执行事务

Seata 在实现 XA 模式时进行了一定的调整,但大体上相似:
RM 一阶段工作:

  1. 注册分支事务到 TC
  2. 执行分支业务 SQL 但不提交
  3. 报告执行状态到 TC

TC 二阶段工作:

  • TC 检测各分支事务执行状态
  • 如果都成功,通知所有 RM 提交事务
  • 如果有失败,通知所有 RM 回滚事务

RM 二阶段工作:

  • 接受 TC 指令,提交或回滚事务

[

XA 模式总结

优点:

  • 事务强一致性,满足 ACID 原则
  • 常用数据库都支持,实现简单,没有代码侵入

缺点:

  • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,所以性能较差
  • 依赖关系型数据库实现事务

AT 模式原理

AT 模式同样是分阶段提交的事务模型,不过缺弥补了 XA 模型中资源锁定周期过长的缺陷。

AT 模式在执行完 sql 之后会直接提交事务,而不是进行等待,在执行的同时 RM 拦截本次执行,记录更新前后的快照到数据库的 undo_log 中。
与 XA 的不同之处在于

阶段一 RM 的工作:

  • 注册分支事务
  • 记录 undo-log(数据快照)
    • 也叫 before image
  • 执行业务 sql 并提交
  • 报告事务状态

阶段二提交时 RM 的工作:

  • 删除 undo-log 即可

阶段二回滚时 RM 的工作:

  • 根据 undo-log 回复数据到更新前

具体案例:例如,一个分支业务的 SQL 是这样的:update tb_account set money = money - 10 where id = 1

如果这条 sql 执行成功,那么 money 字段自然是 90,如果执行失败,则根据数据快照恢复数据。

AT 模式总结

与 XA 模式最大的区别是:

  • XA 模式一阶段不提交事务,锁定资源;AT 模式一阶段直接提交,不锁定资源。
  • XA 模式依赖数据库机制实现回滚;AT 模式利用数据快照实现数据回滚。
  • XA 模式强一致;AT 模式最终一致

优点:

  • 一阶段完成直接提交事务,释放数据库资源,性能比较好
  • 利用全局锁实现读写隔离
  • 没有代码侵入,框架自动完成回滚和提交

缺点:

  • 两阶段之间属于软状态,属于最终一致
  • 框架的快照功能会影响性能,但比 XA 模式要好很多

TCC 模式原理

TCC 模式与 AT 模式非常相似,每阶段都是独立事务,不同的是 TCC 通过人工编码来实现数据恢复。需要实现三个方法:

  • Try:资源的检测和预留;
  • Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
  • Cancel:预留资源释放,可以理解为 Try 的反向操作。

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

举例,一个扣减用户余额的业务。假设账户 A 原来余额是 100,需要余额扣减 30元。

  • 阶段一(Try): 检查余额是否充足,如果充足则冻结金额增加 30 元,可用余额扣除 30

  • 阶段二:假如要提交(Confirm),则冻结金额扣减 30

  • 阶段三:如果要回滚(Cancel),则冻结金额扣减 30,可用余额增加 30

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 设计 - 允许空回滚:

16_51_44__08_13_2019.jpg

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

3 TCC 设计 - 防悬挂控制:

16_51_56__08_13_2019.jpg

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

4 TCC 设计 - 幂等控制:

16_52_07__08_13_2019.jpg

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

TCC 工作模型图:

TCC 模式总结

TCC 模式的每个阶段是做什么的?

  • Try:资源检查和预留
  • Confirm:业务执行和提交
  • Cancel:预留资源的释放

TCC 的优点是什么?

  • 一阶段完成直接提交事务,释放数据库资源,性能好
  • 相比 AT 模型,无需生成快照,无需使用全局锁,性能最强
  • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库

TCC 的缺点是什么?

  • 有代码侵入,需要人为编写 try、Confirm 和 Cancel 接口
  • 软状态,事务是最终一致
  • 需要考虑 Confirm 和 Cancel 的失败情况,做好幂等处理

SAGA 模式

Saga 模式是 SEATA 提供的长事务解决方案。也分为两个阶段:

  • 一阶段:直接提交本地事务(TCC 是预留)
  • 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚

如图所示,SAGA 模式下,事务一旦有一个出现问题,则反向按照事务调用顺序进行补偿,从而保证一致性

1 Saga 模式使用场景

16_44_58__08_13_2019.jpg

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

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

Saga模式的优势是:

  • 一阶段提交本地数据库事务,无锁,高性能;
  • 参与者可以采用事务驱动异步执行,高吞吐;
  • 补偿服务即正向服务的“反向”,易于理解,易于实现;

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

2 基于状态机引擎的 Saga 实现

17_13_19__08_13_2019.jpg

目前 Saga 的实现一般有两种,一种是通过事件驱动架构实现,一种是基于注解加拦截器拦截业务的正向服务实现。Seata 目前是采用事件驱动的机制来实现的,Seata 实现了一个状态机,可以编排服务的调用流程及正向服务的补偿服务,生成一个 json 文件定义的状态图,状态机引擎驱动到这个图的运行,当发生异常的时候状态机触发回滚,逐个执行补偿服务。当然在什么情况下触发回滚用户是可以自定义决定的。该状态机可以实现服务编排的需求,它支持单项选择、并发、异步、子状态机调用、参数转换、参数映射、服务执行状态判断、异常捕获等功能。

3 状态机引擎原理

16_45_32__08_13_2019.jpg

该状态机引擎的基本原理是,它基于事件驱动架构,每个步骤都是异步执行的,步骤与步骤之间通过事件队列流转,
极大的提高系统吞吐量。每个步骤执行时会记录事务日志,用于出现异常时回滚时使用,事务日志会记录在与业务表所在的数据库内,提高性能。

4 状态机引擎设计

16_45_46__08_13_2019.jpg

该状态机引擎分成了三层架构的设计,最底层是“事件驱动”层,实现了 EventBus 和消费事件的线程池,是一个 Pub-Sub 的架构。第二层是“流程控制器”层,它实现了一个极简的流程引擎框架,它驱动一个“空”的流程执行,“空”的意思是指它不关心流程节点做什么事情,它只执行每个节点的 process 方法,然后执行 route 方法流转到下一个节点。这是一个通用框架,基于这两层,开发者可以实现任何流程引擎。最上层是“状态机引擎”层,它实现了每种状态节点的“行为”及“路由”逻辑代码,提供 API 和状态图仓库,同时还有一些其它组件,比如表达式语言、逻辑计算器、流水生成器、拦截器、配置管理、事务日志记录等。

5 Saga 服务设计经验

和TCC类似,Saga的正向服务与反向服务也需求遵循以下设计原则:

1)Saga 服务设计 - 允许空补偿

16_52_22__08_13_2019.jpg

2)Saga 服务设计 - 防悬挂控制

16_52_52__08_13_2019.jpg

3)Saga 服务设计 - 幂等控制

3 分布式事务 Seata 三种模式详解-屹远-31.jpg

4)Saga 设计 - 自定义事务恢复策略

16_53_07__08_13_2019.jpg

前面讲到 Saga 模式不保证事务的隔离性,在极端情况下可能出现脏写。比如在分布式事务未提交的情况下,前一个服务的数据被修改了,而后面的服务发生了异常需要进行回滚,可能由于前面服务的数据被修改后无法进行补偿操作。这时的一种处理办法可以是“重试”继续往前完成这个分布式事务。由于整个业务流程是由状态机编排的,即使是事后恢复也可以继续往前重试。所以用户可以根据业务特点配置该流程的事务处理策略是优先“回滚”还是“重试”,当事务超时的时候,Server 端会根据这个策略不断进行重试。

由于 Saga 不保证隔离性,所以我们在业务设计的时候需要做到“宁可长款,不可短款”的原则,长款是指在出现差错的时候站在我方的角度钱多了的情况,钱少了则是短款,因为如果长款可以给客户退款,而短款则可能钱追不回来了,也就是说在业务设计的时候,一定是先扣客户帐再入帐,如果因为隔离性问题造成覆盖更新,也不会出现钱少了的情况。

6 基于注解和拦截器的 Saga 实现

17_13_37__08_13_2019.jpg

还有一种 Saga 的实现是基于注解+拦截器的实现,Seata 目前没有实现,可以看上面的伪代码来理解一下,one 方法上定义了 @SagaCompensable 的注解,用于定义 one 方法的补偿方法是 compensateOne 方法。然后在业务流程代码 processA 方法上定义 @SagaTransactional 注解,启动 Saga 分布式事务,通过拦截器拦截每个正向方法当出现异常的时候触发回滚操作,调用正向方法的补偿方法。

7 两种 Saga 实现优劣对比

两种 Saga 的实现各有又缺点,下面表格是一个对比:

17_13_49__08_13_2019.jpg

状态机引擎的最大优势是可以通过事件驱动的方法异步执行提高系统吞吐,可以实现服务编排需求,在 Saga 模式缺乏隔离性的情况下,可以多一种“向前重试”的事情恢复策略。注解加拦截器的的最大优势是,开发简单、学习成本低。

8 SAGA 模式总结

Saga 模式优点:

  • 事务参与者可以基于事件驱动实现异步调用,吞吐高
  • 一阶段直接提交事务,无锁,性能好
  • 不用编写 TCC 中的三个阶段,实现简单

缺点:

  • 软状态持续时间不确定,时效性差
  • 没有锁,没有事务隔离,会有脏写

四种模式对比

-XAATTCCSAGA
一致性强一致最终一致最终一致最终一致
隔离性完全隔离基于全局锁隔离基于资源预留隔离无隔离
代码侵入有,需要编写 3 个接口有,需要编写状态机和补偿业务
性能非常好非常好
场景对一致性、隔离性有高要求的业务基于关系型数据库的大多数分布式事务场景都可以对性能要求较高的事务;有非关系型数据库要参与的事务业务流程长、业务流程多;参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口

参考

分布式事务XA、AT、TCC、SAGA - benym
Java分布式事务及seata框架的使用 - 知乎
分布式事务 Seata 及其三种模式详解