Spring Boot集成Seata利用AT模式分布式事务示例 | Spring Cloud 53

一、前言

通过以下系列章节:

docker-compose 实现Seata Server高可用部署 | Spring Cloud 51

Seata AT 模式理论学习、事务隔离及部分源码解析 | Spring Cloud 52

我们对Seata及其AT事务模式有了基础的了解,今天我们通过搭建Spring Boot集成Seata示例,加深对AT事务模式的掌握,避免在后续实战业务中出现脏写及脏读。

二、前置条件

2.1 完成Seata Server安装

具体安装步骤,请见:docker-compose 实现Seata Server高可用部署 | Spring Cloud 51

2.2 创建表 undo_log

Seata AT事务模式需要在每一个微服务对应的数据库中创建表:undo_log(回滚记录表)

  • TMRMClient端)可通过client.undo.logTable属性,配置自定义undo表名,默认undo_log

Mysql建表语句:

-- 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';

其他数据库建表语句请见官网:https://github.com/seata/seata/tree/1.6.1/script/client/at/db

2.3 依赖说明

<!-- 注意一定要引入对版本,要引入spring-cloud版本seata,而不是springboot版本的seata-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <!-- 排除掉springcloud默认的seata版本,以免版本不一致出现问题-->
    <exclusions>
        <exclusion>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </exclusion>
        <exclusion>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- 上面排除掉了springcloud默认色seata版本,此处引入和seata-server版本对应的seata包-->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.6.1</version>
</dependency>

通过添加spring-cloud-starter-alibaba-seata依赖,来实现利用openfeign传播xid

三、使用示例

3.1 项目总体结构

在这里插入图片描述

目录结构解析:

  • consumer-at:为TM (Transaction Manager) - 事务管理器 角色
    定义全局事务的范围:开始全局事务、提交或回滚全局事务。

  • provider-at:为RM (Resource Manager) - 资源管理器 角色
    管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

3.2 provider-at 搭建

3.2.1 完整依赖

seata/nacos-http-at/provider-at/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>nacos-http-at</artifactId>
        <groupId>com.gm</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>provider-at</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <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>
        </dependency>
        <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>


        <!-- 注意一定要引入对版本,要引入spring-cloud版本seata,而不是springboot版本的seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <!-- 排除掉springcloud默认的seata版本,以免版本不一致出现问题-->
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-all</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 上面排除掉了springcloud默认色seata版本,此处引入和seata-server版本对应的seata包-->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.6.1</version>
        </dependency>

        <!--<dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </dependency>-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>io.fabric8</groupId>
                <artifactId>docker-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <excludes>
                    <exclude>**/*.xlsx</exclude>
                    <exclude>**/*.xls</exclude>
                </excludes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>false</filtering>
                <includes>
                    <include>**/*.xlsx</include>
                    <include>**/*.xls</include>
                </includes>
            </resource>
        </resources>
    </build>


</project>

3.2.2 配置文件

src/main/resources/bootstrap.yml

server:
  port: 4000

spring:
  application:
    name: @artifactId@
  cloud:
    nacos:
      username: @nacos.username@
      password: @nacos.password@
      discovery:
        server-addr: ${
    
    NACOS_HOST:nacos1.kc}:${
    
    NACOS_PORT:8848}
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.0.46:3306/seata-demo-provider?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: '1qaz@WSX'
seata:
  # 是否开启spring-boot自动装配,seata-spring-boot-starter 专有配置,默认true
  enabled: true
  # 是否开启数据源自动代理,seata-spring-boot-starter专有配置,默认会开启数据源自动代理,可通过该配置项关闭
  enable-auto-data-source-proxy: true
  # 配置自定义事务组名称,需与下方server.vgroupMapping配置一致,程序会通过用户配置的配置中心去寻找service.vgroupMapping
  tx-service-group: mygroup
  config: # 从nacos配置中心获取client端配置
    type: nacos
    nacos:
      server-addr: ${
    
    NACOS_HOST:nacos1.kc}:${
    
    NACOS_PORT:8848}
      group : DEFAULT_GROUP
      namespace: a4c150aa-fd09-4595-9afe-c87084b22105
      dataId: seataServer.properties
      username: @nacos.username@
      password: @nacos.username@
  registry: # 通过服务中心通过服务发现获取seata-server服务地址
    type: nacos
    nacos:
      # 注:客户端注册中心配置的serverAddr和namespace与Server端一致,clusterName与Server端cluster一致
      application: seata-server # 此处与seata-server的application一致,才能通过服务发现获取服务地址
      group : DEFAULT_GROUP
      server-addr: ${
    
    NACOS_HOST:nacos1.kc}:${
    
    NACOS_PORT:8848}
      userName: @nacos.username@
      password: @nacos.username@
      namespace: a4c150aa-fd09-4595-9afe-c87084b22105
  service:
    # 应用程序(客户端)会通过用户配置的配置中心去寻找service.vgroupMapping.[事务分组配置项]
    vgroup-mapping:
      # 事务分组配置项[mygroup]对应的值为TC集群名[default],与Seata-Server中的seata.registry.nacos.cluster配置一致
      mygroup : default
    # 全局事务开关,默认false。false为开启,true为关闭
    disable-global-transaction: false
  client:
    rm:
      report-success-enable: true
