SpringCloud - Seata Distributed Transaction Service of Spring Cloud Alibaba; AT Transaction Mode (20)

Before reading this article, please refer to:

https://blog.csdn.net/MinggeQingchun/article/details/126100300

https://blog.csdn.net/MinggeQingchun/article/details/126176893

AT mode

Seata AT mode

premise

1. Based on a relational database that supports local ACID transactions

2. Java application, accessing the database through JDBC

overall mechanism

Evolution of the two-phase commit protocol:

1. Phase 1: Business data and rollback log records are committed in the same local transaction, releasing local locks and connection resources

2. The second stage:

(1) The submission is asynchronous and completed very quickly

(2) Rollback is reversely compensated through a one-stage rollback log

write isolation

1. Before committing a local transaction in the first stage, you need to ensure that you get  the global lock first. 

2. If  the global lock cannot be obtained  , the local transaction cannot be submitted

3. The attempt to take  the global lock  is limited within a certain range. If it exceeds the range, it will give up and roll back the local transaction to release the local lock.

read isolation

Based on the database local transaction isolation level of  Read Committed (Read Committed)  or above, the default global isolation level of Seata (AT mode) is  Read Uncommitted (Read Uncommitted)  .

If the application is in a specific scenario, it is necessary to require global  read-committed  . The current Seata method is through the proxy of the SELECT FOR UPDATE statement.

1. AT transaction mode: SpringBoot single application multi-data source AT distributed transaction

In the Spring Boot monomer project, if multiple data sources are used, it is necessary to ensure the data consistency of multiple data sources, that is, the problem of distributed transactions occurs. Seata's AT transaction mode is used to solve the distributed transaction problem.

Take the following picture as an example to place an order

1. Create database, table, insert data, etc.

