[TOC]

Seata 是什么?

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

Seata 整体工作流程

Seata 对分布式事务的协调和控制,主要是通过 XID 和 3 个核心组件实现的。

XID

XID 是全局事务的唯一标识,它可以在服务的调用链路中传递,绑定到服务的事务上下文中。

核心组件

Seata 定义了 3 个核心组件:

  • TC(Transaction Coordinator):事务协调器,它是事务的协调者(这里指的是 Seata 服务器),主要负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚。
  • TM(Transaction Manager):事务管理器,它是事务的发起者,负责定义全局事务的范围,并根据 TC 维护的全局事务和分支事务状态,做出开始事务、提交事务、回滚事务的决议。
  • RM(Resource Manager):资源管理器,它是资源的管理者(这里可以将其理解为各服务使用的数据库)。它负责管理分支事务上的资源,向 TC 注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚。

以上三个组件相互协作,TC 以 Seata 服务器(Server)形式独立部署,TM 和 RM 则是以 Seata Client 的形式集成在微服务中运行,其整体工作流程如下图。

img

Seata 的整体工作流程如下:

  1. TM 向 TC 申请开启一个全局事务,全局事务创建成功后,TC 会针对这个全局事务生成一个全局唯一的 XID;
  2. XID 通过服务的调用链传递到其他服务;
  3. RM 向 TC 注册一个分支事务,并将其纳入 XID 对应全局事务的管辖;
  4. TM 根据 TC 收集的各个分支事务的执行结果,向 TC 发起全局事务提交或回滚决议;
  5. TC 调度 XID 下管辖的所有分支事务完成提交或回滚操作。

四种模式

Seata 提供了 AT、TCC、SAGA 和 XA 四种事务模式,可以快速有效地对分布式事务进行控制。

在这四种事务模式中使用最多,最方便的就是 AT 模式。与其他事务模式相比,AT 模式可以应对大多数的业务场景,且基本可以做到无业务入侵,开发人员能够有更多的精力关注于业务逻辑开发。

AT 模式

AT: Automatic Transaction

AT 模式的前提

任何应用想要使用 Seata 的 AT 模式对分布式事务进行控制,必须满足以下 2 个前提:

  • 必须使用支持本地 ACID 事务特性的关系型数据库,例如 MySQL、Oracle 等;
  • 应用程序必须是使用 JDBC 对数据库进行访问的 JAVA 应用。

整体机制

两阶段提交协议的演变:

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

工作机制

以一个示例来说明整个 AT 分支的工作过程。

业务表:product

FieldTypeKey
idbigint(20)PRI
namevarchar(100)
sincevarchar(100)

AT 分支事务的业务逻辑:

update product set name = 'GTS' where name = 'TXC';

一阶段

过程:

  1. 解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息。
  2. 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
select id, name, since from product where name = 'TXC';

得到前镜像:

idnamesince
1TXC2014
  1. 执行业务 SQL:更新这条记录的 name 为 ‘GTS’。

  2. 查询后镜像:根据前镜像的结果,通过 主键 定位数据。

select id, name, since from product where id = 1;

得到后镜像:

idnamesince
1GTS2014
  1. 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。
{
    "branchId": 641789253,
    "undoItems": [{
        "afterImage": {
            "rows": [{
                "fields": [{
                    "name": "id",
                    "type": 4,
                    "value": 1
                }, {
                    "name": "name",
                    "type": 12,
                    "value": "GTS"
                }, {
                    "name": "since",
                    "type": 12,
                    "value": "2014"
                }]
            }],
            "tableName": "product"
        },
        "beforeImage": {
            "rows": [{
                "fields": [{
                    "name": "id",
                    "type": 4,
                    "value": 1
                }, {
                    "name": "name",
                    "type": 12,
                    "value": "TXC"
                }, {
                    "name": "since",
                    "type": 12,
                    "value": "2014"
                }]
            }],
            "tableName": "product"
        },
        "sqlType": "UPDATE"
    }],
    "xid": "xid:xxx"
}
  1. 提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁
  2. 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
  3. 将本地事务提交的结果上报给 TC。
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. 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
  2. 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
  3. 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。
  4. 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
update product set name = 'TXC' where id = 1;
  1. 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。

第三点中是无法处理的情况, seata使用 @GlobalLock+@Transactional@GlobalTransaction 来加了全局锁, 在回滚完成前, 其他请求将被阻塞, 如果真的出现了这种情况, 可自定义实现FailureHandler做邮件通知或其他

Seata常见问题