management:
  endpoints:
    web:
      exposure:
        include: '*'

logging:
  level:
    io.seata: debug

# mybatis-plus配置控制台打印完整带参数SQL语句
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

注意事项:

  • 要求客户端配置(seata.tx-service-group: [事务分组配置项]service.vgroupMapping.[事务分组配置项]: [TC集群名])与Seata-Server配置(service.vgroupMapping.[事务分组配置项]: [TC集群名] )在[事务分组配置项][TC集群名]配置一致,且上述[TC集群名]seata.registry中的cluster配置一致
  • seata.config.typeseata.registry.type均为nacos
  • configregistrynacos的配置,其中namespacegroup须与Seata-Server的配置一致

seata更多配置说明,请见官网:http://seata.io/zh-cn/docs/user/configurations.html

3.2.3 建表语句

DROP TABLE IF EXISTS `t_b`;
CREATE TABLE `t_b`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `create_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1662719643897974787 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

在这里插入图片描述

3.2.4 功能搭建

3.2.4.1 启动类

com/gm/seata/nacos_http_provider/NacosHttpProviderATApplication.java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class NacosHttpProviderATApplication {
    
    

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

}

3.2.4.2 实体类

com/gm/seata/nacos_http_provider/entity/B.java

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@TableName("t_b")
public class B {
    
    

    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    private String createBy;

    private LocalDateTime createTime;
}

3.2.4.3 Mapper类

com/gm/seata/nacos_http_provider/mapper/BMapper.java

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gm.seata.nacos_http_provider.entity.B;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface BMapper extends BaseMapper<B> {
    
    
}

3.2.4.4 Service类

com/gm/seata/nacos_http_provider/service/BService.java

public interface BService {
    
    
    void save(String createBy);
}

com/gm/seata/nacos_http_provider/service/impl/BServiceImpl.java

import com.gm.seata.nacos_http_provider.entity.B;
import com.gm.seata.nacos_http_provider.mapper.BMapper;
import com.gm.seata.nacos_http_provider.service.BService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;

@Service
public class BServiceImpl implements BService {
    
    

    @Autowired
    BMapper bMapper;

    @Override
    public void save(String createBy) {
    
    
        B b = new B();
        b.setCreateBy(createBy);
        b.setCreateTime(LocalDateTime.now());
        bMapper.insert(b);
        if (Math.random() < 0.5) {
    
    
            throw new RuntimeException("模拟调用第三方接口异常");
        }
    }
}

通过随机模拟RM (Resource Manager) - 资源管理器 角色调用失败

3.2.4.5 Controller类

com/gm/seata/nacos_http_provider/controller/ProviderController.java

import com.gm.seata.nacos_http_provider.service.BService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProviderController {
    
    

    @Autowired
    BService bService;

    @RequestMapping(value = "save", method = RequestMethod.GET)
    public void save(@RequestParam("createBy") String createBy) {
    
    
        bService.save(createBy);
    }
}

3.3 consumer-at 搭建

3.3.1 完整依赖

seata/nacos-http-at/consumer-at/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>nacos-http-at</artifactId>
        <groupId>com.gm</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>consumer-at</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <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>
        </dependency>
        <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>

        <!-- 注意一定要引入对版本,要引入spring-cloud版本seata,而不是springboot版本的seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <!-- 排除掉springcloud默认的seata版本,以免版本不一致出现问题-->
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-all</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 上面排除掉了springcloud默认色seata版本,此处引入和seata-server版本对应的seata包-->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.6.1</version>
        </dependency>
        <!--<dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </dependency>-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>

    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>io.fabric8</groupId>
                <artifactId>docker-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <excludes>
                    <exclude>**/*.xlsx</exclude>
                    <exclude>**/*.xls</exclude>
                </excludes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>false</filtering>
                <includes>
                    <include>**/*.xlsx</include>
                    <include>**/*.xls</include>
                </includes>
            </resource>
        </resources>
    </build>


</project>

3.3.2 配置文件

src/main/resources/bootstrap.yml

server:
  port: 3000

spring:
  application:
    name: @artifactId@
  cloud:
    nacos:
      username: @nacos.username@
      password: @nacos.password@
      discovery:
        server-addr: ${
    
    NACOS_HOST:nacos1.kc}:${
    
    NACOS_PORT:8848},${
    
    NACOS_HOST:nacos2kc}:${
    
    NACOS_PORT:8848},${
    
    NACOS_HOST:nacos3.kc}:${
    
    NACOS_PORT:8848}
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.0.35:3306/seata-demo-consumer?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: '1qaz@WSX'
seata:
  enabled: true
  enable-auto-data-source-proxy: true
  tx-service-group: mygroup
  config:
    type: nacos
    nacos:
      server-addr: ${
    
    NACOS_HOST:nacos1.kc}:${
    
    NACOS_PORT:8848}
      group : DEFAULT_GROUP
      namespace: a4c150aa-fd09-4595-9afe-c87084b22105
      dataId: seataServer.properties
      username: @nacos.username@
      password: @nacos.username@
  registry:
    type: nacos
    nacos:
      application: seata-server
      group : DEFAULT_GROUP
      server-addr: ${
    
    NACOS_HOST:nacos1.kc}:${
    
    NACOS_PORT:8848}
      userName: @nacos.username@
      password: @nacos.username@
      namespace: a4c150aa-fd09-4595-9afe-c87084b22105
  service:
    vgroup-mapping:
      mygroup : default
    disable-global-transaction: false
  client:
    rm:
      # 是否上报一阶段成功,默认false,true用于保持分支事务生命周期记录完整,false可提高不少性能
      report-success-enable: true

management:
  endpoints:
    web:
      exposure:
        include: '*'

# mybatis-plus配置控制台打印完整带参数SQL语句
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3.3.3 建表语句

DROP TABLE IF EXISTS `t_a`;
CREATE TABLE `t_a`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `create_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1659824277015949315 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