(1) accountdb account library, account account table

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for account
-- ----------------------------
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account`  (
  `id` int(20) NOT NULL AUTO_INCREMENT,
  `user_id` int(20) NULL DEFAULT NULL,
  `balance` decimal(20, 0) NULL DEFAULT NULL,
  `update_time` datetime(6) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

(2) productdb product library, product product table

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for product
-- ----------------------------
DROP TABLE IF EXISTS `product`;
CREATE TABLE `product`  (
  `id` int(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `price` decimal(10, 2) NULL DEFAULT NULL,
  `stock` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `add_time` datetime(6) NULL DEFAULT NULL,
  `update_time` datetime(6) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

(3) orderdb order library, orders order table

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for orders
-- ----------------------------
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders`  (
  `id` int(20) NOT NULL AUTO_INCREMENT,
  `user_id` int(20) NULL DEFAULT NULL,
  `product_id` int(20) NULL DEFAULT NULL,
  `pay_amount` decimal(20, 0) NULL DEFAULT NULL,
  `add_time` datetime(6) NULL DEFAULT NULL,
  `update_time` datetime(6) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

(4) undo_log table 

Seata AT mode

-- 注意此处0.7.0+ 增加字段 context
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,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

Note:

Each library must create  an undo_log  table, which is a table that must be created in Seata AT mode, mainly used for the rollback of branch transactions

Seata AT mode

 

2. Create a SpringBoot single application

1. Create a springboot application named springcloud-alibaba-2-seata-distributed-transaction

2. Add dependencies (non-Spring CLoud microservice projects, no Spring CLoud dependencies)

<groupId>com.company</groupId>
    <artifactId>springcloud-alibaba-2-seata-distributed-transaction</artifactId>
    <version>1.0.0</version>

    <name>springcloud-alibaba-2-seata-distributed-transaction</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.12.RELEASE</spring-boot.version>
        <spring-cloud-alibaba.version>2.2.7.RELEASE</spring-cloud-alibaba.version>
    </properties>

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

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

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

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

        <!-- seata-spring-boot-starter -->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>

        <!-- dynamic-datasource-spring-boot-starter动态数据源 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>

    </dependencies>

    <!-- dependencyManagement标签 通常适用于多模块环境下定义一个top module来专门管理公共依赖的情况
        在子项目中不写该依赖项,那么子项目仍然会从父项目depenManagement中继承该artifactId和groupId依赖项(全部继承)
        若子项目 中dependencies中的dependency声明了version,则父项目中dependencyManagement中的声明无效

        Spring Cloud、Spring Cloud Alibaba 以及 Spring Boot 之间版本依赖参考官网
        https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E
    -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- spring-cloud-dependencies -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR12</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <!--mybatis代码自动生成插件-->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.4.0</version>
                <configuration>
                    <!--配置文件的位置-->
                    <configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
                    <!--生成代码过程中是否打印日志-->
                    <verbose>true</verbose>
                    <!--生成时是否覆盖java文件,xml文件总是合并-->
                    <overwrite>true</overwrite>
                </configuration>
            </plugin>
        </plugins>

        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.*</include>
                </includes>
            </resource>
        </resources>
    </build>

3. application.properties configuration file

#内嵌服务器端口
server.port=8081

#应用服务名称
spring.application.name=springcloud-alibaba-2-seata-distributed-transaction

# 设置默认的数据源或者数据源组,默认值即为master
spring.datasource.dynamic.primary=order-ds

# 订单order数据源配置
spring.datasource.dynamic.datasource.order-ds.url=jdbc:mysql://localhost:3306/orderdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
spring.datasource.dynamic.datasource.order-ds.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.order-ds.username=root
spring.datasource.dynamic.datasource.order-ds.password=admin123456

# 商品product数据源配置
spring.datasource.dynamic.datasource.product-ds.url=jdbc:mysql://localhost:3306/productdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
spring.datasource.dynamic.datasource.product-ds.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.product-ds.username=root
spring.datasource.dynamic.datasource.product-ds.password=admin123456

# 账户account数据源配置
spring.datasource.dynamic.datasource.account-ds.url=jdbc:mysql://localhost:3306/accountdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
spring.datasource.dynamic.datasource.account-ds.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.account-ds.username=root
spring.datasource.dynamic.datasource.account-ds.password=admin123456

# 是否启动对Seata的集成
spring.datasource.dynamic.seata=true


#-----------------------------------------------------------
#单机版 tc server 配置
# Seata应用编号,默认为 ${spring.application.name}
seata.application-id=springboot-seata
# Seata事务组编号,用于TC集群名,一般格式为:${spring.application.name}-group
seata.tx-service-group=springboot-seata-group
# 虚拟组和分组的映射 seata.service.vgroup-mapping.${seata.tx-service-group}=default
seata.service.vgroup-mapping.springboot-seata-group=default
# 分组和Seata服务的映射,此处default指上面 seata.service.vgroup-mapping.springboot-seata-group 的值 default
seata.service.grouplist.default=192.168.133.129:8091
# 存储模式 默认 file模式
seata.config.type=file
# 默认为 file
seata.registry.type=file
#------------------------------------------------------------

4. Write the corresponding controller, model, mapper, and service classes. Only the classes related to the calling sequence are given here

 controller test class

@Slf4j //lombok
@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

    @RequestMapping("/order")
    public Integer createOrder(@RequestParam("userId") Integer userId,
                               @RequestParam("productId") Integer productId) throws Exception {

        log.info("请求下单, 用户:{}, 商品:{}", userId, productId);

        return orderService.createOrder(userId, productId);
    }
}

order logic class

Note:

(1) @DS annotation; switching between multiple data sources

(2) @GlobalTransactional annotation; seata global transaction annotation