脏写导致数据回滚失败?

二阶段-提交

  1. 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
  2. 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。

总结:

  1. 第一阶段是把 所有要修改的数据存在seata的UNDO_LOG表里(修改前的数据),并拿到全局锁

  2. 第二阶段 看执行结果决定是回滚还是提交

​ 2.1 如果是回滚: 则把UNDO_LOG里对应的数据写回去

​ 2.2 如果是提交: 则删除UNDO_LOG中的数据

TCC 模式

什么是 TCC

TCC 是分布式事务中的二阶段提交协议,它的全称为 Try-Confirm-Cancel,即资源预留(Try)、确认操作(Confirm)、取消操作(Cancel),他们的具体含义如下:

  1. Try:对业务资源的检查并预留;
  2. Confirm:对业务处理进行提交,即 commit 操作,只要 Try 成功,那么该步骤一定成功;
  3. Cancel:对业务处理进行取消,即回滚操作,该步骤回对 Try 预留的资源进行释放。

TCC 是一种侵入式的分布式事务解决方案,以上三个操作都需要业务系统自行实现,对业务系统有着非常大的入侵性,设计相对复杂,但优点是 TCC 完全不依赖数据库,能够实现跨数据库、跨应用资源管理,对这些不同数据访问通过侵入式的编码方式实现一个原子操作,更好地解决了在各种复杂业务场景下的分布式事务问题。

Seata TCC 模式

Seata TCC 模式跟通用型 TCC 模式原理一致,我们先来使用 Seata TCC 模式实现一个分布式事务:

假设现有一个业务需要同时使用服务 A 和服务 B 完成一个事务操作,我们在服务 A 定义该服务的一个 TCC 接口:

public interface TccActionOne {
    @TwoPhaseBusinessAction(name = "DubboTccActionOne", commitMethod = "commit", rollbackMethod = "rollback")
    public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "a") String a);

    public boolean commit(BusinessActionContext actionContext);

    public boolean rollback(BusinessActionContext actionContext);
}

同样,在服务 B 定义该服务的一个 TCC 接口:

public interface TccActionTwo {
    @TwoPhaseBusinessAction(name = "DubboTccActionTwo", commitMethod = "commit", rollbackMethod = "rollback")
    public void prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "b") String b);

    public void commit(BusinessActionContext actionContext);

    public void rollback(BusinessActionContext actionContext);
}

在业务所在系统中开启全局事务并执行服务 A 和服务 B 的 TCC 预留资源方法:

@GlobalTransactional
public String doTransactionCommit(){
    //服务A事务参与者
    tccActionOne.prepare(null,"one");
    //服务B事务参与者
    tccActionTwo.prepare(null,"two");
}

以上就是使用 Seata TCC 模式实现一个全局事务的例子,可以看出,TCC 模式同样使用 @GlobalTransactional 注解开启全局事务,而服务 A 和服务 B 的 TCC 接口为事务参与者,Seata 会把一个 TCC 接口当成一个 Resource,也叫 TCC Resource。

TCC 接口可以是 RPC,也可以是 JVM 内部调用,意味着一个 TCC 接口,会有发起方和调用方两个身份,以上例子,TCC 接口在服务 A 和服务 B 中是发起方,在业务所在系统中是调用方。如果该 TCC 接口为 Dubbo RPC,那么调用方就是一个 dubbo:reference,发起方则是一个 dubbo:service。

img

Seata 启动时会对 TCC 接口进行扫描并解析,如果 TCC 接口是一个发布方,则在 Seata 启动时会向 TC 注册 TCC Resource,每个 TCC Resource 都有一个资源 ID;如果 TCC 接口时一个调用方,Seata 代理调用方,与 AT 模式一样,代理会拦截 TCC 接口的调用,即每次调用 Try 方法,会向 TC 注册一个分支事务,接着才执行原来的 RPC 调用。

当全局事务决议提交/回滚时,TC 会通过分支注册的的资源 ID 回调到对应参与者服务中执行 TCC Resource 的 Confirm/Cancel 方法。

如何控制异常

在 TCC 模型执行的过程中,还可能会出现各种异常,其中最为常见的有空回滚、幂等、悬挂等。下面我讲下 Seata 是如何处理这三种异常的。

如何处理空回滚

什么是空回滚?

空回滚指的是在一个分布式事务中,在没有调用参与方的 Try 方法的情况下,TM 驱动二阶段回滚调用了参与方的 Cancel 方法。

那么空回滚是如何产生的呢?

img

