分布式事务组件Seata基础知识

分布式事务组件Seata基础知识

简介

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

​ 在分布式系统下,一个业务可能会包含多个服务的调用,每个服务都是一个分支事务,要保障所有分支事务状态最终一致,就需要实现分布式事务。

​ 以下是Seata中分布式事务的领域模型图。在Seata中,每一个分布式事务都拥有一个ID,叫做XID,事务管理器(TM)通过RPC至事务协调者(TC)创建全局事务(Global Transaction),将TC生成的XID传递至其TM所调用的任意资源管理者(RM)中,RM通过其接收到的XID,将其所管理的资源且被该调用所使用到的资源注册为一个事务分支(Branch Transaction),当该请求的调用链全部结束时TM将事务的决议结果(Commit/Rollback)通知TC,TC将协调所有RM进行事务的二阶段动作。

image-20241231104116230

  • TC (Transaction Coordinator) - 事务协调者(Seata服务本身);维护全局和分支事务的状态,驱动全局事务提交或回滚。

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

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

分布式系统概述

​ 分布式事务必然在分布式系统中才会产生,也就必然涉及到分布式系统的共同特点,接下来将介绍分布式系统中的CAP定理和BASE理论。

CAP定理

​ CAP 定理指出,在一个分布式系统中,不可能同时满足以下三个特性(最多同时满足两个):

  • 一致性(Consistency):所有节点在同一时间具有相同的数据副本,对数据的更新操作在所有节点上要么全部成功,要么全部失败,任何时刻读取到的数据都是最新的、一致的。
  • 可用性(Availability):每个请求都能在一定时间内收到一个非错的响应,无论请求是读还是写操作,系统总是可以正常提供服务,不会出现响应超时或无法连接等情况。
  • 分区容错性(Partition Tolerance):当分布式系统中的网络出现分区故障时,即部分节点之间无法通信,系统仍然能够继续运行并提供一定程度的服务,不会因为网络分区而导致整个系统崩溃。

image-20241231111810687

​ 在分布式系统中,网络分区是难以避免的,通常会优先保证分区容错性,然后根据具体业务需求在一致性和可用性之间进行权衡。

BASE理论

​ BASE 是对 CAP 定理中一致性和可用性权衡的延伸,核心思想是通过牺牲强一致性来获得高可用性。

  • 基本可用(Basically Available):指分布式系统在出现故障时,允许损失部分可用性,即系统功能可以部分运行,响应时间可以延长等,但仍然能够满足核心业务的基本需求。
  • 软状态(Soft state):允许系统中的数据存在中间状态,并且该中间状态不会影响系统的整体可用性,即系统中的数据在一定时间内可以是不一致的。
  • 最终一致性(Eventually consistent):强调系统中的数据最终会达到一致状态,虽然在某一时刻可能存在数据不一致的情况,但经过一段时间的同步和协调,数据会逐渐趋于一致。

Seata的四种模式

​ 分布式事务的各个子事务的一致性问题,参考CAP定理和BASE理论可以有以下两种主要的解决思想:

  • AP:各个子事务分别执行和提交,允许出现短暂的不一致现象,然后通过其他措施实现最终一致性
  • CP:各个子事务执行后相互等待,同时进行事务的提交和回滚,在等待时系统处于弱可用状态

​ 基于不同的场景,Seata提供了四种不同的分布式事务模式:

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

Seata服务启动

​ Seata作为分布式事务的事务协调者TC,是需要进行独立部署的,有关于Seata服务的搭建以及项目中如何使用,请查阅官网。

