一、前言
通过以下系列章节:
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
(回滚记录表)
- 在
TM
和RM
(Client
端)可通过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.type
与seata.registry.type
均为nacos
config
与registry
中nacos
的配置,其中namespace
与group
须与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 脏读隔离
-
分布式长事务成功
访问:http://127.0.0.1:3000/read/globallock?id=1662729396682399746
在本地读取上添加注解
@GlobalLock
,利用其参数:lockRetryInterval = 1000
,lockRetryTimes = 30
延长全局锁重试周期并使用select for update
语句 -
分布式长事务失败
访问:http://127.0.0.1:3000/read/globaltransactional?id=1662729396682399746
在本地读取上添加注解
@GlobalTransactional
,利用其参数:lockRetryInterval = 1000
,lockRetryTimes = 30
延长全局锁重试周期并使用select for update
语句分布式长事务执行识别,本地读取方法读取修改前的值
3.4.2.1 脏写隔离
-
分布式长事务执行成功,本地事务执行失败
访问:http://127.0.0.1:3000/write/globaltransactional?id=1662729396682399746
在本地读取上添加注解
@GlobalTransactional
未使用参数对全局锁重试周期进行延长出现以下异常:
-
分布式长事务执行失败,本地事务执行成功
访问:http://127.0.0.1:3000/write/globallock?id=1662729396682399746
在本地读取上添加注解
@GlobalLock
,利用其参数:lockRetryInterval = 1000
,lockRetryTimes = 30
延长全局锁重试周期分布式长事务执行失败,本地写入方法执行成功