Skip to content

Seata

Seata(Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架)是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。

如下图所示,Seata 中有三大模块,分别是 TMRMTC。 其中 TMRM 是作为 Seata 的客户端与业务系统集成在一起,TC 作为 Seata 的服务端独立部署。

(图片来源:Seata 官网)

  • TC (Transaction Coordinator) - 事务协调者

    维护全局和分支事务的状态,驱动全局事务提交或回滚。

  • TM (Transaction Manager) - 事务管理器

    定义全局事务的范围:开始全局事务、提交或回滚全局事务。

  • RM (Resource Manager) - 资源管理器

    管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

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

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

分布式事务 Seata 解决方案

Seata 会有 4 种分布式事务解决方案,分别是 AT 模式、TCC 模式、Saga 模式和 XA 模式。

AT 模式

AT 模式是一种无侵入的分布式事务解决方案。在 AT 模式下,用户只需关注自己的“业务 SQL”,用户的 “业务 SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作。

(图片来源:Seata 官网)

AT 模式如何做到对业务的无侵入

  • 一阶段:

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

    (图片来源:Seata 官网)

  • 二阶段提交:

    二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

    (图片来源:Seata 官网)

  • 二阶段回滚:

    二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

    (图片来源:Seata 官网)

AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写“业务 SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。

Spring 集成分布式事务框架 Seata(AT 模式)

单机

Seata-Server

Server 端存储模式(store.mode)现有 filedbredis 三种(后续将引入 raftmongodb)。file 模式无需改动,直接启动即可。

注:file 模式为单机模式,全局事务会话信息内存中读写并持久化本地文件 root.data,性能较高。

  1. Releases 页面下载相应版本并解压。

  2. 启动 seata-server。

    • 在 Linux/Mac 下 - ./bin/seata-server.sh
    • 在 Windows 下 - bin\seata-server.bat

    支持的启动参数:

    参数全写作用备注
    -h--host指定在注册中心注册的 IP不指定时获取当前的 IP,外部访问部署在云环境和容器中的 server 建议指定
    -p--post指定 server 启动的端口默认为 8091
    -m--storeMode事务日志存储方式支持 filedbredis,默认为 file。注:redis 需 seata-server 1.3 版本及以上
    -n--serverNode用于指定 seata-server 节点 ID如 1,2,3...,默认为 1
    -e--seataEnv指定 seata-server 运行环境devtest 等,服务启动时会使用 registry-dev.conf 这样的配置

    如:

    sh
    $ ./bin/seata-server.sh -p 8091 -h 127.0.0.1 -m file

Seata-Client

添加 Seata 依赖
  • <dependencyManagement> 中加入 Seata 依赖管理

    xml
    <dependencyManagement>
    	<dependencies>
    		<dependency>
    		    <groupId>com.alibaba.cloud</groupId>
    		    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    		    <version>2021.1</version>
    		    <type>pom</type>
    		    <scope>import</scope>
    		</dependency>
    	</dependencies>
    </dependencyManagement>
  • <dependencies> 中加入 Seata 依赖

    xml
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
undo_log 建表

注:每个业务库中必须包含 undo_log 表,若与分库分表组件联用,分库不分表。

  • PostgreSQL 数据库 undo_log 表结构

    sql
    -- for AT mode you must to init this sql for you business database. the seata server not need it.
    CREATE TABLE IF NOT EXISTS public.undo_log
    (
        id            SERIAL       NOT NULL,
        branch_id     BIGINT       NOT NULL,
        xid           VARCHAR(128) NOT NULL,
        context       VARCHAR(128) NOT NULL,
        rollback_info BYTEA        NOT NULL,
        log_status    INT          NOT NULL,
        log_created   TIMESTAMP(0) NOT NULL,
        log_modified  TIMESTAMP(0) NOT NULL,
        CONSTRAINT pk_undo_log PRIMARY KEY (id),
        CONSTRAINT ux_undo_log UNIQUE (xid, branch_id)
    );
    
    CREATE SEQUENCE IF NOT EXISTS undo_log_id_seq INCREMENT BY 1 MINVALUE 1 ;
项目配置
  • application.properties

    properties
    spring.cloud.alibaba.seata.tx-service-group=my_test_tx_group
    
    seata.enabled=true
    
    seata.registry.type=file
    seata.config.type=file
    seata.service.grouplist.default=127.0.0.1:8091
数据源配置
  • Druid

    java
    import javax.sql.DataSource;
    
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import com.alibaba.druid.pool.DruidDataSource;
    
    @Configuration
    public class DataSourceConfiguration {
        
        @ConfigurationProperties(prefix = "spring.datasource")
        @Bean
        public DataSource dataSource() {
            DruidDataSource druidDataSource = new DruidDataSource();
            return druidDataSource;
        }
    }
开启分布式事务
  • 只需要使用一个 @GlobalTransactional 注解在业务方法上

    java
    import io.seata.spring.annotation.GlobalTransactional;
    
    @GlobalTransactional
    public void purchase(String userId, String commodityCode, int count) {
    	storageFeignClient.deduct(commodityCode, count);
    	
    	orderFeignClient.create(userId, commodityCode, count);
    	
    	storageFeignClient.validStorageData();
    }