The main service can be annotated with @GlobalTransactional, and the called service does not need to add @GlobalTransactional and @Transactional

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrdersMapper ordersMapper;

    @Autowired
    private AccountService accountService;

    @Autowired
    private ProductService productService;

    @Override
    /**
     * MyBatis-Plus 使用 @DS注解 做多数据源切换
     * 语法:@DS(value = "数据源名称")
     * 1、依赖:
     *         <dependency>
     *             <groupId>com.baomidou</groupId>
     *             <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
     *             <version>3.0.0</version>
     *         </dependency>
     * 2、yml 或 properties 配置
     * # 设置默认的数据源或者数据源组,默认值即为master
     * spring.datasource.dynamic.primary=order-ds
     *
     * # 订单order数据源配置
     * spring.datasource.dynamic.datasource.order-ds.url=jdbc:mysql://localhost:3306/orderdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
     * spring.datasource.dynamic.datasource.order-ds.driver-class-name=com.mysql.cj.jdbc.Driver
     * spring.datasource.dynamic.datasource.order-ds.username=root
     * spring.datasource.dynamic.datasource.order-ds.password=admin123456
     *
     * # 商品product数据源配置
     * spring.datasource.dynamic.datasource.product-ds.url=jdbc:mysql://localhost:3306/productdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
     * spring.datasource.dynamic.datasource.product-ds.driver-class-name=com.mysql.cj.jdbc.Driver
     * spring.datasource.dynamic.datasource.product-ds.username=root
     * spring.datasource.dynamic.datasource.product-ds.password=admin123456
     * 3、@DS注解到实现类或者实现类的方法上才可以
     * 当注解添加到类上,意味着此类里的方法都使用此数据源;
     * 当注解添加到方法上时,意味着此方法上使用的数据源优先级高于其他一切配置
     * 注:
     * (1)注解添加在dao.mapper上无效
     * (2)注解添加到interface Service类上无效
     * (3)注解添加到interface Service方法上无效
     */
    @DS(value = "order-ds")
    @GlobalTransactional //seata全局事务注解
    public Integer createOrder(Integer userId, Integer productId) throws Exception {
        Integer amount = 1; // 购买数量暂时设置为 1

        log.info("当前 XID: {}", RootContext.getXID());

        //1、减库存
        Product product = productService.reduceStock(productId, amount);

        //2、减余额
        accountService.reduceBalance(userId, product.getPrice());

        //3、下订单
        Orders order = new Orders();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setPayAmount(product.getPrice().multiply(new BigDecimal(amount)));
        order.setAddTime(new Date());
        ordersMapper.insertSelective(order);

        //造成异常,测试是否回滚
        //int a = 10/0;

        log.info("下订单: {}", order.getId());

        // 返回订单编号
        return order.getId();
    }
}

 product logic class

@Slf4j
@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private ProductMapper productMapper;

    @Override
    @DS(value = "product-ds")
    public Product reduceStock(Integer productId, Integer amount) throws Exception {
        log.info("当前 XID: {}", RootContext.getXID());

        // 检查库存
        Product product = productMapper.selectByPrimaryKey(productId);
        if (product.getStock() < amount) {
            throw new Exception("库存不足");
        }

        // 扣减库存
        int updateCount = productMapper.reduceStock(productId, amount);
        // 扣除成功
        if (updateCount == 0) {
            throw new Exception("库存不足");
        }

        //造成异常,测试是否回滚
        //int a = 10/0;

        // 扣除成功
        log.info("扣除 {} 库存成功", productId);

        return product;
    }
}

account logic class

@Slf4j
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    @DS(value = "account-ds")
    public void reduceBalance(Integer userId, BigDecimal money) throws Exception {
        log.info("当前 XID: {}", RootContext.getXID());

        // 检查余额
        Account account = accountMapper.selectAccountByUserId(userId);
        if (account.getBalance().doubleValue() < money.doubleValue()) {
            throw new Exception("余额不足");
        }

        // 扣除余额
        int updateCount = accountMapper.reduceBalance(userId, money);
        // 扣除成功
        if (updateCount == 0) {
            throw new Exception("余额不足");
        }

        //造成异常,测试是否回滚
        //int a = 10/0;

        log.info("扣除用户 {} 余额成功", userId);
    }
}

5. Start the seata-server; enter the browser to visit  http://localhost:8081/order?userId=1&productId=1

You can write the following code in the implementation classes of OrderServiceImpl, ProductServiceImpl, and AccountServiceImpl respectively to perform transaction rollback testing

//造成异常,测试是否回滚
int a = 10/0;

@DS annotation 

MyBatis-Plus uses the @DS annotation to switch multiple data sources 

grammar:

@DS(value = "data source name")

1. Rely on:

<!-- dynamic-datasource-spring-boot-starter动态数据源 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>

2. yml or properties configuration 

# 设置默认的数据源或者数据源组,默认值即为master
spring.datasource.dynamic.primary=order-ds

# 订单order数据源配置
spring.datasource.dynamic.datasource.order-ds.url=jdbc:mysql://localhost:3306/orderdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
spring.datasource.dynamic.datasource.order-ds.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.order-ds.username=root
spring.datasource.dynamic.datasource.order-ds.password=admin123456

