Seata四种事务模式介绍+示例代码

Seata四种事务模式介绍+示例代码

Seata四种事务模式介绍+示例代码_第1张图片

什么是Seata?

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

Seata支持哪些分布式事务模式?

Seata目前支持以下四种分布式事务模式:

  • XA 模式:强一致性的两阶段提交协议,需要数据库支持XA接口,牺牲了一定的可用性,无业务侵入²。
  • AT 模式:最终一致性的两阶段提交协议,通过自动补偿机制实现数据回滚,无业务侵入,也是Seata的默认模式¹。
  • TCC 模式:最终一致性的两阶段提交协议,需要业务实现Try、Confirm和Cancel三个操作,有业务侵入,灵活度高¹。
  • SAGA 模式:长事务模式,通过状态机编排或者注解方式实现业务逻辑,需要业务实现正向和反向两个操作,有业务侵入¹。

下面我们来详细介绍每种模式的原理和示例。

XA 模式

XA 模式是基于XA协议实现的分布式事务模式,XA协议是由X/Open组织提出的一种两阶段提交协议,它定义了一个全局事务管理器(Transaction Manager)和多个资源管理器(Resource Manager)之间的接口。资源管理器通常是数据库或者消息队列等支持本地事务的组件。XA协议要求资源管理器实现以下接口:

  • start(xid):开始一个分支事务,xid是全局事务ID。
  • end(xid):结束一个分支事务。
  • prepare(xid):准备提交或回滚一个分支事务。
  • commit(xid):提交一个分支事务。
  • rollback(xid):回滚一个分支事务。

XA协议的工作流程如下:

Seata四种事务模式介绍+示例代码_第2张图片

  • 第一阶段:
    • 全局事务管理器向所有参与的资源管理器发送start(xid)命令,要求它们开始一个新的分支事务,并将xid作为标识。
    • 资源管理器执行各自的业务操作,并锁定相关资源。
    • 全局事务管理器向所有参与的资源管理器发送end(xid)命令,要求它们结束当前的分支事务。
    • 资源管理器结束当前的分支事务,并向全局事务管理器汇报准备就绪状态(prepared)或者失败状态(abort)。
  • 第二阶段:
    • 如果全局事务管理器收到了所有资源管理器的准备就绪状态,那么它会向所有参与的资源管理器发送commit(xid)命令,要求它们提交当前的分支事务,并释放相关资源。
    • 如果全局事务管理器收到了任何一个资源管理器的失败状态,或者在超时时间内没有收到所有资源管理器的状态,那么它会向所有参与的资源管理器发送rollback(xid)命令,要求它们回滚当前的分支事务,并释放相关资源。

XA 模式可以保证分布式事务的强一致性,但是也有以下缺点:

  • 需要数据库支持XA接口,目前只有MySQL、Oracle、TiDB和MariaDB支持¹。
  • 需要在第一阶段锁定相关资源,导致资源占用时间长,影响并发性能和可用性。
  • 需要全局事务管理器维护全局事务状态,增加了系统复杂度和开销。

Seata实现了XA模式,它将Seata服务端作为全局事务管理器,将Seata客户端作为资源管理器。Seata客户端通过拦截JDBC连接,实现了对XA接口的代理,从而可以与数据库进行通信。Seata客户端还通过注册中心(如Nacos)与Seata服务端进行通信,汇报分支事务的状态,并接收全局事务的命令。

下面是一个使用Seata XA 模式实现分布式事务的简单示例,假设有两个微服务:订单服务和库存服务,订单服务负责创建订单,库存服务负责扣减库存。当用户下单时,需要同时调用两个服务,并保证数据一致性。如果其中一个服务失败了,需要回滚另一个服务的操作。

步骤一:引入Seata依赖

在两个微服务的pom.xml文件中,添加Seata依赖:

<dependency>
    <groupId>io.seatagroupId>
    <artifactId>seata-spring-boot-starterartifactId>
    <version>1.4.2version>
dependency>
步骤二:配置Seata

在两个微服务的application.properties文件中,添加Seata相关配置:

# Seata 服务端地址
seata.service.vgroup-mapping.my_test_tx_group=default
seata.service.grouplist.default=127.0.0.1:8091
# Seata 应用ID
seata.application.id=order-service # 或者 inventory-service
# Seata 事务组ID
seata.tx-service-group=my_test_tx_group
# Seata 数据源代理模式
seata.datasource.proxy-mode=XA
# Seata 数据源配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false # 或者 inventory_db
spring.datasource.username=root
spring.datasource.password=root
步骤三:创建数据库表