在这里插入图片描述

3.3.4 功能搭建

3.3.4.1 启动类

com/gm/seata/nacos_http_consumer/NacosHttpConsumerATApplication.java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients("com.gm.seata.nacos_http_consumer.service")
public class NacosHttpConsumerATApplication {
    
    

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

3.3.4.2 实体类

com/gm/seata/nacos_http_consumer/entity/A.java

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@TableName("t_a")
public class A {
    
    

    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    private String createBy;

    private LocalDateTime createTime;
}

3.3.4.3 Mapper类

com/gm/seata/nacos_http_consumer/mapper/AMapper.java

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gm.seata.nacos_http_consumer.entity.A;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface AMapper extends BaseMapper<A> {
    
    
}

3.3.4.4 Service类

com/gm/seata/nacos_http_consumer/service/ProviderServiceFeign.java

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

@FeignClient(value = "provider-at")
public interface ProviderServiceFeign {
    
    

    @RequestMapping(value = "save", method = RequestMethod.GET)
    void save(@RequestParam("createBy") String createBy);

}

com/gm/seata/nacos_http_consumer/service/AService.java

import com.gm.seata.nacos_http_consumer.entity.A;
import java.util.Map;

public interface AService {
    
    
    Long save(String createBy);

    void update(Long id, Map<String, String> map);

    void writeIsolation_GlobalLock(Long id);

    void writeIsolation_GlobalTransactional(Long id);

    Map<String, Object> readIsolation_GlobalLock(Long id);

    Map<String, Object> readIsolation_GlobalTransactional(Long id);

    Map<String, Object> dirtyReadExample(Long id);