# 商品product数据源配置
spring.datasource.dynamic.datasource.product-ds.url=jdbc:mysql://localhost:3306/productdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
spring.datasource.dynamic.datasource.product-ds.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.product-ds.username=root
spring.datasource.dynamic.datasource.product-ds.password=admin123456

# 账户account数据源配置
spring.datasource.dynamic.datasource.account-ds.url=jdbc:mysql://localhost:3306/accountdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
spring.datasource.dynamic.datasource.account-ds.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.account-ds.username=root
spring.datasource.dynamic.datasource.account-ds.password=admin123456

3. @DS can only be annotated to the implementation class or the method of the implementation class

When the annotation is added to the class, it means that the methods in this class use this data source;

When the annotation is added to the method, it means that the data source used on this method has priority over all other configurations

Note:

(1) Adding annotations is invalid on dao.mapper

(2) Adding annotations to the interface Service class is invalid

(3) Notes added to the interface Service method are invalid

Note:

If the try catch is abnormal, the transaction will not be rolled back

2. AT transaction mode: Spring Cloud Alibaba microservice AT distributed transaction

1. Temporarily close the firewall on Linux, or set port access permissions

systemctl stop firewalld

2. Because Nacos uses mysql persistence, you need to start the mysql service first (manual installation or docker startup, docker startup needs to start docker and mount the mysql service first, otherwise restarting docker or mysql will cause mysql data loss)

3. First start the nacos service

sh startup.sh -m standalone

The stand-alone environment must be started with the -m standalone parameter; the cluster environment is started without parameters 

4. Because of the Nacos registration center used by Seata server, you need to configure the conf/registry.conf file, select nacos

Seata Server configuration
In config/registry.conf under the Seata Server installation directory, change the configuration mode (config.type) to Nacos, and configure the relevant information of the Nacos configuration center

If the registration center is used, such as type="nacos", etc., check whether the nacos application name application, service registration address serverAddr, group group, namespace namespace, cluster cluster, username username, and password password are correct, etc.

config {
  # Seata 支持 file、nacos 、apollo、zk、consul、etcd3 等多种配置中心
  #配置方式修改为 nacos
  type = "nacos"
 
  nacos {
    #修改为使用的 nacos 服务器地址
    serverAddr = "127.0.0.1:8848"
    #配置中心的命名空间
    namespace = ""
    #配置中心所在的分组
    group = "SEATA_GROUP"
    #Nacos 配置中心的用户名
    username = "nacos"
    #Nacos 配置中心的密码
    password = "nacos"
  }
}

Otherwise, an error will be reported:

no available service found in cluster 'default', please make sure registry config correct and keep your seata server running

Seata Client configuration

In Seata Client (that is, the service in the microservice architecture), configure the Nacos configuration center through configuration files such as application.yml

#-----------------------------------------------------------
#单机版 tc server 配置
# Seata应用编号,默认为 ${spring.application.name}
seata.application-id=springcloud-order-seata
# Seata事务组编号,用于TC集群名,一般格式为:${spring.application.name}-group
seata.tx-service-group=springcloud-order-seata-group
# 注:虚拟组和分组的映射要写对,不然报错:
# no available service 'null' found, please make sure registry config correct
# 虚拟组和分组的映射 seata.service.vgroup-mapping.${seata.tx-service-group}=default
seata.service.vgroup-mapping.springcloud-order-seata-group=default
# 分组和Seata服务的映射,此处default指上面 seata.service.vgroup-mapping.springboot-seata-group 的值 default
#seata.service.grouplist.default=192.168.133.129:8091
# 存储模式 默认 file模式
seata.config.type=file
# 默认为 file
#seata.registry.type=file
#------------------------------------------------------------
 
 
#设置使用注册中心
#seata-spring-boot-starter 1.1版本少一些配置项
seata.enabled=true
seata.registry.type=nacos
# 集群
seata.registry.nacos.cluster=default
# 分组
seata.registry.nacos.group=SEATA_GROUP
# 应用名
seata.registry.nacos.application=seata-server
# 服务注册地址
seata.registry.nacos.server-addr=192.168.133.129:8848

Note:! ! !

Seata application number seata.application-id, the default is ${spring.application.name}

Seata transaction group number seata.tx-service-group, used for TC cluster name, the general format is: ${spring.application.name}-group