在两个微服务对应的数据库中,创建业务表:

-- 订单表
CREATE TABLE `order_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT '0',
  `money` int(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_user_commodity` (`user_id`,`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- 库存表
CREATE TABLE `storage_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_commodity_code` (`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
步骤四:编写业务代码

在两个微服务中,分别编写订单服务和库存服务的业务代码,使用@GlobalTransactional注解标注分布式事务的入口方法,使用JdbcTemplate或者Mybatis等方式操作数据库:

// 订单服务
@Service
public class OrderService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private InventoryService inventoryService;

    @GlobalTransactional // 分布式事务入口
    public void createOrder(String userId, String commodityCode, int orderCount) {
        // 扣减库存
        inventoryService.deduct(commodityCode, orderCount);

        // 创建订单
        int orderMoney = calculate(commodityCode, orderCount);
        jdbcTemplate.update("insert into order_tbl(user_id, commodity_code, count, money) values (?, ?, ?, ?)",
                new Object[]{userId, commodityCode, orderCount, orderMoney});

        // 模拟异常,测试回滚
        if (orderCount == 2) {
            throw new RuntimeException("create order failed");
        }
    }

    private int calculate(String commodityCode, int orderCount) {
        return 100 * orderCount;
    }
}

// 库存服务
@Service
public class InventoryService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void deduct(String commodityCode, int count) {
        jdbcTemplate.update("update storage_tbl set count = count - ? where commodity_code = ?",
                new Object[]{count, commodityCode});
    }
}
步骤五:启动Seata服务端和客户端

下载Seata服务端的压缩包,解压后修改conf目录下的file.conf和registry.conf文件,配置好数据库和注册中心(如Nacos)等信息,然后运行bin目录下的seata-server.bat或者seata-server.sh文件,启动Seata服务端。

在两个微服务的项目目录下,运行mvn spring-boot:run命令,启动Seata客户端。

步骤六:测试分布式事务

使用Postman或者curl等工具,向订单服务发送请求,创建订单:

curl -X POST http://localhost:8081/order/create?userId=U100001&commodityCode=C00321&orderCount=2

观察控制台输出和数据库变化,可以发现当orderCount为2时,会触发异常,并回滚订单服务和库存服务的操作,保证数据一致性。当orderCount为其他值时,会正常执行,并提交订单服务和库存服务的操作。

AT 模式

AT 模式是基于自动补偿机制实现的分布式事务模式,它不需要数据库支持XA接口,也不需要业务实现额外的操作。AT 模式的核心思想是:在第一阶段提交本地事务时,记录数据的前后镜像(before image and after image),在第二阶段根据镜像进行反向操作来实现数据回滚。AT 模式的隔离流程如下:

Seata四种事务模式介绍+示例代码_第3张图片
Seata四种事务模式介绍+示例代码_第4张图片

  • 第一阶段:
    • Seata客户端拦截业务SQL语句,并解析出数据表、主键、字段等信息。
    • Seata客户端向Seata服务端申请一个全局事务ID(XID)。
    • Seata客户端向数据库发送查询SQL语句,获取数据的前镜像,并缓存在本地。
    • Seata客户端向数据库发送执行SQL语句,修改数据,并生成数据的后镜像。
    • Seata客户端将前后镜像和XID一起保存到undo_log表中,并提交本地事务。
  • 第二阶段:
    • 如果全局事务需要提交,Seata客户端会删除undo_log表中对应XID的记录,并结束分支事务。
    • 如果全局事务需要回滚,Seata客户端会根据undo_log表中对应XID的记录,生成并执行反向SQL语句,恢复数据到前镜像状态,并删除undo_log表中对应XID的记录。

AT 模式可以实现分布式事务的最终一致性,但是也有以下缺点:

  • 需要记录数据的前后镜像,增加了数据库的存储和网络的传输开销。
  • 需要在第一阶段获取全局锁,防止脏写,影响并发性能。
  • 需要在第二阶段根据镜像生成反向SQL语句,可能存在SQL解析和执行的不准确性。

Seata实现了AT模式,它将Seata服务端作为全局事务协调器,将Seata客户端作为分支事务执行器。Seata客户端通过拦截JDBC连接,实现了对SQL语句的解析和执行,以及对undo_log表的操作。Seata客户端还通过注册中心(如Nacos)与Seata服务端进行通信,汇报分支事务的状态,并接收全局事务的命令。