    A dirtyWriteExample(Long id);
}

com/gm/seata/nacos_http_consumer/service/impl/AServiceImpl.java

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.gm.seata.nacos_http_consumer.service.ProviderServiceFeign;
import io.seata.spring.annotation.GlobalLock;
import io.seata.spring.annotation.GlobalTransactional;
import com.gm.seata.nacos_http_consumer.entity.A;
import com.gm.seata.nacos_http_consumer.mapper.AMapper;
import com.gm.seata.nacos_http_consumer.service.AService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class AServiceImpl implements AService {
    
    

    @Autowired
    ProviderServiceFeign providerServiceFeign;

    @Autowired
    AMapper aMapper;

    @Autowired
    JdbcTemplate jdbcTemplate;

    @GlobalTransactional
    @Override
    public Long save(String createBy) {
    
    
        providerServiceFeign.save(createBy);
        A a = new A();
        a.setCreateBy(createBy);
        a.setCreateTime(LocalDateTime.now());
        aMapper.insert(a);

        if (Math.random() < 0.5) {
    
    
            throw new RuntimeException("模拟调用本地接口异常");
        }
        return a.getId();
    }

    /**
     * 增加本地线程休眠,模式全局事务进行
     *
     * @param id
     */
    @GlobalTransactional
    @Override
    public void update(Long id, Map<String, String> map) {
    
    
        String createBy = UUID.randomUUID().toString();
        providerServiceFeign.save(createBy);

        A a = aMapper.selectById(id);
        map.put("修改前", JSON.toJSON(a).toString());

        a.setCreateBy(createBy);
        a.setCreateTime(LocalDateTime.now());
        QueryWrapper<A> queryWrapper = new QueryWrapper();
        queryWrapper.eq("id", id);
        aMapper.update(a, queryWrapper);

        map.put("修改后", JSON.toJSON(a).toString());
        try {
    
    
            TimeUnit.SECONDS.sleep(15);
        } catch (InterruptedException e) {
    
    
            throw new RuntimeException(e);
        }

        if (Math.random() < 0.5) {
    
    
            throw new RuntimeException("模拟调用本地接口异常");
        }
    }

    /**
     * 写隔离,通过GlobalLock方式,可了解lockRetryInterval和lockRetryTimes属性
     *
     * @param id
     */
    @GlobalLock(lockRetryInterval = 1000, lockRetryTimes = 30)
    @Override
    public void writeIsolation_GlobalLock(Long id) {
    
    
        A a = new A();
        a.setId(id);
        a.setCreateBy(UUID.randomUUID().toString());
        a.setCreateTime(LocalDateTime.now());
        QueryWrapper<A> queryWrapper = new QueryWrapper();
        queryWrapper.eq("id", id);
        aMapper.update(a, queryWrapper);
    }

    /**
     * 写隔离,通过GlobalTransactional方式,可了解lockRetryInterval和lockRetryTimes属性
     *
     * @param id
     */
    @GlobalTransactional/*(lockRetryInterval = 1000, lockRetryTimes = 30)*/
    @Override
    public void writeIsolation_GlobalTransactional(Long id) {
    
    
        A a = new A();
        a.setId(id);
        a.setCreateBy(UUID.randomUUID().toString());
        a.setCreateTime(LocalDateTime.now());
        QueryWrapper<A> queryWrapper = new QueryWrapper();
        queryWrapper.eq("id", id);
        aMapper.update(a, queryWrapper);
    }

    @GlobalLock(lockRetryInterval = 1000, lockRetryTimes = 30)
    @Override
    public Map<String, Object> readIsolation_GlobalLock(Long id) {
    
    
        return jdbcTemplate.queryForMap("select * from t_a where id=? for update ", id);
    }

    @GlobalTransactional(lockRetryInterval = 1000, lockRetryTimes = 30)
    @Override
    public Map<String, Object> readIsolation_GlobalTransactional(Long id) {
    
    
        return jdbcTemplate.queryForMap("select * from t_a where id=? for update ", id);
    }

    @Override
    public Map<String, Object> dirtyReadExample(Long id) {
    
    
        return jdbcTemplate.queryForMap("select * from t_a where id=?", id);
    }

    @Override
    public A dirtyWriteExample(Long id) {
    
    
        A a = new A();
        a.setId(id);
        a.setCreateBy(UUID.randomUUID().toString());
        a.setCreateTime(LocalDateTime.now());
        QueryWrapper<A> queryWrapper = new QueryWrapper();
        queryWrapper.eq("id", id);
        aMapper.update(a, queryWrapper);
        return a;
    }
}

注意事项:

  • TM (Transaction Manager) - 事务管理器 角色 的调用方法上添加@GlobalTransactional注解
  • 以上部分模拟事务隔离避免:脏写与脏读

3.3.4.5 Controller类

com/gm/seata/nacos_http_consumer/controller/ConsumerController.java

import com.alibaba.fastjson.JSON;
import com.gm.seata.nacos_http_consumer.entity.A;
import com.gm.seata.nacos_http_consumer.service.AService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestController
public class ConsumerController {
    
    