​ 如果Seata的store是DB,需要创建一些表用于管理事务的状态,给出MySQL数据库的建表语句如下:

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
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `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_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);

​ 来源GitHub官网。

image-20241231175036726

XA模式

​ Seata XA 模式是利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式。XA 协议是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准。

XA 模式利用事务资源对 XA 协议的支持:XA协议是由 X/Open 组织定义的一种分布式事务协议,事务资源需自身支持XA协议,符合XA协议的规范

​ XA 模式采用的是两阶段提交(2PC)的机制,分为准备阶段(Prepare Phase)和提交或回滚阶段(Commit/Rollback Phase),流程如下:

  1. TM 开启全局事务
  2. RM 向 TC 注册分支事务、执行 SQL 并向 TC 报告分支事务状态(第一阶段,此时事务处于挂起状态)
  3. TM 申请结束全局事务
  4. TC 向 RM 发送 commit/rollback 请求(第二阶段,根据分支事务的报告情况,选择提交或回滚)

image-20241231143034244

优缺点

  • 优点
    • 强一致性保证:严格遵循 ACID 特性,确保在分布式环境下多个资源的事务操作要么全部成功提交,要么全部回滚,不会出现部分成功部分失败的情况,保证了数据的强一致性。
    • 通用性:XA 模式是一种通用的分布式事务解决方案,适用于各种支持 XA 规范的资源管理器,包括不同类型的数据库、消息队列等,具有较好的兼容性和可扩展性。
  • 缺点
    • 资源锁定时间长:在整个事务执行过程中,资源会被锁定,直到事务完成提交或回滚。如果事务执行时间较长,会导致资源长时间被占用,降低资源的利用率,可能会引发其他性能问题。

使用方式

​ XA 模式对业务是无侵入的,在项目的application.yaml配置文件中配置为XA模式即可,通过@GlobalTransactional注解即可声明分布式事务。

1
2
seata:
data-source-proxy-mode: XA

原理

​ Seata 的 XA和AT 事务模式是对 JDBC 标准接口操作的拦截和增强。通过代理DataSource实现(ORM框架位于JDBC结构的上层,所以支持所有的ORM 框架),实际上这种方式在XA模式上有局限性,不同的厂商、不同版本的数据库驱动实现机制是厂商私有的,无法保证兼容的正确性,这点在 Oracle 上体现非常明显,参见 Druid issue

​ 实际上Seata还提供了另一种方案实现,具体请查阅官网。

image-20241231152630665

AT模式

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

​ AT 模式也是采取的两阶段提交的方式来实现:

  • 一阶段:执行业务SQL,并记录回滚日志(在执行数据库操作前,它会记录数据的原始状态beforeImage,执行操作后,再记录数据的修改后状态afterImage,将这两个状态信息作为回滚日志记录下来),然后直接提交本地事务
  • 二阶段:
    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿

​ AT 模式是最终一致性的,每个事务分支无需同时进行提交和回滚,防止数据库资源的锁定,但若不采取其他措施,并发情况下可能会导致数据错误(多个事务同时操作了相同的数据)。因此,在AT模式中还引入了全局锁,本地事务在提交前,需向TC申请全局锁(细粒度的,根据主键,也就是行级别的),在全局事务提交后释放

写隔离

​ Seata通过全局锁的机制,防止并发下的脏写问题,以一个示例来说明:两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。

  1. 事务提交

    ​ tx1 先开始,开启本地事务,拿到本地锁(其实指的是数据库自身的锁机制),更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待全局锁。

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

    image-20241231161717847

  2. 事务回滚

    ​ 同样的,tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待全局锁。

    ​ tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。此时,如果 tx2 仍在等待该数据的全局锁,那么tx2就持有本地锁,则 tx1 的分支回滚会失败(由于数据库自身的锁机制,此时需要回滚的数据的锁在tx2的本地事务上)。分支的回滚会一直重试,直到 tx2 的全局锁等锁超时,放弃全局锁并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。

    image-20241231162318620

    ​ Seata在回滚时会拿undo_log中的afterImage与当前数据(当前数据的获取根据本地事务的隔离级别)进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改(如果本地事务的隔离级别是读已提交及以上的话,那么一般是由被非Seata管理的事务进行了修改,比如普通的@Transactional事务)。这种情况,需要根据配置策略来做处理(官网上是这么说的,具体怎么配置我没找到)。

隔离级别

​ Seata AT模式的隔离级别默认是读未提交(会发生脏读),这里的隔离级别指的是分布式事务的,而不是本地事务的,比如tx1的全局事务未提交,但是某个事务分支的本地事务已经提交了,那么其他事务能够看到本地事务提交后的数据,这对于全局事务来说是读未提交

​ 如果需要全局事务的读已提交,截止到2.2版本(编写本文时目前最新), Seata 的方式是通过 SELECT FOR UPDATE 语句的代理实现的,当你使用了SELECT FOR UPDATE 语句,会申请全局锁,如果全局锁被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到全局锁拿到,即读取的相关数据是已提交的,才返回。

image-20241231165653589

优缺点

  • 优点
    • 资源锁定时间短:相较于XA模式,一阶段完成直接提交事务,释放数据库资源,性能比较好。
    • 支持多种数据库:AT模式支持的数据库有MySQL、Oracle、PostgreSQL、 TiDB、MariaDB。
  • 缺点
    • 非强一致性:两阶段之间属于软状态,属于最终一致。

使用方式

​ AT 模式对业务也是无侵入的,但在AT模式下需要申请全局锁,需创建相关的表(在业务库中),来源GitHub官网。

image-20241231175345266

​ 当数据库为MySQL时,建表语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

​ 然后在项目的application.yaml配置文件中配置为XA模式即可,通过@GlobalTransactional注解即可声明分布式事务。

1
2
seata:
data-source-proxy-mode: AT

TCC模式

​ TCC(Try-Confirm-Cancel)模式是 Seata 支持的一种由业务方细粒度控制的侵入式分布式事务解决方案,其分布式事务模型直接作用于服务层,不依赖底层数据库(不依赖于支持ACID事务的数据库),可以灵活选择业务资源的锁定粒度(不像AT模式那样由Seata自动维护全局锁),减少资源锁持有时间,可扩展性好。

​ TCC模式与AT模式非常相似,每阶段都是独立事务,会提交本地事务并释放资源,不同的点主要在于下两点:

  1. 不再由Seata维护全局锁:可以灵活选择业务资源的锁定粒度,业务自行加锁

  2. 需要提供“准备”、“提交”和“回滚” 3 个操作:业务方需要自行定义 TCC 资源的“准备”、“提交”和“回滚”。

    ​ Try 操作作为一阶段,负责资源的检查和预留

    ​ Confirm 操作作为二阶段提交操作,执行真正的业务;

    ​ Cancel 是二阶段回滚操作,执行预留资源的取消,使资源回到初始状态。

隔离级别

​ AT模式通过全局锁的机制能够实现分布式事务的读未提交(默认)和读已提交。但在TCC模式下,是不需要考虑隔离级别的,因为在Try中进行了资源的预留,后续的 Confirm 和 Cancel 都是对Try中预留资源的操作,所操作的资源本身就是隔离的(当然前提是TCC的三个操作都是按照正确的语义写的)。

优缺点

  • 优点

    • 性能极致:在AT模式的基础上,性能更近一步,无需维护回滚日志和全局锁。
    • 不依赖底层数据库:通过补偿机制实现分布式事务,可支持非事务型数据库(如Redis)。
  • 缺点

    • 对业务侵入性强:TCC 是一种侵入式的分布式事务解决方案,需要业务系统自行实现 Try,Confirm,Cancel 三个操作,对业务系统有着非常大的入侵性,设计相对复杂

    • 非强一致性:同AT模式,两阶段之间属于软状态,属于最终一致。

    • 如果使用不当会造成数据错误:在AT模式中会由Seata维护全局锁保障数据的最终一致性,但在TCC模式中,需要自行编码实现 Try,Confirm,Cancel 三个操作以及对资源的锁定(如果有必要的话),如果编码不当,可能会导致数据的错误。

      • 比如Confirm和Cancal操作失败了,此时Seata会进行重试操作,需做好Confirm和Cancal操作的幂等处理

      • 比如某个分支事务执行Try操作阻塞,导致全局事务超时回滚,此时Try操作阻塞的分支事务并未完成Try操作就要执行Cancel操作了,这时Cancel操作不应当对Try当中的资源进行补偿,而是应该进行空回滚

        在进行空回滚后,如果Try操作又继续成功执行,此时的全局事务早已回滚,该分支事务不会再由Seata进行Confirm和Cancal了,此时造成了业务悬挂

      为了解决这些问题,一般会再建立一张表记录分布式事务的状态,来做到Confirm和Cancal操作的幂等处理、根据情况进行空回滚操作以及防止业务悬挂,在Seata1.5.1版本中提供了对这些功能的支持,请见官网博客

使用方式

​ TCC对业务具有较强的侵入性,不同的业务肯定会有不同的逻辑,本文以经典的扣减库存进行举例。

​ 在Seata1.5.1版本中提供了对幂等、悬挂和空回滚的支持,需在Seata Server服务连接的db插入一张事务控制表,MySQL语法的建表语句如下,在声明TCC模式管理的事务的注解@TwoPhaseBusinessAction中设置属性useTCCFence = true来开启这个功能,否则需要业务代码自行实现,如何自行实现请查阅上面给出的官网博客连接查看源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
`xid` VARCHAR(128) NOT NULL COMMENT 'global id',
`branch_id` BIGINT NOT NULL COMMENT 'branch id',
`action_name` VARCHAR(64) NOT NULL COMMENT 'action name',
`status` TINYINT NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
`gmt_create` DATETIME(3) NOT NULL COMMENT 'create time',
`gmt_modified` DATETIME(3) NOT NULL COMMENT 'update time',
PRIMARY KEY (`xid`, `branch_id`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_status` (`status`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

库存Service

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
/*
* 通过 @LocalTCC 这个注解,RM 初始化的时候会向 TC 注册一个分支事务
* 如果 TCC 参与者是本地 bean(非远程RPC服务),本地 TCC bean 还需要在接口定义中添加 @LocalTCC 注解
*
* 以上这两句话是我在官网不同位置找的的,不是很理解,反正加上就完了...
*/
@LocalTCC
public interface StorageService {

/**
* Seata 会把一个 TCC 接口当成一个 Resource,也叫 TCC Resource。
* 在业务接口中核心的注解是 @TwoPhaseBusinessAction,表示当前方法使用 TCC 模式管理事务提交,
* 并标明了 Try,Confirm,Cancel 三个阶段。
* name属性,给当前事务注册了一个全局唯一的的 TCC bean name。同时 TCC 模式的三个执行阶段分别是:
* Try 阶段,预定操作资源(Prepare) 这一阶段所以执行的方法便是被 @TwoPhaseBusinessAction 所修饰的方法。
* Confirm 阶段,执行主要业务逻辑(Commit) 这一阶段使用 commitMethod 属性所指向的方法,来执行Confirm 的工作。
* Cancel 阶段,事务回滚(Rollback) 这一阶段使用 rollbackMethod 属性所指向的方法,来执行 Cancel 的工作。
*
* 该方法的返回值任意,Confirm和Cancel必须返回 boolean 类型
*
* 扣减库存
* @param productId 产品id
* @param count 数量
* @return
*/
@TwoPhaseBusinessAction(name = "storageTcc", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
boolean decrease(@BusinessActionContextParameter(paramName = "productId") Long productId,
@BusinessActionContextParameter(paramName = "count") Integer count);

/**
* 可以在 TCC 模式下使用 BusinessActionContext 在事务上下文中传递查询参数。如下属性:
* xid 全局事务id
* branchId 分支事务id
* actionName 分支资源id,(resource id)
* actionContext 业务传递的参数,可以通过 @BusinessActionContextParameter 来标注需要传递的参数。
*
* 提交事务
* @param actionContext
* @return
*/
boolean commit(BusinessActionContext actionContext);

/**
* 回滚事务
* @param actionContext
* @return
*/
boolean rollback(BusinessActionContext actionContext);
}

库存Service Impl

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
@Service
public class StorageServiceImpl implements StorageService {

@Transactional(rollbackFor = Exception.class)
@Override
public boolean decrease(Long productId, Integer count) {

//扣减库存

return true;
}

@Transactional(rollbackFor = Exception.class)
@Override
public boolean commit(BusinessActionContext actionContext) {

//返回true表示成功,返回false表示失败
return true;
}

@Transactional(rollbackFor = Exception.class)
@Override
public boolean rollback(BusinessActionContext actionContext) {

//补偿库存
Long productId = actionContext.getActionContext("productId", Long.class);
Integer count = actionContext.getActionContext("count", Integer.class);

//返回true表示成功,返回false表示失败
return true;
}
}

业务Service(声明全局事务)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class BusinessServiceImpl implements BusinessService{

@Resource
private StorageService storageService;

@GlobalTransactional(rollbackFor = Exception.class)
public void doBusiness(){
//其他业务

boolean decrease = storageService.decrease(1L, 1);

//其他业务
}
}

SAGA模式

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

  • 一阶段:执行真正的事务逻辑并提交。
  • 二阶段:
    • 成功:啥也不干。
    • 失败:进行补偿。

image-20250101185847399

隔离级别

​ SAGA模式直接提交事务且没有锁机制,无法保障隔离性。

​ 缺乏隔离性的应对:

  • 由于 Saga 事务不保证隔离性,,在极端情况下可能由于脏写无法完成回滚操作,比如举一个极端的例子,分布式事务内先给用户 A 充值,然后给用户 B 扣减余额,如果在给 A 用户充值成功,在事务提交以前,A 用户把余额消费掉了,如果事务发生回滚,这时则没有办法进行补偿了。这就是缺乏隔离性造成的典型的问题,实践中一般的应对方法是:
    • 业务流程设计时遵循“宁可长款,不可短款”的原则,长款意思是客户少了钱机构多了钱,以机构信誉可以给客户退款,反之则是短款,少的钱可能追不回来了。所以在业务流程设计上一定是先扣款。
    • 有些业务场景可以允许让业务最终成功,在回滚不了的情况下可以继续重试完成后面的流程,所以状态机引擎(目前 SEATA 提供的 SAGA 模式是基于状态机引擎来实现的)除了提供“回滚”能力还需要提供“向前”恢复上下文继续执行的能力,让业务最终执行成功,达到最终一致性的目的。

优缺点

  • 优点

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

    • 不保证隔离性

使用方式

​ SAGA 模式的实现方式一般是两种,一种基本状态机或流程引擎通过 DSL 方式编排流程程和补偿定义,一种是基于 Java 注解+拦截器实现补偿,目前 SEATA 提供的 SAGA 模式是基于状态机引擎来实现的,这两种方式的优缺点如下:

  • 状态机+DSL

    • 优点

      - 可以用可视化工具来定义业务流程,标准化,可读性高,可实现服务编排的功能
      - 提高业务分析人员与程序开发人员的沟通效率
      - 业务状态管理:流程本质就是一个状态机,可以很好的反映业务状态的流转
      - 提高异常处理灵活性:可以实现宕机恢复后的“向前重试”或“向后补偿”
      - 天然可以使用 Actor 模型或 SEDA 架构等异步处理引擎来执行,提高整体吞吐量

    • 缺点

      - 业务流程实际是由 JAVA 程序与 DSL 配置组成,程序与配置分离,开发起来比较繁琐
      - 如果是改造现有业务,对业务侵入性高
      - 引擎实现成本高

  • 拦截器+java 注解

    • 优点

      - 程序与注解是在一起的,开发简单,学习成本低
      - 方便接入现有业务
      - 基于动态代理拦截器,框架实现成本低

    • 缺点

      - 框架无法提供 Actor 模型或 SEDA 架构等异步处理模式来提高系统吞吐量
      - 框架无法提供业务状态管理
      - 难以实现宕机恢复后的“向前重试”,因为无法恢复线程上下文

​ 需要注意的是,SAGA模式同TCC模式一样,需要解决接口幂等、允许空回滚以及防止业务悬挂。

​ 具体的使用示例较为复杂,请查阅官网(TODO以后有时间的话或者实际使用到了会更新)。

集群

​ 集群的目的,是做到服务的高可用,简单理解就是集群能够在主节点宕机后继续正常运行,常见的方式是通过部署多个提供相同服务的节点,并通过注册中心实时感知主节点的上下线情况,以便及时切换到可用的节点。在计算与存储分离的架构下,只需将数据存储在共享的存储中间件中,任何一个节点都可以通过访问该公共存储区域获取所有节点操作的事务信息,从而实现高可用的能力。

普通集群

​ 目前Seata的分布式事务数据存储模式有file,db,redis,根据一致性的排名,db 模式下的事务记录可以得到最好的保证,其次是 file 模式的异步刷盘,最后是 redis 模式下的 aof 和 rdb。

  • file 模式是 Seata 自实现的事务存储方式,它以顺序写的形式将事务信息存储到本地磁盘上。为了兼顾性能,默认采用异步方式,并将事务信息存储在内存中,确保内存和磁盘上的数据一致性。当 Seata-Server(TC)意外宕机时,在重新启动时会从磁盘读取事务信息并恢复到内存中,以便继续运行事务上下文。
  • db 是 Seata 的抽象事务存储管理器(AbstractTransactionStoreManager)的另一种实现方式。它依赖于数据库,如 PostgreSQL、MySQL、Oracle 等,在数据库中进行事务信息的增删改查操作。一致性由数据库的本地事务保证,数据也由数据库负责持久化到磁盘。
  • redis 和 db 类似,也是一种事务存储方式。它利用 Jedis 和 Lua 脚本来进行事务的增删改查操作,部分操作(如竞争锁)在 Seata 2.x 版本中全部采用了 Lua 脚本。数据的存储与 db 类似,依赖于存储方(Redis)来保证数据的一致性。与 db 类似,redis 在 Seata 中采用了计算和存储分离的架构设计.

​ file 模式将数据存储在本地磁盘和节点内存中,数据写操作没有任何同步,这意味着目前的 file 模式无法实现高可用,仅支持单机部署。

​ db,redis作为存储时,能做到集群部署,实现高可用。

Raft集群

​ Raft是一种新型易于理解的分布式一致性复制协议,由斯坦福大学的 Diego Ongaro 和 John Ousterhout 提出,Raft协议提供了更完整更清晰的协议描述,并提供了清晰的节点增删描述。 Raft 作为复制状态机,是分布式系统中最核心最基础的组件,提供命令在多个节点之间有序复制和执行,当多个节点初始状态一致的时候,保证节点之间状态一致。

​ Seata-Raft模式的设计思路是通过封装无法高可用的file模式,利用Raft算法实现多个TC之间数据的同步。该模式保证了使用file模式时多个TC的数据一致性,同时将异步刷盘操作改为使用Raft日志和快照进行数据恢复。

其他

@GlobalLock注解

​ 开启全局事务的@GlobalTransactional注解可以在事务提交前,查询全局锁是否存在,以防止AT模式下的脏写,同时通过对SELECT FOR UPDATE 语句的代理可以做到全局事务的读已提交以防止脏读。

​ 这两个机制都是对于@GlobalTransactional注解声明的全局事务才生效的,如果是简单的不涉及到分布式的事务,加上@GlobalTransactional注解开启全局事务会带来额外的性能损耗(比如向 TC 发起开启全局事务等 RPC 过程),但是不开启会发生脏读和脏写问题,为此又提供了一个@GlobalLock注解,该注解只会在执行过程中查询全局锁是否存在,不会去开启全局事务,适用于不涉及到分布式的事务。

​ 加上@GlobalLock注解后,本地事务提交前会检查全局锁,防止脏写,如果使用的是SELECT FOR UPDATE 语句则会在执行 SQL 前检查全局锁是否存在,只有当全局锁完成之后,才能继续执行 SQL,这样就防止了脏读。

事务上下文

​ Seata 的事务上下文由 RootContext 来管理。应用开启一个全局事务后,RootContext 会自动绑定该事务的 XID,事务结束(提交或回滚完成),RootContext 会自动解绑 XID。

​ 应用可以通过 RootContext 的 API 接口来获取当前运行时的全局事务 XID,当获取的XID不为空则表示当前运行在一个全局事务的上下文中。

1
2
// 获取 XID
String xid = RootContext.getXID();