下面是一个使用Seata AT 模式实现分布式事务的简单示例,假设有两个微服务:订单服务和库存服务,订单服务负责创建订单,库存服务负责扣减库存。当用户下单时,需要同时调用两个服务,并保证数据一致性。如果其中一个服务失败了,需要回滚另一个服务的操作。

步骤一:引入Seata依赖

在两个微服务的pom.xml文件中,添加Seata依赖:

<dependency>
    <groupId>io.seatagroupId>
    <artifactId>seata-spring-boot-starterartifactId>
    <version>1.4.2version>
dependency>
步骤二:配置Seata

在两个微服务的application.properties文件中,添加Seata相关配置:

# Seata 服务端地址
seata.service.vgroup-mapping.my_test_tx_group=default
seata.service.grouplist.default=127.0.0.1:8091
# Seata 应用ID
seata.application.id=order-service # 或者 inventory-service
# Seata 事务组ID
seata.tx-service-group=my_test_tx_group
# Seata 数据源代理模式
seata.datasource.proxy-mode=AT
# Seata 数据源配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false # 或者 inventory_db
spring.datasource.username=root
spring.datasource.password=root
步骤三:创建数据库表

在两个微服务对应的数据库中,创建业务表和undo_log表:

-- 订单表
CREATE TABLE `order_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT '0',
  `money` int(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_user_commodity` (`user_id`,`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- 库存表
CREATE TABLE `storage_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_commodity_code` (`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- undo_log 表(两个数据库都需要)
CREATE TABLE `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(100) 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     NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME     NOT NULL COMMENT 'modify datetime',
    `ext`           VARCHAR(100) DEFAULT NULL COMMENT 'reserved field',
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
步骤四:编写业务代码

在两个微服务中,分别编写订单服务和库存服务的业务代码,使用@GlobalTransactional注解标注分布式事务的入口方法,使用@DataSourceProxy注解标注数据源代理对象,使用JdbcTemplate或者Mybatis等方式操作数据库:

// 订单服务
@Service
public class OrderService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private InventoryService inventoryService;

    @GlobalTransactional // 分布式事务入口
    public void createOrder(String userId, String commodityCode, int orderCount) {
        // 扣减库存
        inventoryService.deduct(commodityCode, orderCount);

        // 创建订单
        int orderMoney = calculate(commodityCode, orderCount);
        jdbcTemplate.update("insert into order_tbl(user_id, commodity_code, count, money) values (?, ?, ?, ?)",
                new Object[]{userId, commodityCode, orderCount, orderMoney});

        // 模拟异常,测试回滚
        if (orderCount == 2) {
            throw new RuntimeException("create order failed");
        }
    }

    private int calculate(String commodityCode, int orderCount) {
        return 100 * orderCount;
    }
}

// 库存服务
@Service
public class InventoryService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @DataSourceProxy // 数据源代理
    public void deduct(String commodityCode, int count) {
        jdbcTemplate.update("update storage_tbl set count = count - ? where commodity_code = ?",
                new Object[]{count, commodityCode});
    }
}
步骤五:启动Seata服务端和客户端

下载Seata服务端的压缩包,解压后修改conf目录下的file.conf和registry.conf文件,配置好数据库和注册中心(如Nacos)等信息,然后运行bin目录下的seata-server.bat或者seata-server.sh文件,启动Seata服务端。

在两个微服务的项目目录下,运行mvn spring-boot:run命令,启动Seata客户端。

步骤六:测试分布式事务

使用Postman或者curl等工具,向订单服务发送请求,创建订单:

curl -X POST http://localhost:8081/order/create?userId=U100001&commodityCode=C00321&orderCount=2

观察控制台输出和数据库变化,可以发现当orderCount为2时,会触发异常,并回滚订单服务和库存服务的操作,保证数据一致性。当orderCount为其他值时,会正常执行,并提交订单服务和库存服务的操作。

TCC 模式

TCC 模式是基于补偿机制实现的分布式事务模式,它不需要数据库支持XA接口,但需要业务实现三个操作:Try、Confirm和Cancel。TCC 模式的核心思想是:在第一阶段执行Try操作,预留或锁定相关资源;在第二阶段根据全局事务的状态,执行Confirm操作或者Cancel操作,释放或回滚相关资源。TCC 模式的工作流程如下:

Seata四种事务模式介绍+示例代码_第5张图片

  • 第一阶段:
    • Seata客户端向Seata服务端申请一个全局事务ID(XID)。
    • Seata客户端调用各个业务服务的Try操作,预留或锁定相关资源,并记录本地事务日志。
    • Seata客户端向Seata服务端汇报各个业务服务的Try操作的状态(成功或失败)。
  • 第二阶段:
    • 如果全局事务需要提交,Seata客户端会调用各个业务服务的Confirm操作,确认并释放相关资源,并删除本地事务日志。
    • 如果全局事务需要回滚,Seata客户端会调用各个业务服务的Cancel操作,取消并回滚相关资源,并删除本地事务日志。

TCC 模式可以实现分布式事务的最终一致性,但是也有以下缺点:

  • 需要业务实现三个操作,增加了开发和测试的难度和成本。
  • 需要在第一阶段预留或锁定相关资源,导致资源占用时间长,影响并发性能和可用性。
  • 需要保证三个操作的幂等性和一致性,避免出现悬挂或空回滚的情况。

Seata实现了TCC模式,它将Seata服务端作为全局事务协调器,将Seata客户端作为分支事务执行器。Seata客户端通过注解方式,标注业务服务的三个操作,并通过反射机制调用相应的方法。Seata客户端还通过注册中心(如Nacos)与Seata服务端进行通信,汇报分支事务的状态,并接收全局事务的命令。

下面是一个使用Seata TCC 模式实现分布式事务的简单示例,假设有两个微服务:订单服务和库存服务,订单服务负责创建订单,库存服务负责扣减库存。当用户下单时,需要同时调用两个服务,并保证数据一致性。如果其中一个服务失败了,需要回滚另一个服务的操作。

步骤一:引入Seata依赖

在两个微服务的pom.xml文件中,添加Seata依赖:

<dependency>
    <groupId>io.seatagroupId>
    <artifactId>seata-spring-boot-starterartifactId>
    <version>1.4.2version>
dependency>
步骤二:配置Seata

在两个微服务的application.properties文件中,添加Seata相关配置:

# Seata 服务端地址
seata.service.vgroup-mapping.my_test_tx_group=default
seata.service.grouplist.default=127.0.0.1:8091
# Seata 应用ID
seata.application.id=order-service # 或者 inventory-service
# Seata 事务组ID
seata.tx-service-group=my_test_tx_group
# Seata 数据源配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false # 或者 inventory_db
spring.datasource.username=root
spring.datasource.password=root
步骤三:创建数据库表

在两个微服务对应的数据库中,创建业务表:

-- 订单表
CREATE TABLE `order_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT '0',
  `money` int(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_user_commodity` (`user_id`,`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- 库存表
CREATE TABLE `storage_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_commodity_code` (`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
步骤四:编写业务代码

在两个微服务中,分别编写订单服务和库存服务的业务代码,使用@GlobalTransactional注解标注分布式事务的入口方法,使用@TCC注解标注业务服务的三个操作,使用JdbcTemplate或者Mybatis等方式操作数据库:

// 订单服务
@Service
public class OrderService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private InventoryService inventoryService;

    @GlobalTransactional // 分布式事务入口
    public void createOrder(String userId, String commodityCode, int orderCount) {
        // 扣减库存
        inventoryService.deduct(commodityCode, orderCount);

        // 创建订单
        int orderMoney = calculate(commodityCode, orderCount);
        order(userId, commodityCode, orderCount, orderMoney);

        // 模拟异常,测试回滚
        if (orderCount == 2) {
            throw new RuntimeException("create order failed");
        }
    }

    private int calculate(String commodityCode, int orderCount) {
        return 100 * orderCount;
    }

    @TCC(confirmMethod = "confirmOrder", cancelMethod = "cancelOrder") // TCC操作
    public void order(String userId, String commodityCode, int orderCount, int orderMoney) {
        // Try操作:预留订单资源
        jdbcTemplate.update("insert into order_tbl(user_id, commodity_code, count, money) values (?, ?, ?, ?)",
                new Object[]{userId, commodityCode, orderCount, orderMoney});
    }

    public void confirmOrder(String userId, String commodityCode, int orderCount, int orderMoney) {
        // Confirm操作:无需操作
        System.out.println("confirm order");
    }

    public void cancelOrder(String userId, String commodityCode, int orderCount, int orderMoney) {
        // Cancel操作:取消订单资源
        jdbcTemplate.update("delete from order_tbl where user_id = ? and commodity_code = ?",
                new Object[]{userId, commodityCode});
    }
}

// 库存服务
@Service
public class InventoryService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @TCC(confirmMethod = "confirmDeduct", cancelMethod = "cancelDeduct") // TCC操作
    public void deduct(String commodityCode, int count) {
        // Try操作:预留库存资源
        jdbcTemplate.update("update storage_tbl set count = count - ? where commodity_code = ?",
                new Object[]{count, commodityCode});
    }

    public void confirmDeduct(String commodityCode, int count) {
        // Confirm操作:无需操作
        System.out.println("confirm deduct");
    }

    public void cancelDeduct(String commodityCode, int count) {
        // Cancel操作:恢复库存资源
        jdbcTemplate.update("update storage_tbl set count = count + ? where commodity_code = ?",
                new Object[]{count, commodityCode});
    }
}
步骤五:启动Seata服务端和客户端

下载Seata服务端的压缩包,解压后修改conf目录下的file.conf和registry.conf文件,配置好数据库和注册中心(如Nacos)等信息,然后运行bin目录下的seata-server.bat或者seata-server.sh文件,启动Seata服务端。

在两个微服务的项目目录下,运行mvn spring-boot:run命令,启动Seata客户端。

步骤六:测试分布式事务

使用Postman或者curl等工具,向订单服务发送请求,创建订单:

curl -X POST http://localhost:8081/order/create?userId=U100001&commodityCode=C00321&orderCount=2

观察控制台输出和数据库变化,可以发现当orderCount为2时,会触发异常,并回滚订单服务和库存服务的操作,保证数据一致性。当orderCount为其他值时,会正常执行,并提交订单服务和库存服务的操作。

SAGA 模式

SAGA 模式是一种长事务模式,它将一个长时间运行的事务拆分为多个子事务,每个子事务都可以独立地提交或回滚,从而避免长时间占用资源。SAGA 模式的核心思想是:通过状态机来编排子事务的执行顺序和逻辑,每个子事务都有正向操作和反向操作,正向操作用于执行业务逻辑,反向操作用于补偿业务逻辑。SAGA 模式的工作流程如下:

Seata四种事务模式介绍+示例代码_第6张图片

  • 第一阶段:
    • Seata客户端向Seata服务端申请一个全局事务ID(XID)。
    • Seata客户端根据状态机的定义,依次调用各个子事务的正向操作,执行业务逻辑,并记录本地事务日志。
    • Seata客户端向Seata服务端汇报各个子事务的正向操作的状态(成功或失败)。
  • 第二阶段:
    • 如果全局事务需要提交,Seata客户端会删除本地事务日志,并结束分支事务。
    • 如果全局事务需要回滚,Seata客户端会根据本地事务日志,从后往前依次调用各个子事务的反向操作,补偿业务逻辑,并删除本地事务日志。

SAGA 模式可以实现分布式事务的最终一致性,但是也有以下缺点:

  • 需要业务实现正向操作和反向操作,增加了开发和测试的难度和成本。
  • 需要保证反向操作的幂等性和可靠性,避免出现数据不一致的情况。
  • 需要考虑业务的补偿策略和顺序,避免出现业务逻辑的冲突。

Seata实现了SAGA模式,它将Seata服务端作为全局事务协调器,将Seata客户端作为分支事务执行器。Seata客户端通过状态机引擎来编排子事务的执行顺序和逻辑,并通过注解方式或者编程方式来标注业务服务的正向操作和反向操作。Seata客户端还通过注册中心(如Nacos)与Seata服务端进行通信,汇报分支事务的状态,并接收全局事务的命令。

下面是一个使用Seata SAGA 模式实现分布式事务的简单示例,假设有两个微服务:订单服务和库存服务,订单服务负责创建订单,库存服务负责扣减库存。当用户下单时,需要同时调用两个服务,并保证数据一致性。如果其中一个服务失败了,需要回滚另一个服务的操作。

步骤一:引入Seata依赖

在两个微服务的pom.xml文件中,添加Seata依赖:

<dependency>
    <groupId>io.seatagroupId>
    <artifactId>seata-spring-boot-starterartifactId>
    <version>1.4.2version>
dependency>
步骤二:配置Seata

在两个微服务的application.properties文件中,添加Seata相关配置:

# Seata 服务端地址
seata.service.vgroup-mapping.my_test_tx_group=default
seata.service.grouplist.default=127.0.0.1:8091
# Seata 应用ID
seata.application.id=order-service # 或者 inventory-service
# Seata 事务组ID
seata.tx-service-group=my_test_tx_group
# Seata 数据源配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false # 或者 inventory_db
spring.datasource.username=root
spring.datasource.password=root
步骤三:创建数据库表

在两个微服务对应的数据库中,创建业务表:

-- 订单表
CREATE TABLE `order_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT '0',
  `money` int(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_user_commodity` (`user_id`,`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- 库存表
CREATE TABLE `storage_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_commodity_code` (`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
步骤四:编写业务代码

在两个微服务中,分别编写订单服务和库存服务的业务代码,使用@GlobalTransactional注解标注分布式事务的入口方法,使用@Compensable注解标注业务服务的正向操作和反向操作,使用JdbcTemplate或者Mybatis等方式操作数据库:

// 订单服务
@Service
public class OrderService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private InventoryService inventoryService;

    @GlobalTransactional // 分布式事务入口
    public void createOrder(String userId, String commodityCode, int orderCount) {
        // 扣减库存
        inventoryService.deduct(commodityCode, orderCount);

        // 创建订单
        int orderMoney = calculate(commodityCode, orderCount);
        order(userId, commodityCode, orderCount, orderMoney);

        // 模拟异常,测试回滚
        if (orderCount == 2) {
            throw new RuntimeException("create order failed");
        }
    }

    private int calculate(String commodityCode, int orderCount) {
        return 100 * orderCount;
    }

    @Compensable(compensationMethod = "cancelOrder") // SAGA操作
    public void order(String userId, String commodityCode, int orderCount, int orderMoney) {
        // 正向操作:创建订单资源
        jdbcTemplate.update("insert into order_tbl(user_id, commodity_code, count, money) values (?, ?, ?, ?)",
                new Object[]{userId, commodityCode, orderCount, orderMoney});
    }

    public void cancelOrder(String userId, String commodityCode, int orderCount, int orderMoney) {
        // 反向操作:删除订单资源
        jdbcTemplate.update("delete from order_tbl where user_id = ? and commodity_code = ?",
                new Object[]{userId, commodityCode});
    }
}

// 库存服务
@Service
public class InventoryService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Compensable(compensationMethod = "cancelDeduct") // SAGA操作
    public void deduct(String commodityCode, int count) {
        // 正向操作:扣减库存资源
        jdbcTemplate.update("update storage_tbl set count = count - ? where commodity_code = ?",
                new Object[]{count, commodityCode});
    }

    public void cancelDeduct(String commodityCode, int count) {
        // 反向操作:恢复库存资源
        jdbcTemplate.update("update storage_tbl set count = count + ? where commodity_code = ?",
                new Object[]{count, commodityCode});
    }
}
步骤五:启动Seata服务端和客户端

下载Seata服务端的压缩包,解压后修改conf目录下的file.conf和registry.conf文件,配置好数据库和注册中心(如Nacos)等信息,然后运行bin目录下的seata-server.bat或者seata-server.sh文件,启动Seata服务端。

在两个微服务的项目目录下,运行mvn spring-boot:run命令,启动Seata客户端。

步骤六:测试分布式事务

使用Postman或者curl等工具,向订单服务发送请求,创建订单:

curl -X POST http://localhost:8081/order/create?userId=U100001&commodityCode=C00321&orderCount=2

观察控制台输出和数据库变化,可以发现当orderCount为2时,会触发异常,并回滚订单服务和库存服务的操作,保证数据一致性。当orderCount为其他值时,会正常执行,并提交订单服务和库存服务的操作。

总结

Seata是一款支持多种分布式事务模式的框架,它可以根据不同的业务场景和需求,选择合适的模式来实现数据的一致性。Seata的四种模式各有优缺点,需要根据具体情况进行权衡和选择。以下是一个简单的对比表:

模式 一致性 可用性 侵入性 复杂度
XA
AT 最终
TCC 最终
SAGA 最终
  • Seata官方网站:https://seata.io/
  • Seata官方文档:https://seata.io/en-us/docs/overview/what-is-seata.html
  • Seata GitHub仓库:https://github.com/seata/seata
  • Seata示例项目:https://github.com/seata/seata-samples

你可能感兴趣的:(分布式,java,数据库,微服务,spring,spring,cloud)