集群

Seata 的高可用依赖于注册中心、配置中心和数据库来实现。本次部署使用 Nacos 作为注册中心和配置中心,使用 PostgreSQL 作为存储事务数据的数据库。

Nacos

  1. Releases 页面下载相应版本并解压。

  2. 直接启动( standalone 代表着单机模式运行,非集群模式)。

    • 在 Linux/Mac 下 - ./bin/startup.sh -m standalone
    • 在 Windows 下 - bin\startup.cmd -m standalone
  3. 启动端口默认 8848,访问路径默认 /nacos

    (图片来源:自己截得)

  4. 进入 Nacos 控制台,用户名和密码默认都是 nacos

    (图片来源:自己截得)

    (图片来源:自己截得)

Seata-Server

Server 端存储模式(store.mode)现有 filedbredis 三种(后续将引入 raftmongodb)。db 模式为高可用模式,全局事务会话信息通过 db 共享,相应性能差些。

  1. 创建 db 模式需要的表。

    全局事务会话信息由 3 块内容构成,全局事务-->分支事务-->全局锁,对应表 global_tablebranch_tablelock_table

    sql
    -- -------------------------------- The script used when storeMode is 'db' --------------------------------
    -- the table to store GlobalSession data
    CREATE TABLE IF NOT EXISTS public.global_table
    (
        xid                       VARCHAR(128) NOT NULL,
        transaction_id            BIGINT,
        status                    SMALLINT     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                TIMESTAMP(0),
        gmt_modified              TIMESTAMP(0),
        CONSTRAINT pk_global_table PRIMARY KEY (xid)
    );
    
    CREATE INDEX idx_gmt_modified_status ON public.global_table (gmt_modified, status);
    CREATE INDEX idx_transaction_id ON public.global_table (transaction_id);
    
    -- the table to store BranchSession data
    CREATE TABLE IF NOT EXISTS public.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            SMALLINT,
        client_id         VARCHAR(64),
        application_data  VARCHAR(2000),
        gmt_create        TIMESTAMP(6),
        gmt_modified      TIMESTAMP(6),
        CONSTRAINT pk_branch_table PRIMARY KEY (branch_id)
    );
    
    CREATE INDEX idx_xid ON public.branch_table (xid);
    
    -- the table to store lock data
    CREATE TABLE IF NOT EXISTS public.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),
        gmt_create     TIMESTAMP(0),
        gmt_modified   TIMESTAMP(0),
        CONSTRAINT pk_lock_table PRIMARY KEY (row_key)
    );
    
    CREATE INDEX idx_branch_id ON public.lock_table (branch_id);
    
    CREATE TABLE distributed_lock (
        lock_key     VARCHAR(20)  NOT NULL,
        lock_value        VARCHAR(20)  NOT NULL,
        expire       BIGINT       NOT NULL,
        CONSTRAINT pk_distributed_lock_table PRIMARY KEY (lock_key)
    );
    
    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);

    同时,也需要创建 undo_log 表。

    sql
    -- for AT mode you must to init this sql for you business database. the seata server not need it.
    CREATE TABLE IF NOT EXISTS public.undo_log
    (
        id            SERIAL       NOT NULL,
        branch_id     BIGINT       NOT NULL,
        xid           VARCHAR(128) NOT NULL,
        context       VARCHAR(128) NOT NULL,
        rollback_info BYTEA        NOT NULL,
        log_status    INT          NOT NULL,
        log_created   TIMESTAMP(0) NOT NULL,
        log_modified  TIMESTAMP(0) NOT NULL,
        CONSTRAINT pk_undo_log PRIMARY KEY (id),
        CONSTRAINT ux_undo_log UNIQUE (xid, branch_id)
    );
    
    CREATE SEQUENCE IF NOT EXISTS undo_log_id_seq INCREMENT BY 1 MINVALUE 1 ;
  2. 配置导入 Nacos。

    为了方便管理 Seata 配置,在 Nacos 控制台建立一个 seata 命名空间。

    (图片来源:自己截得)

    记下命名空间 ID。

    (图片来源:自己截得)

    调整 config.txt 内容中。

    properties
    service.vgroupMapping.my_test_tx_group=default
    store.mode=db
    store.db.datasource=druid
    store.db.dbType=postgresql
    store.db.driverClassName=org.postgresql.Driver
    store.db.url=jdbc:postgresql://localhost:5432/postgres?currentSchema=seata
    store.db.user=postgres
    store.db.password=postgres

    (图片来源:自己截得)

    执行 nacos-config.sh 脚本。

    sh
    $ ./nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t f66d1e4b-003a-4247-8612-bcb3beb37ef3

    参数描述:https://github.com/seata/seata/tree/develop/script/config-center

    提示

    执行 nacos-config.sh 脚本时需要 config.txt 在脚本文件的父目录下。

    (图片来源:自己截得)

    config.txt 里的配置已同步到 Nacos 中。

    (图片来源:自己截得)

  3. Releases 页面下载相应版本并解压。

  4. 修改 seata-server 中 registry.conf 的注册中心配置

    config
    registry {
      type = "nacos"
    
      nacos {
        application = "seata-server"
        serverAddr = "127.0.0.1:8848"
        group = "SEATA_GROUP"
        namespace = "f66d1e4b-003a-4247-8612-bcb3beb37ef3"
        cluster = "default"
        username = ""
        password = ""
      }
    }
    
    config {
      type = "nacos"
        
      nacos {
        serverAddr = "127.0.0.1:8848"
        namespace = "f66d1e4b-003a-4247-8612-bcb3beb37ef3"
        group = "SEATA_GROUP"
        username = ""
        password = ""
      }
    }
  5. 启动 seata-server。

    • 在 Linux/Mac 下 - ./bin/seata-server.sh
    • 在 Windows 下 - bin\seata-server.bat

    支持的启动参数:

    参数全写作用备注
    -h--host指定在注册中心注册的 IP不指定时获取当前的 IP,外部访问部署在云环境和容器中的 server 建议指定
    -p--post指定 server 启动的端口默认为 8091
    -m--storeMode事务日志存储方式支持 filedbredis,默认为 file。注:redis 需 seata-server 1.3 版本及以上
    -n--serverNode用于指定 seata-server 节点 ID如 1,2,3...,默认为 1
    -e--seataEnv指定 seata-server 运行环境devtest 等,服务启动时会使用 registry-dev.conf 这样的配置

    如:

    sh
    $ ./bin/seata-server.sh -p 8091 -h 127.0.0.1 -m db -n 1
  6. 查看是否注册到 Nacos。

    (图片来源:自己截得)

  7. 启动多个 seata-server 实例。

    sh
    $ ./bin/seata-server.sh -p 8091 -h 127.0.0.1 -m db -n 2
    $ ./bin/seata-server.sh -p 8091 -h 127.0.0.1 -m db -n 3
    ...

