Seata框架实现分布式事务
Seata是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的分布式解决方案。
-
AT 模式(原子性模式):
- AT(Atomic Transaction)模式是一种使用关系型数据库的分布式事务模式。在这种模式下,Seata利用数据库的本地事务来实现全局事务的一致性。
- AT 模式通常适用于已经使用关系型数据库的传统应用,无需修改业务逻辑代码。
-
TCC 模式(Try-Confirm-Cancel 模式):
- TCC(Try-Confirm-Cancel)模式是一种通过三个阶段来实现分布式事务的模式,包括尝试(Try)、确认(Confirm)、取消(Cancel)阶段。
- TCC 模式适用于需要精细控制事务的应用,允许开发人员在每个阶段编写自定义逻辑。
-
SAGA 模式:
- SAGA 模式是一种通过有限状态机来实现分布式事务的模式。在SAGA模式中,事务被拆分为一系列状态转换,每个状态转换代表一个事务操作。
- SAGA 模式适用于需要异步和长时间运行的分布式事务,允许应用在事务的不同状态之间进行状态迁移。
-
XA 模式(支持 X/Open XA 协议):
- XA 模式是一种支持 X/Open XA 协议的分布式事务模式。它允许应用使用 XA 接口来控制分布式事务,包括两阶段提交(2PC)和协调器。
-
HLS (Highly Available Lock Service)模式:
- HLS 模式是一种专为高可用性场景设计的分布式事务模式,它使用高可用性锁服务来实现全局事务的协调。
AT模式实现原理
AT模式目前看来是Seata框架独有的一种模式,其它的分布式框架上并没有此种模式的实现。其是由二阶段提交演变而来。解决了二阶段提交的同步阻塞等问题。
演变后的两阶段提交协议:
-
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
-
二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
Seata框架中有三个概念要阐述下。
- TC:事务协调器,它是独立的组件,需要独立部署运行,Seata提供了这个独立运行的程序,它负责维护全局事务的运行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信,协调各个分支事务的提交或回滚。
- TM:事务管理器,TM需要嵌入应用程序,它负责开启一个全局事务,并定义全局事务的范围,它的目的是最终向TC发起全局提交或回滚指令。
- RM:与TC通信,控制分支事务,负责分支注册、报告分支事务状态,并接收事务协调器TC的指令,命令分支事务完成本地事务的提交或回滚。
流程示意图如下:
这个图还是比较清晰地,建议是先好好的理解下全流程。
Seata AT模式的具体流程如下。
- 订单服务收到请求,订单服务中的TM向TC申请开启一个全局事务。
- TC收到请求,创建一个全局事务,并将全局事务ID(称为XID)返回给订单服务的TM。
- 订单服务的RM向TC注册分支事务。
- 订单服务执行本地分支事务的业务逻辑并提交,释放锁定的数据库资源。
- 订单服务向TC上报本地分支事务的提交结果。
- 订单服务调用远程的积分服务,此时将XID通过参数传给积分服务。
- 积分服务向TC注册分支事务。
- 积分服务执行本地分支事务的业务逻辑并提交,释放锁定的数据库资源,并返回订单服务。
- 积分服务向TC上报本地分支事务的提交结果。
- 订单服务的TM向TC发起全局事务的提交或回滚。
- TC向XID管辖下的全部分支事务发出提交或回滚的指令。
设计思路
TM的设计
流程的开始与结束是由TM决定的,这个TM就是订单服务Controller入口进去后的某个Service方法(当然也可能不是,看你的代码架构,我这里只按照我自己的平常的开发模式来。)在这个Service方法中,处理了订单服务以及积分服务的业务。当Service方法完成后,那么就是整个业务做完了,事务即完成。因此,要我来实现,这个TM对应的Service方法,我会选择用一个注解包裹,通过动态代理的方式,在这个方法的前后分别负责全局事务的开始与结束流程。
RM的设计
RM负责执行具体的业务,将数据入库同时上报给TC。由于二阶段回滚时需要根据回滚日志replay,那么就一定需要记录业务数据执行的日志。那怎么记录?回想了下Mybatis中的数据源代理,这里也是一样的思路。必须拦截或是代理原先的数据源,解析原执行的sql,注入Seata的逻辑,增强其执行逻辑。我们看下RM要做的事情,第一阶段:
第二阶段提交:
第二阶段回滚:
在执行sql的过程中,各个代理对象起到的作用如下:
所以在RM端最关键的就是数据源代理以及远程通信。这两块尤其是前者,就是AT模式的技术实现。
TC的设计
TC端需要管理Seata全局事务会话信息,一般是由全局事务、分支事务、全局锁构成,对应表globaltable、branchtable、lock_table。因此在安装部署的时候(file模式除外)都会创建这三个表。从上面来看,目前我们还没有实现隔离性,所谓的隔离性是指多个用户并发访问数据库时,数据库为每个用户开启的事务不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。这也是这里有一个全局锁表的原因。每次本地事务提交前,都会向TC端申请注册分支,同时还会申请全局锁,RM端通过拿到的全局锁保证了读写的隔离性,因此一旦当前事务持有全局锁,那么其他的事务不能提交。
写隔离
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
- tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的全局锁 ,本地提交释放本地锁。
- tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的全局锁,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待全局锁。
- tx1 二阶段全局提交,释放全局锁。tx2 拿到全局锁提交本地事务。
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的全局锁等锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程全局锁在 tx1 结束前一直是被 tx1 持有的,所以不会发生脏写的问题。
读隔离
seata at 模式默认的隔离级别为读未提交(因为已经提交的sql有可能会回滚)。如果要实现读已提交,select语句需要更改为 SELECT FOR UPDATE 语句。
SELECT FOR UPDATE 语句的执行会申请全局锁,如果全局锁被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是已提交的,才返回。
AT模式优缺点
优点:
- 相对简单: Seata AT模式相对于某些其他分布式事务实现方式来说较为简单。它不需要大规模修改应用程序代码,通常可以通过替换数据源或集成Seata的方式来使用。
- 支持传统数据库: AT模式适用于使用传统关系型数据库的应用程序。对于那些已经在生产环境中使用关系型数据库的应用来说,它提供了一种相对容易实施的分布式事务解决方案。
- 较高的一致性: AT模式通常提供较高级别的一致性,因为它依赖于数据库的本地事务,这有助于确保事务的一致性。
- 容错性: Seata AT模式是一个比较容错的分布式事务解决方案。即使在分支事务中出现故障,Seata可以通过回滚操作来确保全局事务的一致性。
缺点:
- 性能开销: AT模式在事务执行期间需要锁定资源,这可能会导致性能开销。锁定的资源可能会影响并发性能,特别是在高并发场景下。
- 数据库依赖: AT模式依赖于数据库的本地事务,因此不适用于不使用关系型数据库的应用或多数据库之间的分布式事务。
- 一致性难以满足: 尽管AT模式提供了较高级别的一致性,但在某些特殊情况下,如跨多个数据库的事务,一致性难以满足。
- 不适用于长事务: AT模式通常不适用于需要长时间运行的事务,因为资源锁定可能会阻止其他事务的执行。
- 局限性: AT模式的适用范围相对有限,不适用于所有分布式事务场景。在一些高并发、低延迟或需要更高一致性级别的场景中,可能需要考虑其他分布式事务模式,如TCC或SAGA。
总的来说,Seata AT模式是一个相对简单且适用于特定场景的分布式事务解决方案。开发人员应该在使用时考虑性能、数据库依赖、一致性需求和事务时延等因素,以确保它适合他们的应用需求。
AT模式适用场景
- 传统关系型数据库: 当应用程序使用关系型数据库作为主要的数据存储和事务管理工具时,Seata AT模式是一种合适的选择。
- 无需修改业务逻辑: AT模式通常无需对现有业务逻辑进行大规模的修改。应用程序可以继续使用现有的数据访问层和事务管理方法。
- 一致性要求较高: AT模式可以提供相对较高的一致性保证,适用于需要强一致性的场景。
- 较短的事务时延: AT模式通常适用于较短时延的事务,因为在事务期间数据库锁定资源,可能会影响其他事务的执行。
- 容忍较低的并发度: 由于在全局事务执行期间,分支事务需要占用数据库资源,因此容忍较低的并发度。
- 只存在数据库回滚的业务: 这也是最重要的一个选型标准,需要看你的分布式事务回滚是否只回滚关系型数据库的数据,因为Seata只支持数据库中的数据回滚。
需要注意的是,尽管Seata AT模式适用于一些传统应用,但它不是适用于所有分布式系统的解决方案。在某些高并发、低延迟或需要较长时间运行的场景中,可能需要考虑其他分布式事务模式,如TCC或SAGA。
TCC模式实现原理
TCC(Try-Confirm-Cancel)将一个事务分成两阶段:
- Try阶段:尝试锁定资源
- Confirm阶段:如果Try阶段所有资源均锁定成功,那么执行Confirm阶段,真正的扣除资源。
- Cancel阶段:如果Try阶段有部分资源锁定失败,那么执行Cancel阶段,回滚Try阶段锁定的资源。
注意:除了Try阶段为主动触发外,Confirm/Cancel均有框架从自动发起。
TCC系统模型如下所示:
1、业务操作分两阶段完成
接入 TCC 前,业务操作只需要一步就能完成,但是在接入 TCC 之后,需要考虑如何将其分成 2 阶段完成,把资源的检查和预留放在一阶段的 Try 操作中进行,把真正的业务操作的执行放在二阶段的 Confirm 操作中进行。
以下举例说明业务模式如何分成两阶段进行设计,举例场景:“账户A的余额中有 100 元,需要扣除其中 30 元”;
在接入 TCC 之前,用户编写 SQL:“update 账户表 set 余额 = 余额 - 30 where 账户 = A”,便能一步完成扣款操作。
在接入 TCC 之后,就需要考虑如何将扣款操作分成 2 步完成:
- Try 操作:资源的检查和预留;
在扣款场景,Try 操作要做的事情就是先检查 A 账户余额是否足够,再冻结要扣款的 30 元(预留资源);此阶段不会发生真正的扣款。
- Confirm 操作:执行真正业务的提交;
在扣款场景下,Confirm 阶段走的事情就是发生真正的扣款,把A账户中已经冻结的 30 元钱扣掉。
- Cancel 操作:预留资源的是否释放;
在扣款场景下,扣款取消,Cancel 操作执行的任务是释放 Try 操作冻结的 30 元钱,是 A 账户回到初始状态。
TCC中需要解决的问题
1、并发控制
用户在实现 TCC 时,应当考虑并发性问题,将锁的粒度降到最低,以最大限度的提高分布式事务的并发性。
以下还是以A账户扣款为例,“账户 A 上有 100 元,事务 T1 要扣除其中的 30 元,事务 T2 也要扣除 30 元,出现并发”。
在一阶段 Try 操作中,分布式事务 T1 和分布式事务 T2 分别冻结资金的那一部分资金,相互之间无干扰;这样在分布式事务的二阶段,无论 T1 是提交还是回滚,都不会对 T2 产生影响,这样 T1 和 T2 在同一笔业务数据上并行执行。
Seata 是怎么处理状态锁定的呢?
在 TCC 事务控制表记录当前状态以及扣减的数量,是业务表之外的事务表,这样不会影响其他事务的执行。
2、允许空回滚
如下图所示,事务协调器在调用 TCC 服务的一阶段 Try 操作时,可能会出现因为丢包而导致的网络超时,此时事务管理器会触发二阶段回滚,调用 TCC 服务的 Cancel 操作,而 Cancel 操作调用未出现超时。
TCC 服务在未收到 Try 请求的情况下收到 Cancel 请求,这种场景被称为空回滚;空回滚在生产环境经常出现,用户在实现TCC服务时,应允许允许空回滚的执行,即收到空回滚时返回成功。
要想防止空回滚,那么必须在 Cancel 方法中识别这是一个空回滚,Seata 是如何做的呢?
Seata 的做法是新增一个 TCC 事务控制表,包含事务的 XID 和 BranchID 信息,在 Try 方法执行时插入一条记录,表示一阶段执行了,执行 Cancel 方法时读取这条记录,如果记录不存在,说明 Try 方法没有执行。
3、防悬挂控制
如下图所示,事务协调器在调用 TCC 服务的一阶段 Try 操作时,可能会出现因网络拥堵而导致的超时,此时事务管理器会触发二阶段回滚,调用 TCC 服务的 Cancel 操作,Cancel 调用未超时;在此之后,拥堵在网络上的一阶段 Try 数据包被 TCC 服务收到,出现了二阶段 Cancel 请求比一阶段 Try 请求先执行的情况,此 TCC 服务在执行晚到的 Try 之后,将永远不会再收到二阶段的 Confirm 或者 Cancel ,造成 TCC 服务悬挂。
用户在实现 TCC 服务时,要允许空回滚,但是要拒绝执行空回滚之后 Try 请求,要避免出现悬挂。
Seata 是怎么处理悬挂的呢?
在 TCC 事务控制表记录状态的字段 status 中增加一个状态:
- suspended:4
当执行二阶段 Cancel 方法时,如果发现 TCC 事务控制表有相关记录,说明二阶段 Cancel 方法优先一阶段 Try 方法执行,因此插入一条 status=4 状态的记录,当一阶段 Try 方法后面执行时,判断 status=4 ,则说明有二阶段 Cancel 已执行,并返回 false 以阻止一阶段 Try 方法执行成功。
4、幂等控制
无论是网络数据包重传,还是异常事务的补偿执行,都会导致 TCC 服务的 Try、Confirm 或者 Cancel 操作被重复执行;用户在实现 TCC 服务时,需要考虑幂等控制,即 Try、Confirm、Cancel 执行一次和执行多次的业务结果是一样的。
同样的也是在 TCC 事务控制表中增加一个记录状态的字段 status,该字段有 3 个值,分别为:
- tried:1
- committed:2
- rollbacked:3
二阶段 Confirm/Cancel 方法执行后,将状态改为 committed 或 rollbacked 状态。当重复调用二阶段 Confirm/Cancel 方法时,判断事务状态即可解决幂等问题。
TCC模式优缺点
优点:
- 精细控制: TCC 模式允许开发人员对分布式事务的每个阶段进行精细控制。这意味着您可以明确定义尝试、确认和取消操作,以满足特定的业务需求。
- 灵活性: TCC 模式非常灵活,适用于各种分布式事务场景。它允许开发人员根据不同的业务逻辑来实现尝试、确认和取消操作。
- 高一致性: TCC 模式通常提供较高级别的一致性,因为它允许在每个阶段执行额外的逻辑来确保事务的一致性。这对需要强一致性的业务非常有用。
- 允许回滚: TCC 模式支持事务的回滚,因此可以在确认阶段出现问题时执行取消操作以还原事务状态。
缺点:
- 复杂性: TCC 模式的实现相对复杂,因为它需要开发人员明确定义每个阶段的操作。这可能增加了代码编写和维护的工作量。
- 性能开销: TCC 模式的性能开销通常较高,因为它需要在每个阶段执行额外的逻辑和操作。这可能会导致一些性能损失,特别是在高并发场景下。
- 难以实施: TCC 模式需要对业务逻辑有较深入的理解,以确保正确实现尝试、确认和取消操作。这可能对一些开发人员来说有一定的学习曲线。
- 不适用于长事务: TCC 模式通常不适用于需要长时间运行的事务,因为它可能会阻塞资源并影响性能。
总的来说,TCC 模式在分布式事务管理中具有一些独特的优势,但也伴随着一些挑战。选择是否使用 TCC 模式应基于业务需求、复杂性和性能等因素进行仔细考虑。在需要较高一致性和精细控制的情况下,TCC 模式是一个可选的分布式事务解决方案。
Saga模式
基本机制
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务(执行处理时候出错了,给一个修复的机会)都由业务开发实现。
Saga 模式下分布式事务通常是由事件驱动的,各个参与者之间是异步执行的,Saga 模式是一种长事务解决方案。
为什么需要Saga
之前我们学习的Seata分布式三种操作模型中所使用的的微服务全部可以根据开发者的需求进行修改,但是在一些特殊环境下,比如老系统,封闭的系统(无法修改,同时没有任何分布式事务引入),那么AT、XA、TCC模型将全部不能使用,为了解决这样的问题,才引用了Saga模型。
比如:事务参与者可能是其他公司的服务或者是遗留系统,无法改造,可以使用Saga模式。
Saga模式是Seata提供的长事务解决方案,提供了异构系统的事务统一处理模型。在Saga模式中,所有的子业务都不在直接参与整体事务的处理(只负责本地事务的处理),而是全部交由了最终调用端来负责实现,而在进行总业务逻辑处理时,在某一个子业务出现问题时,则自动补偿全面已经成功的其他参与者,这样一阶段的正向服务调用和二阶段的服务补偿处理全部由总业务开发实现。
Saga状态机
目前Seata提供的Saga模式只能通过状态机引擎来实现,需要开发者手工的进行Saga业务流程绘制,并且将其转换为Json配置文件,而后在程序运行时,将依据子配置文件实现业务处理以及服务补偿处理,而要想进行Saga状态图的绘制,一般需要通过Saga状态机来实现。
基本原理:
- 通过状态图来定义服务调用的流程并生成json定义文件
- 状态图中一个节点可以调用一个服务,节点可以配置它的补偿节点
- 状态图 json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚
- 可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能
Saga状态机的应用
官方提供了一个状态机设计器
官方文档地址:seata.io/zh-cn/docs/…
Seata Safa状态机可视化图形设计器使用地址:github.com/seata/seata…
Saga模式优缺点
优点:
- 灵活性: Saga模式非常灵活,适用于各种分布式事务场景。它允许开发人员根据不同的业务需求定义分布式事务的各个阶段,包括尝试、补偿和确认。
- 分布式事务的长时运行: Saga模式适用于长时间运行的分布式事务。事务可以在各个阶段之间暂停和恢复,从而应对各种可能的失败情况。
- 容错性: Saga模式允许定义补偿操作,以处理可能的故障和错误情况。这提供了一种容错机制,以确保分布式事务在异常情况下也能够最终达到一致状态。
- 局部一致性: Saga模式强调局部一致性,即在每个阶段都努力使数据和状态保持一致,而不需要等待全局一致性。这有助于提高性能和并发度。
- 分布式事务监控: Seata提供了用于Saga模式的监控和调试工具,有助于开发人员跟踪和诊断分布式事务的执行。
缺点:
- 复杂性: Saga模式的实现相对复杂,因为它需要开发人员明确定义每个阶段的操作、补偿操作以及确认操作。这可能增加了代码编写和维护的工作量。
- 性能开销: Saga模式的性能开销较高,因为它需要在每个阶段执行额外的逻辑和操作,以及记录和管理事务状态。这可能会导致一些性能损失,特别是在高并发场景下。
- 事务复杂性: Saga模式引入了分布式事务的复杂性,包括事务的分阶段执行和事务状态的管理。这可能需要对业务逻辑有较深入的理解,以确保正确实现各个阶段。
- 不适用于所有场景: Saga模式不适用于所有分布式事务场景,特别是对于需要强一致性和精细控制的业务,可能更适用于其他分布式事务模式。
总的来说,Seata Saga模式在分布式事务管理中具有一些优势,但也伴随着一些挑战。选择是否使用Saga模式应基于业务需求、复杂性和性能等因素进行仔细考虑。在需要处理长时间运行的分布式事务和容错性的场景下,Saga模式是一个可选的分布式事务解决方案。
SeataXA 模式设计思路
前提
- 支持XA 事务的数据库。
- Java 应用,通过 JDBC 访问数据库。
整体机制
在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。
- 执行阶段:
- 可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚
- 持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化 (即,之后任何意外都不会造成无法回滚的情况)
- 完成阶段:
- 分支提交:执行 XA 分支的 commit
- 分支回滚:执行 XA 分支的 rollback
XA模式使用
@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
// DataSourceProxy for AT mode
// return new DataSourceProxy(druidDataSource);
// DataSourceProxyXA for XA mode
return new DataSourceProxyXA(druidDataSource);
}
设置XA模式
seata:
data-source-proxy-mode: XA
Seata XA模式优势确定
优势
- 业务无侵入:和 AT 一样,XA 模式将是业务无侵入的,不给应用设计和开发带来额外负担。
- 支持多种数据库 :XA 协议被主流关系型数据库广泛支持,不需要额外的适配即可使用
- 支持多种语言
缺点:
会有阻塞模式 ,降低性能