简介

1、什么是 Seata

Seata 是什么? | Apache Seata

seata 是事务,是保持一致性的,在高并发情况下不安全,需要加分布式锁。

2、Seata 的分布式事务机制

如何才能做到不对业务进行侵入的情况下实现分布式事务呢?

Seata客户端是通过对数据源进行代理实现的,使用的是 DataSourceProxy类,所以在程序这边,我们只需要将对应的代理类注册为 Bean即可。

2.1、架构图

  • RM(Resource Manager):资源管理者,管理分支事务处理的资源,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。用于直接执行本地事务的提交和回滚。

  • TM(Transaction Manager):事务管理者,定义全局事务的范围、开始全局事务、提交或回滚全局事务。TM 是分布式事务的核心管理者。比如现在我们需要在借阅服务中开启全局事务,来让其自身、图书服务、用户服务都参与进来,也就是说一般全局事务发起者就是 TM。

  • TC(Transaction Coordinator):事务协调者,维护全局和分支事务的状态,协调全局事务提交或回滚,这个就是我们的 Seata服务器,用于全局控制,比如在 XA模式下就是一个协调者的角色,而一个分布式事务的启动就是由 TM 向 TC 发起请求,TC 再来与其他的 RM 进行协调操作。 TM 请求 TC,从而开启一个全局事务,TC 会生成一个 XID 作为该全局事务的编号,XID 会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起;

RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联;

TM 请求 TC,告知 TC 对应 XID 的全局事务是进行提交还是回滚;然后 TC 驱动 RM 将对应 XID 的自己的本地事务进行提交还是回滚;

2.2、Seata 支持 4种事务模式

官方文档:Seata 是什么? | Apache Seata

2.2.1、AT模式

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

一阶段,Seata 会拦截“业务SQL”,首先解析 SQL语义,找到“业务SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

二阶段如果确认提交的话,因为“业务SQL”在一阶段已经提交至数据库, 所以 Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。当然如果发送错误,需要回滚,那么就用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和“after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

  • 阶段一RM的工作:

  1. 注册分支事务

  2. 记录 undo_log(数据快照,分为前镜像 before image 和后镜像 after image)

  3. 执行业务sql 并提交

  4. 报告事务状态

  • 阶段二提交时RM的工作:

  1. 删除 undo_log 即可

  • 阶段二回滚时RM的工作:

  1. 根据 undo_log 恢复数据到更新前

2.2.2、TCC模式

  • Try:资源的检测和预留;

  • Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。

  • Cancel:预留资源释放,可以理解为try的反向操作。

2.2.3、XA模式

要求数据库支持 XA模式才可以。

  • RM一阶段工作:

  1. 注册分支事务到 TC

  2. 执行业务SQL 但不提交

  3. 向 TC 报告事务状态

  • TC二阶段工作:

  1. 检查各分支事务执行状态,如果都成功,则通知所有 RM 提交事务;如果有失败,则通知所有 RM 回滚事务

  • RM三阶段工作:

  1. 接收 TC 的命令,提交或回滚事务

2.2.4、Saga模式

Sega模式用于处理长事务,每个执行者需要实现事务的正向操作和补偿操作。也分成两阶段。

  • 一阶段:直接提交本地事务

  • 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚

优点:

  • 事务参与者可以基于事件驱动实现异步调用,吞吐高

  • 一阶段直接提交事务,无锁,性能好

  • 不用编写 TCC 中的三个阶段,实现简单 缺点:

  • 软状态持续时间不确定,时效性差

  • 没有锁,没有事务隔离,会有脏写

2.2.5、四种模式对比

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

2.3、事务分组机制

事务分组就是 seata 的资源逻辑,可以按微服务的需要,在应用程序(客户端)对自行定义事务分组,每组取一个名字。

集群:seata-server服务端一个或多个节点组成的集群cluster。 应用程序(客户端)使用时需要指定 事务逻辑分组 与 Seata服务端集群(默认为default)的映射关系。

2.3.1、设置事务分组再映射到集群的原因

为啥要设计成通过事务分组再直接映射到集群?干嘛不直接指定集群呢?为什么是获取事务分组到映射集群的配置呢?

这样设计后,事务分组可以作为资源的逻辑隔离单位,出现某集群故障时可以快速 failover,只切换对应分组,可以把故障缩减到服务级别,但前提也是你有足够 server集群。

3、下载 Seata

Seata服务端下载:Releases · apache/incubator-seata · GitHub

源码:https://github.com/seata/seata/archive/refs/heads/develop.zip

3.1、部署

普通启动类修改环境变量是在 环境 -> 环境变量 写入 server.port=9999

Seata服务端支持本地部署或是基于注册发现中心部署(比如 Nacos、Eureka 等)。

本地部署的话不需要对 Seata 的配置文件做任何修改。

3.1.1、文件模式部署

下载完之后,可以直接双击 seata\seata-server-1.4.2\bin\seata-server.bat 运行。也可以放进 idea 中配置端口运行。

  1. 把文件夹放入 idea 中

  2. 下拉选择 Edit Configuration(编辑配置...),

  3. 点击 + 号,选择 Shell Script

  4. 名字改为 Seata Server;脚本路径选择 seata\seata-server-1.4.2\bin\seata-server.bat 的路径;脚本选项 -p 8868,默认端口是 8091;解释器路径 /bin/bash

