分布式事务解决方案:Seata

·  阅读 1666

概述

一、Seata 是什么

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

seata-1.png

二、分布式事务

以一个用户购买商品的业务逻辑为例。

由于整个项目随着业务需求的增加,由原来的单体应用,拆分成了多个微服务应用:

  • 仓储服务:对给定的商品扣除仓储数量。
  • 订单服务:根据采购需求创建订单。
  • 帐户服务:从用户帐户中扣除余额。

此时每个服务内部的数据一致性只能由本地的事务保证,但是对于全局(服务间)数据一致性问题没办法保证。

全局事务包含若干分支(本地)事务,并统一协调分支事务达成一致,同一全局事务下的分支事务,要么一起提交,要么一起回滚。

seata-2.png

全局数据一致性的产生就是一次业务操作需要处理多个数据源,这时就产生了分布式事务问题。

三、Seata 组件

TM (Transaction Manager) - 事务管理器

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

TC (Transaction Coordinator) - 事务协调者

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

RM (Resource Manager) - 资源管理器

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

seata-12.png

Seata 文档与下载

Seata 官网:seata.io/zh-cn/index…

Seata 中文文档:seata.io/zh-cn/docs/…

Seata 下载:seata.io/zh-cn/blog/…

Seata GitHub:github.com/seata/seata

Seata 快速开始

一、Seata-Server 配置

下载 seata-server

这里选择 seata-server-1.4.2,下载并解压。

配置 seata-server

  1. 进入到 ~/seata-server-1.4.2/conf 目录中,配置 registry.conf
registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "consul"

  consul {
    cluster = "seata-server"
    serverAddr = "127.0.0.1:8500"
    aclToken = ""
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"

  file {
    name = "file.conf"
  }
}
复制代码

配置说明:

  • registry.type:这里使用 Consul 作为注册中心;

  • config.file:使用文件作为配置中心;

注:如果选择其他配置中心的话,可以到 GitHub:github.com/seata/seata… 上下载对应配置中心脚本。

  1. 配置 file.conf
service {
  #vgroup->rgroup
  vgroup_mapping.tx_group = "default"
  #only support single node
  default.grouplist = "127.0.0.1:8091" 
  #degrade current not support
  enableDegrade = false
  #disable
  disable = false
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
}

## transaction log store, only used in seata-server
store {
  ## store mode: file、db、redis
  mode = "db"
  ## rsa decryption public key
  publicKey = ""

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    ## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
    url = "jdbc:mysql://127.0.0.1:3306/seata-server?rewriteBatchedStatements=true"
    user = "root"
    password = "123456"
    minConn = 5
    maxConn = 100
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }
}
复制代码

配置说明:

  • service.vgroup_mapping.tx_group = "default":事务组 tx_group,默认集群 default;

  • service.default.grouplist = "127.0.0.1:8091":default 集群地址;

  • store.mode:事务信息存储数据库;

  • store.db:配置连接数据库的相关信息;

Seata-Server 数据库初始化

从 Seata GitHub:github.com/seata/seata… 上,下载 mysql.sql。

到 MySQL 中,创建数据库 seata-server,并执行刚下载的 sql 脚本。

seata-3.png

注:数据库需要与 seata-server-1.4.2/conf/registry.conf 配置文件中,store.db.url 配置对应。

启动 seata-server

进入到 seata-server-1.4.2/bin/ 目录下,双击 seata-server.bat 即可启动 Seata。

注:需要先启动 Consul,然后启动 Seata,并在 Consul 上查看 Seata 是否成功注册到 Consul。

seata-4.png

注:注册到 Consul 的服务名与 registry.conf 配置文件中的 registry.consul.custer 一致。

二、Seata-Client 配置

Seata-Client 数据库

Seata 的客户端使用与官方示例相同的三个服务:

  • seata-account-service:账户服务
  • seata-order-service:订单服务
  • seata-storage-service:库存服务

seata-account 数据库