Seata virtual group and group mapping seata.service.vgroup-mapping.${seata.tx-service-group}=default

The correspondence between the three should be written correctly, otherwise an error will be reported:

no available service 'null' found, please make sure registry config correct

test application

1. Create 4 SpringBoot modules

(1)springcloud-alibaba-2-seata-distributed-commons

<dependencies>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>

        <!--spring-cloud-starter-openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>3.0.0</version>
        </dependency>
    </dependencies>
@FeignClient(name = "springcloud-alibaba-2-seata-distributed-account")
public interface FeignAccountService {

    /**
     * 扣除余额
     *
     * @param userId 用户ID
     * @param money  扣减金额
     * @throws Exception 失败时抛出异常
     */
    @PostMapping("/account/reduceBalance")
    void reduceBalance(@RequestParam("userId") Integer userId, @RequestParam("money") BigDecimal money);
}


@FeignClient(name = "springcloud-alibaba-2-seata-distributed-order")
public interface FeignOrderService {

    /**
     * 创建订单
     *
     * @param userId 用户ID
     * @param productId 产品ID
     * @return 订单编号
     * @throws Exception 创建订单失败,抛出异常
     */
    Integer createOrder(Integer userId, Integer productId) throws Exception;

}

@FeignClient(name = "springcloud-alibaba-2-seata-distributed-product")
public interface FeignProductService {

    /**
     * 减库存
     *
     * @param productId 商品ID
     * @param amount    扣减数量
     * @throws Exception 扣减失败时抛出异常
     */
    @PostMapping("/product/reduceStock")
    Product reduceStock(@RequestParam("productId") Integer productId, @RequestParam("amount") Integer amount);
}

(2)springcloud-alibaba-2-seata-distributed-order

Notice:

1. The exception needs to be thrown up layer by layer. If you handle the exception in the sub-service (such as GlobalExceptionHandler), seata will think that you have manually handled the exception

2. In case of transaction failure, first check RootContext.getXID(), whether the xid is passed and consistent

3. The main service can be annotated with @GlobalTransactional, and the called service does not need to add @GlobalTransactional and @Transactional

4. @GlobalTransactional(rollbackFor = Exception.class) is best to add rollbackFor = Exception.class, which means that Exception is rolled back, otherwise some exceptions (such as custom exceptions) will not be rolled back

@Slf4j //lombok
@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

    @RequestMapping("/order")
    public Integer createOrder(@RequestParam("userId") Integer userId,
                               @RequestParam("productId") Integer productId) throws Exception {

        log.info("请求下单, 用户:{}, 商品:{}", userId, productId);

        return orderService.createOrder(userId, productId);
    }
}


@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrdersMapper ordersMapper;

    @Autowired
    private FeignAccountService accountService;

    @Autowired
    private FeignProductService productService;

    @Override
    @GlobalTransactional //seata全局事务注解
    public Integer createOrder(Integer userId, Integer productId) {
        Integer amount = 1; // 购买数量暂时设置为 1

        log.info("当前 XID: {}", RootContext.getXID());

        //1、减库存
        Product product = productService.reduceStock(productId, amount);

        //2、减余额
        accountService.reduceBalance(userId, product.getPrice());

        //3、下订单
        Orders order = new Orders();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setPayAmount(product.getPrice().multiply(new BigDecimal(amount)));
        order.setAddTime(new Date());
        ordersMapper.insertSelective(order);

        //造成异常,测试是否回滚
        int a = 10/0;

        log.info("下订单: {}", order.getId());

        // 返回订单编号
        return order.getId();
    }
}
<groupId>com.company</groupId>
    <artifactId>springcloud-alibaba-2-seata-distributed-order</artifactId>
    <version>1.0.0</version>

    <name>springcloud-alibaba-2-seata-distributed-order</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.12.RELEASE</spring-boot.version>
        <spring-cloud-alibaba.version>2.2.7.RELEASE</spring-cloud-alibaba.version>
    </properties>

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

        <!--spring-cloud-starter-alibaba-sentinel-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

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

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

        <!-- spring-cloud-starter-alibaba-seata
            在 Spring Cloud 项目中,spring-cloud依赖 也会引入 seata-spring-boot-starter 依赖,在此排除
         -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- seata-spring-boot-starter
            注:服务端和客户端版本要一致,不然报错:
            no available service 'default' found, please make sure registry config correct
         -->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.4.2</version>
        </dependency>

        <!--统一通用项目,model类、openfeign接口-->
        <dependency>
            <groupId>com.company</groupId>
            <artifactId>springcloud-alibaba-2-seata-distributed-commons</artifactId>
            <version>1.0.0</version>
        </dependency>

    </dependencies>

    <!-- dependencyManagement标签 通常适用于多模块环境下定义一个top module来专门管理公共依赖的情况
        在子项目中不写该依赖项,那么子项目仍然会从父项目depenManagement中继承该artifactId和groupId依赖项(全部继承)
        若子项目 中dependencies中的dependency声明了version,则父项目中dependencyManagement中的声明无效

        Spring Cloud、Spring Cloud Alibaba 以及 Spring Boot 之间版本依赖参考官网
        https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E
    -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- spring-cloud-dependencies -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR12</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>

        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.*</include>
                </includes>
            </resource>
        </resources>
    </build>