3.1.2、nacos模式部署

  1. Seata 需要在 nacos 的一个命名空间下,当然也可以为 Seata 专门创一个命名空间 seata

  2. 修改 seata\seata-server-1.4.2\conf\registry.conf,

# 注册
registry {
  type = "nacos"
  
  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = "要填写的是命名空间ID"
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}
​
# 配置
config {
  type = "nacos"
​
  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = "要填写的是命名空间ID"
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties"
  }
}
  1. 然后我们需要把配置导入到 nacos 中,方便做管理和更新。源码的 script/config-center/nacos 下有上传文件,但是没有 Windows 下的 .bat文件,Linux 下使用 .sh文件。tenant 就是 nacos 的命名空间ID。WIndows 下的配置上传 nacos 可以参考 seata 整合 nacos(windows)_seata整合nacos-CSDN博客

  2. 因为修改了 Seata的部署方式,所以需要修改微服务的配置文件。从上面的旧方式变成下面的新方式。

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
​
seata:
  # 注册
  registry:
    # 使用 Nacos,默认的是 file
    type: nacos
    nacos:
      # 使用 Seata 的命名空间,这样才能正确找到 Seata服务,
      # SEATA_GROUP,是服务端的配置文件中的默认值,就不用配了
      # group: 
      namespace: 89fc2145-4676-48b8-9edd-29e867879bcb
      username: nacos
      password: nacos
  # 配置
  config:
    type: nacos
    nacos:
      namespace: 89fc2145-4676-48b8-9edd-29e867879bcb
      username: nacos
      password: nacos
  1. 因为 Seata客户端都添加了配置 service: vgroup-mapping,这是事务组和集群的映射,现在我们需要在 nacos 上添加对应的事务组映射配置,DataId 的格式为service.vgroupMapping.事务组名称。每一个微服务都需要添加一个单独的 nacos配置。

3.2、修改事务会话信息的存储方式

默认使用的是 file方式,会自动创建 file_store、sessionStore文件夹。

如果使用的是 nacos模式部署,就修改配置文件中的 store.mode、store.session.mode,配置内容都改成 db。

3.3、修改数据库信息的配置并创建数据库

3.3.1、修改配置

要修改 数据库驱动、数据库URL、数据库用户名密码,

如果使用的是 nacos模式部署,就修改配置文件中的:

store.db.driverClassName配置文件
配置内容:com.mysql.cj.jdbc.Driver
​
store.db.url配置文件
配置内容:改成 Seata对应的数据库
​
store.db.user、store.db.password 这两个配置文件

3.3.2、创建库表

3.3.2.1、创建语句
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`)
) 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 ('HandleAllSession', ' ', 0);
3.3.2.2、修改字段长度

如果 Seata服务端出现报错,可能是我们自定义事务组的名称太长了,将 globle_table表的字段transaction_server_group 长度适当增加一下即可。

4、简单使用

4.1、实例1

这样的 Seata,在高并发下不安全。可以考虑加锁或使用消息中间件。

4.1.1、将各个服务作为 Seata 的客户端

在每个服务中都添加以下设置。

4.1.1.1、引入依赖
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
4.1.1.2、添加配置

引入依赖、添加配置之后,启动就可以让服务连接到 Seata服务器了,但是还没有开启分布式事务。

seata:
  # 自定义分组名称
  # tx-server-group: 
  service:
    vgroup-mapping:
      # 对事务组做映射,默认的分组名为 应用名称-seata-service-group,将其映射到 default集群
      # 配置错误会找不到服务,所以要注意所在微服务项目的服务名称(应用名称)
      book-service-seata-service-group: default
    grouplist:
      default: localhost:8868
4.1.1.3、启动类添加注解

可以通过配置文件让 Seata 进行自动代理,但是好像写上了配置但不生效。

这样启动后,微服务的 Seata客户端就会产生 RM、TM 去注册到 Seata服务端的 TC 上。

@EnableAutoDataSourceProxy
@SpringBootApplication
public class BookApplication {
    public static void main(String[] args) {
        SpringApplication.run(BookApplication.class, args);
    }
}
4.1.1.4、对业务方法添加注解,开启分布式事务

在 impl类的需要开启分布式事务的方法上,添加 @GlobalTransactional注解,那么就会保证这个方法的正确执行。RootContext.getXID() 可以得到一致的 XID。

@GlobalTransactional
@Override
public boolean doBorrow(int uid, int bid) {
    // 这里打印一下 XID 看看,如果打印的是同一个XID,表示使用的就是同一个事务
    System.out.println(RootContext.getXID());
    // 业务处理,远程调用
    // 若抛出异常,就会进行回滚
    return true;
}
4.1.1.5、创建 undo_log数据表

因为默认使用 XA模式,Seata 会分析修改数据的 sql语句,同时生成对应的反向回滚SQL,这个回滚记录会存放在 undo_log表中。所以要求每一个 Seata客户端都有一个对应的 undo_log表,即每个服务连接的数据库都需要创建这样一个表。

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;

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