CREATE TABLE `account` (
  `id` bigint(11NOT NULL AUTO_INCREMENT COMMENT 'id',
  `user_id` bigint(11DEFAULT NULL COMMENT '用户id',
  `total` decimal(10,0DEFAULT NULL COMMENT '总额度',
  `used` decimal(10,0DEFAULT NULL COMMENT '已用余额',
  `residue` decimal(10,0DEFAULT '0' COMMENT '剩余可用额度',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
复制代码

seata-order 数据库

CREATE TABLE `order` (
  `id` bigint(11NOT NULL AUTO_INCREMENT,
  `user_id` bigint(11DEFAULT NULL COMMENT '用户id',
  `product_id` bigint(11DEFAULT NULL COMMENT '产品id',
  `count` int(11DEFAULT NULL COMMENT '数量',
  `money` decimal(11,0DEFAULT NULL COMMENT '金额',
  `status` int(1DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=59 DEFAULT CHARSET=utf8
复制代码

seata-storage 数据库

CREATE TABLE `storage` (
  `id` bigint(11NOT NULL AUTO_INCREMENT,
  `product_id` bigint(11DEFAULT NULL COMMENT '产品id',
  `total` int(11DEFAULT NULL COMMENT '总库存',
  `used` int(11DEFAULT NULL COMMENT '已用库存',
  `residue` int(11DEFAULT NULL COMMENT '剩余库存',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
复制代码

undo_log 表

Seata 需要在每个业务数据库中分别创建一张 undo_log 的表。

表结构可以从 GitHub:github.com/seata/seata… 上下载 mysql.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 `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';
复制代码

seata-5.png

Seata-Client 客户端项目搭建

创建项目 seata-sample-client-service

<groupId>com.example</groupId>
<artifactId>seata-sample-client-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
复制代码

导入依赖

<!--seata-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-seata</artifactId>
    <version>2.2.0.RELEASE</version>
    <exclusions>
        <exclusion>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.4.2</version>
</dependency>

<!--web-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--consul-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

<!-- actuator-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!--openfeign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<!--mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.4</version>
</dependency>

<!--mysql-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.37</version>
</dependency>

<!--druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.12</version>
</dependency>

<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
复制代码

公共模块:common-api

在项目中,创建 Maven 子模块:common-api。

创建一个公共的返回类:CommonResult。

package com.example.common.api.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class CommonResult<T> {
    private T data;
    private String message;
    private Integer code;

    public CommonResult(Integer code, String message) {
        this(null, message, code);
    }
}
复制代码

账户业务模块:seata-account-service

1. 创建两个 SpringBoot 子模块,并导入公共模块 common-api

<groupId>com.example</groupId>
<artifactId>seata-account-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
复制代码
<dependency>
    <groupId>com.example</groupId>
    <artifactId>common-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>
复制代码

2. 修改 application.properties 配置文件

#应用名称
spring.application.name=seata-account-service
#端口
server.port=8082

# ===== ===== ===== ===== Consul ===== ===== ===== =====
#注册服务的Consul环境
spring.cloud.consul.host=127.0.0.1
spring.cloud.consul.port=8500
spring.cloud.consul.token=
#Consul服务名
spring.cloud.consul.discovery.service-name=${spring.application.name}
#微服务注册开关,设置为false表示不要将该工程注册为Consul服务(默认为true)。
spring.cloud.consul.discovery.register=true
#Consul进行健康检查回调的url
spring.cloud.consul.discovery.healthCheckPath=/actuator
#健康检查的间隔
spring.cloud.consul.discovery.healthCheckInterval=10s
#健康检查的超时时间,超时后Consul会将该服务从列表中注销
spring.cloud.consul.discovery.healthCheckCriticalTimeout=600s
#服务实例的id,在整个Consul里必须唯一,否则会导致服务发布失败,我们采用了随机数策略来使其唯一
spring.cloud.consul.discovery.instanceId=${spring.application.name}
#取IP地址进行注册
spring.cloud.consul.discovery.prefer-ip-address=true

# ===== ===== ===== ===== DataSource ===== ===== ===== =====
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/seata-account?characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456

# ===== ===== ===== ===== mybatis ===== ===== ===== =====
mybatis.mapper-locations=classpath:mapper/*.xml

# ===== ===== ===== ===== seata ===== ===== ===== =====
#启用seata
seata.enabled=true
#事务分组
seata.tx-service-group=tx_group
#事务集群名default
seata.service.vgroup-mapping.tx_group=default
#集群default地址
seata.service.grouplist.default=127.0.0.1:8091
#seata数据源代理模式
seata.data-source-proxy-mode=AT
复制代码

3. 实现 Mapper 层代码

Mapper 接口

package com.example.seata.account.service.mapper;

import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface AccountMapper {

    /**
     * 扣减账户余额
     */
    void decrease(@Param("userId") Long userId, @Param("money") Double money);
}
复制代码

Mapper 配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.seata.account.service.mapper.AccountMapper">

    <update id="decrease">
        UPDATE account
        SET residue = residue - #{money}, used = used + #{money}
        WHERE user_id = #{userId};
    </update>
</mapper>
复制代码

4. 实现 Service 层代码

创建 Service 接口

package com.example.seata.account.service.service;

public interface AccountService {

    /**
     * 扣减账户余额
     * @param userId 用户id
     * @param money 金额
     */
    void decrease(Long userId, Double money);
}
复制代码

创建 Service 实现类

package com.example.seata.account.service.service.impl;

import com.example.seata.account.service.mapper.AccountMapper;
import com.example.seata.account.service.service.AccountService;
import io.seata.spring.annotation.GlobalTransactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AccountServiceImpl implements AccountService {

    private static final Logger logger = LoggerFactory.getLogger(AccountServiceImpl.class);

    @Autowired
    private AccountMapper accountMapper;

    @Override
    public void decrease(Long userId, Double money) {
        logger.info("=== ===》 AccountService 扣减账户余额开始 《=== ===");
        //模拟超时异常,全局事务回滚
        try {
            Thread.sleep(30 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        accountMapper.decrease(userId, money);
        logger.info("=== ===》 AccountService 扣减账户余额结束 《=== ===");
    }
}
复制代码

注:在业务中添加一个超时异常。

5. 实现 Controller 层代码

package com.example.seata.account.service.controller;

import com.example.common.api.pojo.CommonResult;
import com.example.seata.account.service.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/account")
public class AccountController {

    @Autowired
    private AccountService accountService;

    /**
     * 扣减账户余额
     */
    @RequestMapping("/decrease")
    public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") Double money){
        accountService.decrease(userId,money);
        return new CommonResult(200, "扣减账户余额成功!");
    }
}
复制代码

6. 创建配置类 DataSource

使用 Seata 对数据源进行代理,注:这里必须配置,否则会报错。

package com.example.seata.account.service.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource() {
        return new DruidDataSource();
    }

    @Primary
    @Bean
    public DataSource dataSource(DruidDataSource druidDataSource) {
        return new DataSourceProxy(druidDataSource);
    }
}
复制代码

7. 在启动类上添加注解

package com.example.seata.account.service;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

// 取消数据源自动创建
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
// Mapper 包扫描
@MapperScan("com.example.seata.account.service.mapper")
// 开启注册中心
@EnableDiscoveryClient
public class SeataAccountServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(SeataAccountServiceApplication.class, args);
    }

}
复制代码

库存业务模块:seata-storage-service

具体流程与 seata-account-service 模块基本一致,可以参考上面流程创建。

applicationn.properties 配置如下:

  • 服务名:seata-account-service
  • 服务端口:8083
  • 数据库:seata-account

Controller 控制器:

package com.example.seata.storage.service.controller;

@RestController
@RequestMapping("/storage")
public class StorageController {
    @Autowired
    private StorageService storageService;
    @RequestMapping("/decrease")
    public CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count) {
        storageService.decrease(productId, count);
        return new CommonResult(200, "扣减库存成功!");
    }
}
复制代码

订单业务模块 seata-order-service

applicationn.properties 配置如下:

  • 服务名:seata-order-service
  • 服务端口:8081
  • 数据库:seata-order

1. OrderMapper 接口

package com.example.seata.order.service.mapper;

import com.example.seata.order.service.pojo.Order;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderMapper {

    /**
     * create order
     */
    Boolean create(Order order);

    /**
     * update order
     */
    Boolean update(@Param("userId") Long userId, @Param("status") Integer status);
}
复制代码

2. OrderMapper 配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.seata.order.service.mapper.OrderMapper">

    <resultMap id="BaseResultMap" type="com.example.seata.order.service.pojo.Order">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="user_id" property="userId" jdbcType="BIGINT"/>
        <result column="product_id" property="productId" jdbcType="BIGINT"/>
        <result column="count" property="count" jdbcType="INTEGER"/>
        <result column="money" property="money" jdbcType="DECIMAL"/>
        <result column="status" property="status" jdbcType="INTEGER"/>
    </resultMap>

    <insert id="create">
        INSERT INTO `order` (`id`, `user_id`, `product_id`, `count`, `money`, `status`)
        VALUES (NULL, #{userId}, #{productId}, #{count}, #{money}, 0);
    </insert>

    <update id="update">
        UPDATE `order`
        SET status = 1
        WHERE user_id = #{userId}
          AND status = #{status};
    </update>
</mapper>
复制代码

3. AccountService 接口

package com.example.seata.order.service.service;

import com.example.common.api.pojo.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "seata-account-service")
public interface AccountService {
    /**
     * 扣减账户余额
     */
    @RequestMapping("/account/decrease")
    CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") Double money);
}
复制代码

4. StorageService 接口

package com.example.seata.order.service.service;

import com.example.common.api.pojo.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "seata-storage-service")
public interface StorageService {
    /**
     * 扣减库存
     */
    @RequestMapping(value = "/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
复制代码

AccountService 和 StorageService 使用 Feign 实现远程调用。

5. OrderService 接口

package com.example.seata.order.service.service;

import com.example.seata.order.service.pojo.Order;

public interface OrderService {
    /**
     * 创建订单
     */
    void create(Order order);
}
复制代码

6. OrderService 实现类

package com.example.seata.order.service.service.impl;

import com.example.seata.order.service.mapper.OrderMapper;
import com.example.seata.order.service.pojo.Order;
import com.example.seata.order.service.service.AccountService;
import com.example.seata.order.service.service.OrderService;
import com.example.seata.order.service.service.StorageService;
import io.seata.core.context.RootContext;
import io.seata.core.exception.TransactionException;
import io.seata.spring.annotation.GlobalTransactional;
import io.seata.tm.api.GlobalTransactionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderServiceImpl implements OrderService {
    private static final Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class);

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private StorageService storageService;
    @Autowired
    private AccountService accountService;

    /**
     * 流程:创建订单 =》 调用库存服务扣减库存 =》 调用账户服务扣减账户余额 =》 修改订单状态
     */
    @Override
    @GlobalTransactional
//    @GlobalTransactional(name = "tx-test", rollbackFor = Exception.class)
    public void create(Order order) {
        logger.info("=== ===》 下单开始 《=== === ");
        //本应用创建订单
        orderMapper.create(order);
        //远程调用库存服务扣减库存
        logger.info("=== ===》 OrderService 扣减库存开始 《=== === ");
        storageService.decrease(order.getProductId(), order.getCount());
        logger.info("=== ===》 OrderService 扣减库存结束 《=== === ");

        //远程调用账户服务扣减余额
        logger.info("=== ===》 OrderService 扣减余额开始 《=== === ");
        accountService.decrease(order.getUserId(), order.getMoney());
        logger.info("=== ===》 OrderService 扣减余额结束 《=== === ");

        //修改订单状态为已完成
        logger.info("=== ===》 OrderService 修改订单状态开始 《=== === ");
        orderMapper.update(order.getUserId(), 0);
        logger.info("=== ===》 OrderService 修改订单状态结束 《=== === ");

        logger.info("=== ===》 下单结束 《=== === ");

        /**
         * 需要异常服务暴露异常到当前服务,当前服务会捕获异常,并回滚全局事务
         * 如果使用了openfeign服务降级,可以使用自定义对象的属性判断是否执行成功
         * 失败可以使用下面方式手动回滚(已测试)
         */
//        try {
//            GlobalTransactionContext.reload(RootContext.getXID()).rollback();
//        } catch (TransactionException e) {
//            e.printStackTrace();
//        }

        /**
         * 手动提交事务
         */
//        try {
//            GlobalTransactionContext.reload(RootContext.getXID()).commit();
//        } catch (TransactionException e) {
//            e.printStackTrace();
//        }

        /**
         * 注:RootContext.getXID():获取全局事务的id
         */
    }
}
复制代码

7. Controller

package com.example.seata.order.service.controller;

import com.example.common.api.pojo.CommonResult;
import com.example.seata.order.service.pojo.Order;
import com.example.seata.order.service.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 创建订单
     */
    @PostMapping("/create")
    public CommonResult create(Order order) {
        orderService.create(order);
        return new CommonResult(200, "订单创建成功!");
    }
}
复制代码

三、测试

模拟分布式事务回滚

  1. 启动 seata-server 和三个服务,查看 Consul 上是否注册成功。

seata-6.png

  1. 查看控制台,seata 是否注册成功

seata-7.png

  1. 三张表中初始数据

seata-8.png

  1. 在 Service 业务类上添加 @GlobalTransactional 注解,看是否可以回滚

seata-9.png

  1. 请求 OrderController 中的接口,查看存在超时异常时是否可以事务回滚
  • 数据库中数据没有变化;

  • 查看 Order 控制台,是否有回滚信息

seata-10.png

将 @GlobalTransactional 注解取消

将 seata-order-service 重启,再次请求,并查看数据库数据是否有变化。

seata-11.png

由于服务访问超时和重试机制,在超时过程中,会重试一次,就导致了数据不一致的情况。

Seata 四种模式

一、Seata 四种模式对比

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

二、模式设置

在 applicaion.properties 配置文件中添加以下配置

seata.data-source-proxy-mode=AT
复制代码

默认是 AT 模式,可以设置为 XA 模式。

三、XA 模式实现

获取 XAConnection 两种方式:

  • 方式一:要求开发者配置 XADataSource;

  • 方式二:根据开发者的普通 DataSource 来创建;

第一种方式,给开发者增加了认知负担,需要为 XA 模式专门去学习和使用 XA 数据源,与 透明化 XA 编程模型的设计目标相违背。

第二种方式,对开发者比较友好,和 AT 模式使用一样,开发者完全不必关心 XA 层面的任何问题,保持本地编程模型即可。

方式二实现思路与 AT 模式相同,参考示例实现即可。

注:需要把配置文件 application.properties 中的模式配置修改为:seata.data-source-proxy-mode=XA

我们优先设计实现第二种方式:数据源代理根据普通数据源中获取的普通 JDBC 连接创建出相应的 XAConnection。

类比 AT 模式的数据源代理机制,如下:

seata-18

但是,第二种方法有局限:无法保证兼容的正确性。

综合考虑,XA 模式的数据源代理设计需要同时支持第一种方式:基于 XA 数据源进行代理。

类比 AT 模式的数据源代理机制,如下:

seata-19

可以参考 Seata 官网的样例:seata-xa

具体实现步骤如下:

1. 修改配置文件中的配置

和 AT 模式使用一样,开发者完全不必关心 XA 层面的任何问题,保持本地编程模型即可。

seata.data-source-proxy-mode=XA
复制代码

2. 代理数据源

@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
	// DataSourceProxy for AT mode
	// return new DataSourceProxy(druidDataSource);

	// DataSourceProxyXA for XA mode
	return new DataSourceProxyXA(druidDataSource);
}
复制代码

四、TCC 模式实现

TCC 模式与 AT 模式非常相似,每阶段都是独立事务,不同的是 TCC 模式通过人工编码来实现数据恢复。

seata-16

根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 TCC (Branch) Transaction Mode.

AT 模式(参考链接 TBD)基于 支持本地 ACID 事务 的 关系型数据库

  • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
  • 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
  • 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。

相应的,TCC 模式,不依赖于底层数据资源的事务支持:

  • 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
  • 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
  • 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。

所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。

创建数据表

在 seata-account 数据库中,添加一个记录表 account_freeze ,记录冻结状态。

CREATE TABLE `account_freeze` (
  `xid` varchar(128) NOT NULL,
  `user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
  `freeze_money` decimal(8,2) unsigned DEFAULT '0.00' COMMENT '冻结金额',
  `state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
  PRIMARY KEY (`xid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT
复制代码

seata-account-service 模块实现

1. 创建实体类:AccountFreeze

package com.example.seata.account.service.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class AccountFreeze {

    private String xid;
    private Long userId;
    private Double freezeMoney;
    private Integer state;
}
复制代码

2. 创建 Mapper 接口:AccountFreezeMapper

package com.example.seata.account.service.mapper;

import com.example.seata.account.service.pojo.AccountFreeze;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface AccountFreezeMapper {

    AccountFreeze selectById(@Param("xid") String xid);

    void insert(AccountFreeze accountFreeze);

    int deleteById(@Param("xid") String xid);

    int updateById(AccountFreeze accountFreeze);
}
复制代码

3. 创建 Mapper 配置文件:AccountFreezeMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.seata.account.service.mapper.AccountFreezeMapper">

    <select id="selectById" parameterType="String" resultType="com.example.seata.account.service.pojo.AccountFreeze">
        select xid,user_id,freeze_money,state
        from account_freeze
        where xid=#{xid}
    </select>

    <insert id="insert" parameterType="com.example.seata.account.service.pojo.AccountFreeze">
        insert account_freeze (xid,user_id,freeze_money,state)
        values (#{xid}, #{userId}, #{freezeMoney}, #{state})
    </insert>

    <delete id="deleteById" parameterType="String">
        delete from account_freeze where xid=#{xid}
    </delete>

    <update id="updateById" parameterType="com.example.seata.account.service.pojo.AccountFreeze">
        update account_freeze set user_id=#{userId},freeze_money=#{freezeMoney},state=#{state}
        where xid=#{xid}
    </update>
</mapper>
复制代码

4. 创建 Service 接口:AccountTCCService

package com.example.seata.account.service.service;

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

@LocalTCC
public interface AccountTCCService {

    /**
     * Try 逻辑(方法参数根据业务逻辑指定)
     * @TwoPhaseBusinessAction 中的 name 属性的值,需要与方法名一致,用于指定 Try 逻辑对应方法
     */
    @TwoPhaseBusinessAction(name = "decrease", commitMethod = "confirm", rollbackMethod = "cancel")
    void decrease(@BusinessActionContextParameter(paramName = "userId") Long userId,
                @BusinessActionContextParameter(paramName = "money") Double money);

    /**
     * 二阶段确认方法
     *
     * @param ctx 上下文,可以传递 Try 方法参数
     * @return 执行是否成功
     */
    boolean confirm(BusinessActionContext ctx);

    /**
     * 二阶段回滚方法
     *
     * @param ctx 上下文,可以传递 Try 方法参数
     * @return 执行是否成功
     */
    boolean cancel(BusinessActionContext ctx);

}
复制代码

5. 创建 Service 实现类:AccountTCCServiceImpl

package com.example.seata.account.service.service.impl;

import com.example.seata.account.service.mapper.AccountFreezeMapper;
import com.example.seata.account.service.mapper.AccountMapper;
import com.example.seata.account.service.pojo.AccountFreeze;
import com.example.seata.account.service.service.AccountTCCService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountTCCServiceImpl implements AccountTCCService {

    private static final Logger logger = LoggerFactory.getLogger(AccountTCCServiceImpl.class);

    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private AccountFreezeMapper freezeMapper;

    private Integer FREEZE_STATE_TRY = 0;
    private Integer FREEZE_STATE_CONFIRM = 1;
    private Integer FREEZE_STATE_CANCEL = 2;

    @Override
    @Transactional
    public void decrease(Long userId, Double money) {
        logger.info("=== ===》 AccountTCCService-try 开始执行 《=== ===");
        // 获取事务id
        String xid = RootContext.getXID();
        // 业务悬挂判断
        if (freezeMapper.selectById(xid) != null) {
            return;
        }

        //模拟异常,全局事务回滚
        int i = 1 / 0;

        logger.info("扣减账户余额");
        accountMapper.decrease(userId, money);
        logger.info("扣减结束");

        logger.info("记录冻结金额和事务状态");
        // 封装
        AccountFreeze accountFreeze = new AccountFreeze();
        accountFreeze.setXid(xid);
        accountFreeze.setUserId(userId);
        accountFreeze.setFreezeMoney(money);
        accountFreeze.setState(FREEZE_STATE_TRY);
        // 插入数据
        freezeMapper.insert(accountFreeze);
        logger.info("记录完成");

        logger.info("=== ===》 AccountTCCService-try 执行结束 《=== ===");
    }

    @Override
    public boolean confirm(BusinessActionContext ctx) {
        logger.info("=== ===》 AccountTCCService-confirm 开始执行 《=== ===");
        // 获取事务id
        String xid = ctx.getXid();
        // 根据id删除冻结数据
        int count = freezeMapper.deleteById(xid);
        logger.info("=== ===》 AccountTCCService-confirm 执行结束 《=== ===");
        return count == 1;
    }

    @Override
    public boolean cancel(BusinessActionContext ctx) {
        logger.info("=== ===》 AccountTCCService-cancel 开始执行 《=== ===");
        // 获取事务id
        String xid = ctx.getXid();
        // 根据id查询冻结记录
        AccountFreeze accountFreeze = freezeMapper.selectById(xid);
        // 处理空回滚
        if (accountFreeze == null) {
            accountFreeze = new AccountFreeze();
            accountFreeze.setXid(xid);
            accountFreeze.setUserId(Long.parseLong(ctx.getActionContext("userId").toString()));
            accountFreeze.setFreezeMoney(0D);
            accountFreeze.setState(FREEZE_STATE_CANCEL);
            // 插入数据
            freezeMapper.insert(accountFreeze);
            logger.info("空回滚处理完成,并返回");
            return true;
        }

        // 幂等判断
        if (accountFreeze.getState() == FREEZE_STATE_CANCEL) {
            logger.info("已处理,并返回");
            // 已经处理过 cancel
            return true;
        }

        logger.info("恢复可用余额");
        // 恢复可用余额
        accountMapper.refund(accountFreeze.getUserId(), accountFreeze.getFreezeMoney());
        // 将冻结金额清零,状态改为 cancel
        accountFreeze.setFreezeMoney(0D);
        accountFreeze.setState(FREEZE_STATE_CANCEL);
        int count = freezeMapper.updateById(accountFreeze);
        logger.info("恢复完成");

        logger.info("=== ===》 AccountTCCService-confirm cancel 《=== ===");
        return count == 1;
    }
}
复制代码

6. 在 Controller 中添加接口:AccountController

@RequestMapping("/decrease/tcc")
public CommonResult decrease_tcc(@RequestParam("userId") Long userId, @RequestParam("money") Double money){
    accountTCCService.decrease(userId,money);
    return new CommonResult(200, "扣减账户余额成功!");
}
复制代码

seata-order-service 模块修改

1. 在 AccountService 远程调用接口中添加对 /account/decrease/tcc 的调用

@RequestMapping("/account/decrease/tcc")
CommonResult decrease_tcc(@RequestParam("userId") Long userId, @RequestParam("money") Double money);
复制代码

2. 修改 OrderServiceImpl 实现

seata-13.png

测试

无异常,正常提交执行

seata-14

模拟异常,测试回滚

seata-15

五、Saga 模式实现

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

image.png

适用场景:

  • 业务流程长、业务流程多
  • 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口

优势:

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

缺点:

  • 不保证隔离性

基于状态机引擎的 Saga 实现:

目前SEATA提供的Saga模式是基于状态机引擎来实现的,机制是:

  1. 通过状态图来定义服务调用的流程并生成 json 状态语言定义文件

  2. 状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点

  3. 状态图 json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚

注意: 异常发生时是否进行补偿也可由用户自定义决定

  1. 可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能

示例状态图:

image.png

状态机设计器演示地址:seata.io/saga_design…

状态图与 JSON 状态语言实现

image.png

{
  "Name": "buyGoodsOnline",
  "Comment": "buy a goods on line, add order, deduct account, deduct storage ",
  "StartState": "OrderService",
  "Version": "0.0.1",
  "States": {
    "OrderService": {
      "Type": "ServiceTask",
      "ServiceName": "OrderService",
      "ServiceMethod": "save",
      "CompensateState": "DeleteOrder",
      "Next": "ChoiceAccountState",
      "Input": [
        "$.[businessKey]",
        "$.[order]"
      ],
      "Output": {
        "OrderServiceResult": "$.#root"
      },
      "Status": {
        "#root == true": "SU",
        "#root == false": "FA",
        "$Exception{java.lang.Throwable}": "UN"
      },
      "Catch": [
        {
          "Exceptions": [
            "java.lang.Throwable"
          ],
          "Next": "CompensationTrigger"
        }
      ]
    },
    "ChoiceAccountState":{
      "Type": "Choice",
      "Choices":[
        {
          "Expression":"[OrderServiceResult] == true",
          "Next":"AccountService"
        }
      ],
      "Default":"Fail"
    },
    "AccountService": {
      "Type": "ServiceTask",
      "ServiceName": "AccountService",
      "ServiceMethod": "decrease",
      "CompensateState": "CompensateRefundAccount",
      "Next": "ChoiceStorageState",
      "Input": [
        "$.[businessKey]",
        "$.[userId]",
        "$.[money]",
        {
          "throwException" : "$.[mockReduceAccountFail]"
        }
      ],
      "Output": {
        "AccountServiceResult": "$.#root"
      },
      "Status": {
        "#root == true": "SU",
        "#root == false": "FA",
        "$Exception{java.lang.Throwable}": "UN"
      },
      "Catch": [
        {
          "Exceptions": [
            "java.lang.Throwable"
          ],
          "Next": "CompensationTrigger"
        }
      ]
    },
    "ChoiceStorageState":{
      "Type": "Choice",
      "Choices":[
        {
          "Expression":"[AccountServiceResult] == true",
          "Next":"StorageService"
        }
      ],
      "Default":"Fail"
    },
    "StorageService": {
      "Type": "ServiceTask",
      "ServiceName": "StorageService",
      "ServiceMethod": "decrease",
      "CompensateState": "CompensateRecoverStorage",
      "Input": [
        "$.[businessKey]",
        "$.[productId]",
        "$.[count]",
        {
          "throwException" : "$.[mockReduceStorageFail]"
        }
      ],
      "Output": {
        "StorageServiceResult": "$.#root"
      },
      "Status": {
        "#root == true": "SU",
        "#root == false": "FA",
        "$Exception{java.lang.Throwable}": "UN"
      },
      "Catch": [
        {
          "Exceptions": [
            "java.lang.Throwable"
          ],
          "Next": "CompensationTrigger"
        }
      ],
      "Next": "Succeed"
    },
    "DeleteOrder": {
      "Type": "ServiceTask",
      "ServiceName": "OrderService",
      "ServiceMethod": "deleteOrder",
      "Input": [
        "$.[businessKey]",
        "$.[order]"
      ]
    },
    "CompensateRefundAccount": {
      "Type": "ServiceTask",
      "ServiceName": "AccountService",
      "ServiceMethod": "refund",
      "Input": [
        "$.[businessKey]",
        "$.[userId]",
        "$.[money]"
      ]
    },
    "CompensateRecoverStorage": {
      "Type": "ServiceTask",
      "ServiceName": "StorageService",
      "ServiceMethod": "recover",
      "Input": [
        "$.[businessKey]",
        "$.[productId]",
        "$.[count]"
      ]
    },
    "CompensationTrigger": {
      "Type": "CompensationTrigger",
      "Next": "Fail"
    },
    "Succeed": {
      "Type":"Succeed"
    },
    "Fail": {
      "Type":"Fail",
      "ErrorCode": "PURCHASE_FAILED",
      "Message": "purchase failed"
    }
  }
}
复制代码

对于各种状态是如何定义的,可以参照官方文档的 State language referance 章节

对于复杂参数入参的定义,可以参照官方文档的 复杂参数的Input定义 章节

注:官方文档没有具体章节的锚点定位 ,直接到上面链接中,使用 Ctrl + F 搜索。

项目搭建

项目架构如下:

seata-sample-client-service-saga
    |--common-api
    |--seata-account-service
    |--seata-business-service
    |--seata-order-service
    |--seata-storage-service
复制代码

account、order 和 storage 三个服务的实现与上面项目基本一致,这里省略具体代码。

1. business 服务结构

image.png

2. application.properties

略... ...

配置 consul 和 seata 相关即可。

3. my_init.sql

create table seata_state_machine_def
(
    id               varchar(32)  not null comment 'id',
    name             varchar(128) not null comment 'name',
    tenant_id        varchar(32)  not null comment 'tenant id',
    app_name         varchar(32)  not null comment 'application name',
    type             varchar(20) comment 'state language type',
    comment_         varchar(255) comment 'comment',
    ver              varchar(16)  not null comment 'version',
    gmt_create       timestamp(3)    not null comment 'create time',
    status           varchar(2)   not null comment 'status(AC:active|IN:inactive)',
    content          longtext comment 'content',
    recover_strategy varchar(16) comment 'transaction recover strategy(compensate|retry)',
    primary key (id)
);

CREATE TABLE seata_state_machine_inst
(
    id                  VARCHAR(128) NOT NULL COMMENT 'id',
    machine_id          VARCHAR(32) NOT NULL COMMENT 'state machine definition id',
    tenant_id           VARCHAR(32) NOT NULL COMMENT 'tenant id',
    parent_id           VARCHAR(128) COMMENT 'parent id',
    gmt_started         TIMESTAMP(3) NOT NULL COMMENT 'start time',
    business_key        VARCHAR(48) COMMENT 'business key',
    start_params        LONGTEXT COMMENT 'start parameters',
    gmt_end             TIMESTAMP(3) COMMENT 'end time',
    excep               BLOB COMMENT 'exception',
    end_params          LONGTEXT COMMENT 'end parameters',
    STATUS              VARCHAR(2) COMMENT 'status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
    compensation_status VARCHAR(2) COMMENT 'compensation status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
    is_running          TINYINT(1) COMMENT 'is running(0 no|1 yes)',
    gmt_updated         TIMESTAMP(3) NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY unikey_buz_tenant (business_key, tenant_id)
);

CREATE TABLE seata_state_inst
(
    id                       VARCHAR(48)  NOT NULL COMMENT 'id',
    machine_inst_id          VARCHAR(128)  NOT NULL COMMENT 'state machine instance id',
    NAME                     VARCHAR(128) NOT NULL COMMENT 'state name',
    TYPE                     VARCHAR(20) COMMENT 'state type',
    service_name             VARCHAR(128) COMMENT 'service name',
    service_method           VARCHAR(128) COMMENT 'method name',
    service_type             VARCHAR(16) COMMENT 'service type',
    business_key             VARCHAR(48) COMMENT 'business key',
    state_id_compensated_for VARCHAR(50) COMMENT 'state compensated for',
    state_id_retried_for     VARCHAR(50) COMMENT 'state retried for',
    gmt_started              TIMESTAMP(3)    NOT NULL COMMENT 'start time',
    is_for_update            TINYINT(1) COMMENT 'is service for update',
    input_params             LONGTEXT COMMENT 'input parameters',
    output_params            LONGTEXT COMMENT 'output parameters',
    STATUS                   VARCHAR(2)   NOT NULL COMMENT 'status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
    excep                    BLOB COMMENT 'exception',
    gmt_update               TIMESTAMP (3) COMMIT 'update time',
    gmt_end                  TIMESTAMP(3) COMMENT 'end time',
    PRIMARY KEY (id, machine_inst_id)
);
复制代码

4. BusinessController,对外访问接口

package com.example.seata.business.service.controller;

import com.example.common.pojo.Order;
import com.example.seata.business.service.saga.SagaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class BusinessController {

    @Autowired
    private SagaService service;

    @RequestMapping("/business/create")
    public String create(@RequestBody Order order) {
        service.create(order);
        return "创建订单完成... ...";
    }
}
复制代码

5. SagaService,创建状态机并驱动 JSON 状态语言执行

package com.example.seata.business.service.saga;

import com.example.common.pojo.Order;
import com.example.seata.business.service.util.ApplicationContextUtils;
import io.seata.saga.engine.StateMachineEngine;
import io.seata.saga.statelang.domain.ExecutionStatus;
import io.seata.saga.statelang.domain.StateMachineInstance;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import java.util.HashMap;
import java.util.Map;

@Service
public class SagaService {
    private static final Logger logger = LoggerFactory.getLogger(SagaService.class);

    public boolean create(Order order) {
        logger.info("=== ===》 交易开始 《=== ===");

        StateMachineEngine stateMachineEngine = (StateMachineEngine) ApplicationContextUtils.getApplicationContext().getBean("stateMachineEngine");

        Map<String, Object> startParams = new HashMap<>(3);
        String businessKey = String.valueOf(System.currentTimeMillis());
        startParams.put("businessKey", businessKey);
        startParams.put("order", order);
        startParams.put("mockReduceAccountFail", "true");
        startParams.put("userId", order.getUserId());
        startParams.put("money", order.getMoney());
        startParams.put("productId", order.getProductId());
        startParams.put("count", order.getCount());

        //sync test
        StateMachineInstance inst = stateMachineEngine.startWithBusinessKey("buyGoodsOnline", null, businessKey, startParams);

        Assert.isTrue(ExecutionStatus.SU.equals(inst.getStatus()), "saga transaction execute failed. XID: " + inst.getId());
        System.out.println("saga transaction commit succeed. XID: " + inst.getId());

        inst = stateMachineEngine.getStateMachineConfig().getStateLogStore().getStateMachineInstanceByBusinessKey(businessKey, null);
        Assert.isTrue(ExecutionStatus.SU.equals(inst.getStatus()), "saga transaction execute failed. XID: " + inst.getId());

        return true;
    }

}
复制代码

6. AccountService 接口和实现类,被状态机调用

package com.example.seata.business.service.service.impl;

import com.example.seata.business.service.feign.AccountFeignService;
import com.example.seata.business.service.service.AccountService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service("AccountService")
public class AccountServiceImpl implements AccountService {
    private static final Logger logger = LoggerFactory.getLogger(AccountServiceImpl.class);

    @Autowired
    private AccountFeignService accountFeignService;

    @Override
    public Boolean decrease(String businessKey, Long userId, Double money) {
        logger.info("远程调用 seata-account-service 的 decrease 方法 ===》");
        return accountFeignService.decrease(businessKey, userId, money);
    }

    @Override
    public Boolean refund(String businessKey, Long userId, Double money) {
        logger.info("远程调用 seata-account-service 的 compensateDecrease 方法 ===》");
        return accountFeignService.refund(businessKey, userId, money);
    }
}
复制代码

注:

@Service("AccountService") 要与 JSON 文件中的 ServiceName 对应;

方法名需要与 JSON 文件中的 ServiceMethod 对应;

方法参数要与 JSON 文件中的 Input 保持一致。

order 与 storage 略... ...

7. AccountFeignService,远程调用 account 服务

package com.example.seata.business.service.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

@FeignClient(value = "seata-account-service")
public interface AccountFeignService {

    /**
     * 扣减账户余额
     */
    @RequestMapping("/account/decrease")
    Boolean decrease(@RequestParam("businessKey") String businessKey, @RequestParam("userId") Long userId, @RequestParam("money") Double money);

    /**
     * 交易补偿
     */
    @RequestMapping("/account/compensateRefund")
    Boolean refund(@RequestParam("businessKey") String businessKey, @RequestParam("userId") Long userId, @RequestParam("money") Double money);
}
复制代码

order 与 storage 略... ...

8. StateMachineConfiguration,状态机配置类

package com.example.seata.business.service.config;

import io.seata.saga.engine.config.DbStateMachineConfig;
import io.seata.saga.engine.impl.ProcessCtrlStateMachineEngine;
import io.seata.saga.rm.StateMachineEngineHolder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.scheduling.concurrent.ThreadPoolExecutorFactoryBean;

import javax.sql.DataSource;
import java.io.IOException;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
public class StateMachineConfiguration {

    @Bean
    public ThreadPoolExecutorFactoryBean threadExecutor(){
        ThreadPoolExecutorFactoryBean threadExecutor = new ThreadPoolExecutorFactoryBean();
        threadExecutor.setThreadNamePrefix("SAGA_ASYNC_EXE_");
        threadExecutor.setCorePoolSize(1);
        threadExecutor.setMaxPoolSize(20);
        return threadExecutor;
    }

    @Bean
    public DbStateMachineConfig dbStateMachineConfig(ThreadPoolExecutorFactoryBean threadExecutor, DataSource druidDataSource) throws IOException {
        DbStateMachineConfig dbStateMachineConfig = new DbStateMachineConfig();
        dbStateMachineConfig.setDataSource(druidDataSource);
        dbStateMachineConfig.setThreadPoolExecutor((ThreadPoolExecutor) threadExecutor.getObject());
        dbStateMachineConfig.setResources(new PathMatchingResourcePatternResolver().getResources("classpath*:statelang/*.json"));
        dbStateMachineConfig.setEnableAsync(true);
        dbStateMachineConfig.setApplicationId("seata-sample-service-saga");
        dbStateMachineConfig.setTxServiceGroup("tx_group");
        return dbStateMachineConfig;
    }

    @Bean
    public ProcessCtrlStateMachineEngine stateMachineEngine(DbStateMachineConfig dbStateMachineConfig){
        ProcessCtrlStateMachineEngine stateMachineEngine = new ProcessCtrlStateMachineEngine();
        stateMachineEngine.setStateMachineConfig(dbStateMachineConfig);
        return stateMachineEngine;
    }

    @Bean
    public StateMachineEngineHolder stateMachineEngineHolder(ProcessCtrlStateMachineEngine stateMachineEngine){
        StateMachineEngineHolder stateMachineEngineHolder = new StateMachineEngineHolder();
        stateMachineEngineHolder.setStateMachineEngine(stateMachineEngine);
        return stateMachineEngineHolder;
    }
}
复制代码

9. ApplicationUtils 工具类

package com.example.seata.business.service.util;

import org.springframework.context.ApplicationContext;

public class ApplicationContextUtils {

    private static ApplicationContext applicationContext;

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public static void setApplicationContext(ApplicationContext applicationContext) {
        ApplicationContextUtils.applicationContext = applicationContext;
    }
}
复制代码

10. 在启动类开启 consul 和 OpenFiegn 功能

package com.example.seata.business.service;

import com.example.seata.business.service.util.ApplicationContextUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.ApplicationContext;

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableDiscoveryClient
@EnableFeignClients
public class SeataBusinessServiceApplication {

    public static void main(String[] args) {
        ApplicationContext applicationContext = SpringApplication.run(SeataBusinessServiceApplication.class, args);
        ApplicationContextUtils.setApplicationContext(applicationContext);
    }

}
复制代码

11. 启动测试

事务提交

image.png

事务回滚

image.png

参考

Seata 中文文档:seata.io/zh-cn/docs/…

博文:

juejin.cn/post/712118…

blog.51cto.com/u_15095774/…

cloud.tencent.com/developer/a…

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改