如上图所示,全局事务开启后,参与者 A 分支注册完成之后会执行参与者一阶段 RPC 方法,如果此时参与者 A 所在的机器发生宕机,网络异常,都会造成 RPC 调用失败,即参与者 A 一阶段方法未成功执行,但是此时全局事务已经开启,Seata 必须要推进到终态,在全局事务回滚时会调用参与者 A 的 Cancel 方法,从而造成空回滚。

要想防止空回滚,那么必须在 Cancel 方法中识别这是一个空回滚,Seata 是如何做的呢?

Seata 的做法是新增一个 TCC 事务控制表,包含事务的 XID 和 BranchID 信息,在 Try 方法执行时插入一条记录,表示一阶段执行了,执行 Cancel 方法时读取这条记录,如果记录不存在,说明 Try 方法没有执行。

如何处理幂等

幂等问题指的是 TC 重复进行二阶段提交,因此 Confirm/Cancel 接口需要支持幂等处理,即不会产生资源重复提交或者重复释放。

那么幂等问题是如何产生的呢?

img

如上图所示,参与者 A 执行完二阶段之后,由于网络抖动或者宕机问题,会造成 TC 收不到参与者 A 执行二阶段的返回结果,TC 会重复发起调用,直到二阶段执行结果成功。

Seata 是如何处理幂等问题的呢?

同样的也是在 TCC 事务控制表中增加一个记录状态的字段 status,该字段有 3 个值,分别为:

  1. tried:1
  2. committed:2
  3. rollbacked:3

二阶段 Confirm/Cancel 方法执行后,将状态改为 committed 或 rollbacked 状态。当重复调用二阶段 Confirm/Cancel 方法时,判断事务状态即可解决幂等问题。

如何处理悬挂

悬挂指的是二阶段 Cancel 方法比 一阶段 Try 方法优先执行,由于允许空回滚的原因,在执行完二阶段 Cancel 方法之后直接空回滚返回成功,此时全局事务已结束,但是由于 Try 方法随后执行,这就会造成一阶段 Try 方法预留的资源永远无法提交和释放了。

那么悬挂是如何产生的呢?

img

如上图所示,在执行参与者 A 的一阶段 Try 方法时,出现网路拥堵,由于 Seata 全局事务有超时限制,执行 Try 方法超时后,TM 决议全局回滚,回滚完成后如果此时 RPC 请求才到达参与者 A,执行 Try 方法进行资源预留,从而造成悬挂。

Seata 是怎么处理悬挂的呢?

在 TCC 事务控制表记录状态的字段 status 中增加一个状态:

  1. suspended:4

当执行二阶段 Cancel 方法时,如果发现 TCC 事务控制表有相关记录,说明二阶段 Cancel 方法优先一阶段 Try 方法执行,因此插入一条 status=4 状态的记录,当一阶段 Try 方法后面执行时,判断 status=4 ,则说明有二阶段 Cancel 已执行,并返回 false 以阻止一阶段 Try 方法执行成功。

Saga模式

Saga 模式是一种补偿协议。在 Saga 模式中,在分布式事务内有多个参与者,每个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向和逆向回滚操作。 如下图所示,T1~T3 都是正向的业务流程,都对应着一个冲正逆向操作 C1~C3。

img

在分布式事务执行过程中,会依次执行各参与者的正向操作:

  • 如果所有正向操作均执行成功,则分布式事务提交;

  • 如果任何一个正向操作执行失败,则分布式事务会退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。

Saga 模式的优势:

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

Saga 模式也存在很明显的缺点:在一阶段已经提交了本地数据库事务,且没有进行"预留"动作,所以不能保证隔离性,不容易进行并发控制。与 AT 模式和 TCC 模式相比,Saga 模式的适用场景有限。

A模式

在 XA 模式中,需要在 Seata 定义的分布式事务范围内,利用事务资源实现对 XA 协议的支持,以 XA 协议的机制来管理分支事务。

本质上,Seata 的 AT、TCC、Saga模式都是补偿型的。事务处理机制构建在框架或应用中。事务资源本身对分布式事务是无感知的。而在 XA 模式下,事务资源对分布式事务是可感知的。

Seata:Spring Cloud Alibaba分布式事务组件(非常详细) (biancheng.net)

Seata 中文官网

深度剖析 Seata TCC 模式(一)

Seata之模式简介 - 带翅膀的猫 (chengpengper.cn)

动手实践Seata四种模式(XA、AT、TCC、SAGA)_小钟要学习!!!的博客-CSDN博客_seata xa 模式