Note:! ! !

The seata-spring-boot-starter server and client versions must be consistent, otherwise an error will be reported:

no available service 'default' found, please make sure registry config correct
server.port=8081

spring.application.name=springcloud-alibaba-2-seata-distributed-order

spring.datasource.url=jdbc:mysql://192.168.133.129:3306/orderdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useSSL=false
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456

#nacos服务的注册与发现
spring.cloud.nacos.discovery.server-addr=192.168.133.129:8848
# 用户名、密码为默认时,测试发现不写 用户名、密码也可以
spring.cloud.nacos.username=nacos
spring.cloud.nacos.password=nacos


#-----------------------------------------------------------
#单机版 tc server 配置
# Seata应用编号,默认为 ${spring.application.name}
seata.application-id=springcloud-order-seata
# Seata事务组编号,用于TC集群名,一般格式为:${spring.application.name}-group
seata.tx-service-group=springcloud-order-seata-group
# 注:虚拟组和分组的映射要写对,不然报错:
# no available service 'null' found, please make sure registry config correct
# 虚拟组和分组的映射 seata.service.vgroup-mapping.${seata.tx-service-group}=default
seata.service.vgroup-mapping.springcloud-order-seata-group=default
# 分组和Seata服务的映射,此处default指上面 seata.service.vgroup-mapping.springboot-seata-group 的值 default
#seata.service.grouplist.default=192.168.133.129:8091
# 存储模式 默认 file模式
seata.config.type=file
# 默认为 file
#seata.registry.type=file
#------------------------------------------------------------


#设置使用注册中心
#seata-spring-boot-starter 1.1版本少一些配置项
seata.enabled=true
seata.registry.type=nacos
# 集群
seata.registry.nacos.cluster=default
# 分组
seata.registry.nacos.group=SEATA_GROUP
# 应用名
seata.registry.nacos.application=seata-server
# 服务注册地址
seata.registry.nacos.server-addr=192.168.133.129:8848

#feign超时时间设置
feign.client.config.default.connect-timeout=60000
feign.client.config.default.read-timeout=60000

(3)springcloud-alibaba-2-seata-distributed-product

@Slf4j
@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private ProductMapper productMapper;

    @Override
    public Product reduceStock(Integer productId, Integer amount) throws Exception {
        log.info("当前 XID: {}", RootContext.getXID());

        // 检查库存
        Product product = productMapper.selectByPrimaryKey(productId);
        if (product.getStock() < amount) {
            throw new Exception("库存不足");
        }

        // 扣减库存
        int updateCount = productMapper.reduceStock(productId, amount);
        // 扣除成功
        if (updateCount == 0) {
            throw new Exception("库存不足");
        }

        //造成异常,测试是否回滚
        //int a = 10/0;

        // 扣除成功
        log.info("扣除 {} 库存成功", productId);

        return product;
    }
}

(4)springcloud-alibaba-2-seata-distributed-account

@Slf4j
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    public void reduceBalance(Integer userId, BigDecimal money) throws Exception {
        log.info("当前 XID: {}", RootContext.getXID());

        // 检查余额
        Account account = accountMapper.selectAccountByUserId(userId);
        if (account.getBalance().doubleValue() < money.doubleValue()) {
            throw new Exception("余额不足");
        }

        // 扣除余额
        int updateCount = accountMapper.reduceBalance(userId, money);
        // 扣除成功
        if (updateCount == 0) {
            throw new Exception("余额不足");
        }

        //造成异常,测试是否回滚
        //int a = 10/0;

        log.info("扣除用户 {} 余额成功", userId);
    }
}