Seata-Client

添加 Seata 依赖
  • <dependencyManagement> 中加入 Seata 依赖管理

    xml
    <dependencyManagement>
    	<dependencies>
    		<dependency>
    		    <groupId>com.alibaba.cloud</groupId>
    		    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    		    <version>2021.1</version>
    		    <type>pom</type>
    		    <scope>import</scope>
    		</dependency>
    	</dependencies>
    </dependencyManagement>
  • <dependencies> 中加入 Seata 依赖

    xml
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.nacos</groupId>
        <artifactId>nacos-client</artifactId>
    </dependency>
undo_log 建表

注:每个业务库中必须包含 undo_log 表,若与分库分表组件联用,分库不分表。

  • PostgreSQL 数据库 undo_log 表结构

    sql
    -- for AT mode you must to init this sql for you business database. the seata server not need it.
    CREATE TABLE IF NOT EXISTS public.undo_log
    (
        id            SERIAL       NOT NULL,
        branch_id     BIGINT       NOT NULL,
        xid           VARCHAR(128) NOT NULL,
        context       VARCHAR(128) NOT NULL,
        rollback_info BYTEA        NOT NULL,
        log_status    INT          NOT NULL,
        log_created   TIMESTAMP(0) NOT NULL,
        log_modified  TIMESTAMP(0) NOT NULL,
        CONSTRAINT pk_undo_log PRIMARY KEY (id),
        CONSTRAINT ux_undo_log UNIQUE (xid, branch_id)
    );
    
    CREATE SEQUENCE IF NOT EXISTS undo_log_id_seq INCREMENT BY 1 MINVALUE 1 ;
项目配置
  • application.properties

    properties
    seata.registry.type=nacos
    seata.registry.nacos.application=seata-server
    seata.registry.nacos.server-addr=127.0.0.1:8848
    seata.registry.nacos.group=SEATA_GROUP
    seata.registry.nacos.namespace=f66d1e4b-003a-4247-8612-bcb3beb37ef3
    seata.registry.nacos.username=
    seata.registry.nacos.password=
    
    seata.config.type=nacos
    seata.config.nacos.namespace=f66d1e4b-003a-4247-8612-bcb3beb37ef3
    seata.config.nacos.server-addr=127.0.0.1:8848
    seata.config.nacos.group=SEATA_GROUP
    seata.config.nacos.username=
    seata.config.nacos.password=
数据源配置
  • Druid

    java
    import javax.sql.DataSource;
    
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import com.alibaba.druid.pool.DruidDataSource;
    
    @Configuration
    public class DataSourceConfiguration {
        
        @ConfigurationProperties(prefix = "spring.datasource")
        @Bean
        public DataSource dataSource() {
            DruidDataSource druidDataSource = new DruidDataSource();
            return druidDataSource;
        }
    }
开启分布式事务
  • 只需要使用一个 @GlobalTransactional 注解在业务方法上

    java
    import io.seata.spring.annotation.GlobalTransactional;
    
    @GlobalTransactional
    public void purchase(String userId, String commodityCode, int count) {
    	storageFeignClient.deduct(commodityCode, count);
    	
    	orderFeignClient.create(userId, commodityCode, count);
    	
    	storageFeignClient.validStorageData();
    }

Released under the MIT License.