    @Autowired
    AService aService;

    @RequestMapping(value = "save", method = RequestMethod.GET)
    public String save(@RequestParam("createBy") String createBy) {
    
    
        try {
    
    
            return "全局事务操作成功,id=" + aService.save(createBy);
        } catch (Exception e) {
    
    
            log.error(e.getMessage(), e);
            return "全局事务操作失败,error=" + e.getMessage();
        }
    }

    /**
     * 模拟长全局事务,随机成功失败
     *
     * @param id
     * @return
     */
    @RequestMapping(value = "update", method = RequestMethod.GET)
    public String update(@RequestParam("id") Long id) {
    
    
        Map<String, String> map = new HashMap<>();
        try {
    
    
            aService.update(id, map);
            return "全局事务操作成功," + JSON.toJSON(map).toString();
        } catch (Exception e) {
    
    
            log.error(e.getMessage(), e);
            return "全局事务操作失败," + JSON.toJSON(map).toString() + ",error=" + e.getMessage();
        }
    }

    @RequestMapping(value = "dirty/write", method = RequestMethod.GET)
    public String dirtyWrite(@RequestParam("id") Long id) {
    
    
        try {
    
    
            A a = aService.dirtyWriteExample(id);
            return "本地事务-写操作成功," + JSON.toJSON(a).toString();
        } catch (Exception e) {
    
    
            log.error(e.getMessage(), e);
            return "本地事务-写操作失败,error=" + e.getMessage();
        }
    }

    @RequestMapping(value = "dirty/read", method = RequestMethod.GET)
    public String dirtyRead(@RequestParam("id") Long id) {
    
    
        return JSON.toJSON(aService.dirtyReadExample(id)).toString();
    }

    @RequestMapping(value = "write/globallock", method = RequestMethod.GET)
    public String writeIsolation_GlobalLock(@RequestParam("id") Long id) {
    
    
        try {
    
    
            aService.writeIsolation_GlobalLock(id);
            return "本地事务-写操作成功";
        } catch (Exception e) {
    
    
            log.error(e.getMessage(), e);
            return "本地事务-写操作失败,error=" + e.getMessage();
        }
    }

    @RequestMapping(value = "write/globaltransactional", method = RequestMethod.GET)
    public String writeIsolation_GlobalTransactional(@RequestParam("id") Long id) {
    
    
        try {
    
    
            aService.writeIsolation_GlobalTransactional(id);
            return "本地事务-写操作成功";
        } catch (Exception e) {
    
    
            log.error(e.getMessage(), e);
            return "本地事务-写操作失败,error=" + e.getMessage();
        }
    }

    @RequestMapping(value = "read/globallock", method = RequestMethod.GET)
    public String readIsolation_GlobalLock(@RequestParam("id") Long id) {
    
    
        return JSON.toJSON(aService.readIsolation_GlobalLock(id)).toString();
    }

    @RequestMapping(value = "read/globaltransactional", method = RequestMethod.GET)
    public String readIsolation_GlobalTransactional(@RequestParam("id") Long id) {
    
    
        return JSON.toJSON(aService.readIsolation_GlobalTransactional(id)).toString();
    }
}

3.4 示例说明

3.4.1 分布式事务演示

访问:http://127.0.0.1:3000/save?createBy=1234567

  • 分布式事务成功

    在这里插入图片描述

    provider-at日志:

    在这里插入图片描述

    consumer-at日志:

    在这里插入图片描述

  • 分布式事务失败 - 模拟provider-at失败

    在这里插入图片描述

    provider-at日志:

    在这里插入图片描述

    consumer-at日志:

    在这里插入图片描述

  • 分布式事务失败 - 模拟consumer-at失败

    在这里插入图片描述

    provider-at日志:

    在这里插入图片描述

    consumer-at日志:

    在这里插入图片描述

3.4.2 事务隔离演示

访问:http://127.0.0.1:3000/update?id=1662729396682399746 模拟长时间分布式事务

Seata AT 模式理论学习、事务隔离及部分源码解析 | Spring Cloud 52

3.4.2.1 脏读隔离

3.4.2.1 脏写隔离

猜你喜欢

转载自blog.csdn.net/ctwy291314/article/details/130889752
今日推荐