2. Start Nacos first, and then start Seata-Server

The list of registered services in Nacos is as follows: 

Note:

If the try catch is abnormal, the transaction will not be rolled back

Three, encounter problems

1. There is a problem starting the seata server

The error is as follows:

Failed to retry rollbacking [192.168.133.129:8091:702852926242021399] Unknown java.lang.RuntimeException: rm client is not connected.
dbkey:jdbc:mysql://localhost:3306/orderdb,clientId:springboot-seata:192.168.133.1:64279

Because the previous blogger tested the AT transaction mode: single application multi-data source distributed transaction, resulting in rollback data in /bin/sessionStore/root.data, but the connected database url is wrong, modify it or delete it directly

rm -rf root.data 

just restart

You can also refer to the error report when starting the seata server

https://blog.csdn.net/MinggeQingchun/article/details/126172351

2、 no available service 'null' found, please make sure registry config correct

no available service 'default' found, please make sure registry config correct

no available service found in cluster 'default', please make sure registry config correct and keep your seata server running

When starting the project, the following three errors occurred because the Seata service was registered to Nacos:

(1) no available service 'null' found, please make sure registry config correct

Seata application number seata.application-id, the default is ${spring.application.name}

Seata transaction group number seata.tx-service-group, used for TC cluster name, the general format is: ${spring.application.name}-group

Seata virtual group and group mapping seata.service.vgroup-mapping.${seata.tx-service-group}=default

The correspondence between the three should be written correctly, otherwise an error will be reported:

no available service 'null' found, please make sure registry config correct

(2)no available service 'default' found, please make sure registry config correct

Blogger Seata Server uses version 1.4.2

<!-- spring-cloud-starter-alibaba-seata
            在 Spring Cloud 项目中,spring-cloud依赖 也会引入 seata-spring-boot-starter 依赖,在此排除
         -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
 
        <!-- seata-spring-boot-starter
            注:服务端和客户端版本要一致,不然报错:
            no available service 'default' found, please make sure registry config correct
         -->
 
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.4.2/version>
</dependency>
 
<dependency>
    <groupId>com.alibaba.nacos</groupId>
    <artifactId>nacos-client</artifactId>
    <version>1.2.0及以上版本</version>
</dependency>

The seata-spring-boot-starter server and client versions must be consistent, otherwise an error will be reported:

no available service 'default' found, please make sure registry config correct

(3)no available service found in cluster 'default', please make sure registry config correct and keep your seata server running

Use the registration center, such as type="nacos", etc. to check whether the nacos application name application, service registration address serverAddr, group group, namespace namespace, cluster cluster, username username, and password password are correct, etc.

config {
  # Seata 支持 file、nacos 、apollo、zk、consul、etcd3 等多种配置中心
  #配置方式修改为 nacos
  type = "nacos"
 
  nacos {
    #修改为使用的 nacos 服务器地址
    serverAddr = "127.0.0.1:8848"
    #配置中心的命名空间
    namespace = ""
    #配置中心所在的分组
    group = "SEATA_GROUP"
    #Nacos 配置中心的用户名
    username = "nacos"
    #Nacos 配置中心的密码
    password = "nacos"
  }
}

Otherwise, an error will be reported:

no available service found in cluster 'default', please make sure registry config correct and keep your seata server running

3. The @GlobalTransactional annotation is invalid, and the transaction will not be rolled

For details, please refer to

Frequently Asked Questions

Dynamically create/close Seata distributed transactions through AOP

Integrating spring-cloud-starter-alibaba-seata @GlobalTransactional failure problem

openfeign+seata+zipkin request service stack overflow exception problem through feign_Stranger's Charm Blog-CSDN Blog

feign calls the service, the called service seata transaction is not opened or the xid is empty -CSDN blog_seata rollback does not take effect

 Seata transaction cannot be rolled back_slivloon's blog-CSDN blog_seata transaction does not roll back

Guess you like

Origin blog.csdn.net/MinggeQingchun/article/details/126153368