文章目录
Spring Cloud Hoxton.SR4 Spring Cloud Alibaba 2.2.2.RELEASE Spring Boot 2.3.0.RELEASE
GitHub:shpunishment/spring-cloud-learning/spring-cloud-seata-test
1. 简介
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
单体应用
单体应用中,一个业务操作需要调用三个模块完成。它们使用单个本地数据源,本地事务自然能保证数据的一致性。
微服务应用
在微服务架构中发生了变化。上面提到的3个模块被设计为在3个不同数据源之上的3个服务。本地事务只能保证单个服务中的数据一致性。但是全局的数据一致性没法保证。
Seata只是上述问题的解决方案。
分布式事务
分布式事务是由一批分支事务组成的全局事务,通常分支事务只是本地事务。
Seata有3个基本组成部分:
- 事务协调器(TC):维护全局事务和分支事务的状态,驱动全局提交或回滚。
- 事务管理器(TM):定义全局事务的范围,开始全局事务,提交或回滚全局事务。
- 资源管理器(RM):管理分支事务正在处理的资源,与TC进行对话以注册分支事务并报告分支事务的状态,并驱动分支事务的提交或回滚。
Seata管理的分布式事务的典型生命周期:
- TM 向 TC 申请开启一个全局事务,TC 创建全局事务并生成一个全局唯一的 XID;
- XID 在微服务调用链路的上下文中传播;
- RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
- TM 向 TC 发起针对 XID 的全局提交或回滚请求;
- TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
以下以 AT 模式为例,完成基本的下单流程。
2. 安装
2.1 nacos
使用Windows启动时报错,修改startup.cmd的mode从cluster为standalone,还是启动失败。
所以这里使用Linux。
使用单体模式启动Nacos,访问http://ip:8848/nacos。默认账号密码为nacos。
./bin/startup.sh -m standalone
2.2 seata-server
GitHub 下载,这里使用seata-server-1.3.0。
这里使用nacos作为seata的注册中心和配置中心。
修改conf/registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "192.168.110.40:8848"
group = "SEATA_GROUP"
# nacos命名空间id,""为nacos保留public空间控件,用户勿配置namespace = "public"
namespace = ""
# seata-server在nacos的集群名
cluster = "seata-server-cluster"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "192.168.110.40:8848"
# nacos命名空间id,""为nacos保留public空间控件,用户勿配置namespace = "public"
namespace = ""
group = "SEATA_GROUP"
# seata-server在nacos的集群名
cluster = "seata-server-cluster"
username = "nacos"
password = "nacos"
}
}
2.2.1 上传配置
使用nacos作为配置中心,需要先将seata的配置上传上去。
在GitHub seata中下载config.txt文件和nacos文件夹下的脚本。这里使用nacos-config.sh。
修改config.txt
# 事务分组 seata_tx_group,可自定义
# seata-server-cluster 为seata-server在nacos的集群名,要和registry.conf中配置一致。
service.vgroupMapping.seata_tx_group=seata-server-cluster
# #事务信息存储到数据库
store.mode=db
store.db.url=jdbc:mysql://192.168.4.23:4306/seata-server?useUnicode=true&characterEncoding=UTF-8&useSSL=false
store.db.user=root
store.db.password=root
上传配置,注意config.txt文件的位置。
sh nacos-config.sh -h 192.168.110.40 -p 8848 -g SEATA_GROUP -u nacos -w nacos
2.2.2 初始化数据库
创建数据库seata-server。
在GitHub Seata中下载Server端的数据库SQL。
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
启动seata-server
./bin/seata-server.sh
3. 使用
下单->减库存->减余额->完成下单。
3.1 准备数据库
创建存储账户信息的数据库seata-account及表。
CREATE TABLE `account` (
`user_id` int(11) NOT NULL COMMENT '用户id',
`total` decimal(10,2) DEFAULT NULL COMMENT '总余额',
`used` decimal(10,2) DEFAULT NULL COMMENT '已用余额',
`residue` decimal(10,2) DEFAULT NULL COMMENT '可用额度',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='账户表';
INSERT INTO `seata-account`.`account` (`user_id`, `total`, `used`, `residue`) VALUES ('1', '1000', '0', '1000');
创建存储商品库存的数据库seata-storage及表。
CREATE TABLE `storage` (
`product_id` int(11) NOT NULL COMMENT '产品id',
`total` int(11) DEFAULT NULL COMMENT '总库存',
`used` int(11) DEFAULT NULL COMMENT '已用库存',
`residue` int(11) DEFAULT NULL COMMENT '剩余库存',
PRIMARY KEY (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品库存表';
INSERT INTO `seata-storage`.`storage` (`product_id`, `total`, `used`, `residue`) VALUES ('1', '100', '0', '100');
创建存储订单的数据库seata-order及表。
CREATE TABLE `order` (
`order_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单id',
`user_id` int(11) DEFAULT NULL COMMENT '用户id',
`product_id` int(11) DEFAULT NULL COMMENT '产品id',
`count` int(11) DEFAULT NULL COMMENT '数量',
`money` decimal(10,2) DEFAULT NULL COMMENT '金额',
`status` int(1) DEFAULT '0' COMMENT '状态,0创建,1完成',
PRIMARY KEY (`order_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单表';
AT模式在每个数据库中创建表undo,也可在GitHub Seata查看。
-- 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(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(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 = utf8 COMMENT ='AT transaction mode undo table';
数据库和表
3.2 创建项目
先用IDEA创建一个Spring Boot的项目,可以随意引用一个Spring Cloud的组件,之后也会删掉。
创建完,删掉除了pom.xml以外的其他文件,再修改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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
</parent>
<groupId>com.shpun</groupId>
<artifactId>spring-cloud-seata-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud-seata-test</name>
<description>spring cloud seata test</description>
<!--修改打包方式为pom-->
<packaging>pom</packaging>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Hoxton.SR4</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.2.RELEASE</spring-cloud-alibaba.version>
</properties>
<modules>
<!--后续添加子模块用-->
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<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>
</dependencies>
</dependencyManagement>
</project>
3.2.1 acount-service
创建子模块 acount-service
修改pom继承
<parent>
<groupId>com.shpun</groupId>
<artifactId>spring-cloud-seata-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
再添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
修改application.yml
server:
port: 8201
spring:
application:
name: account-service
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:4306/seata-account?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root
# 使用阿里的Druid连接池
type: com.alibaba.druid.pool.DruidDataSource
# druid配置
druid:
# 初始化大小
initial-size: 5
# 最小
min-idle: 10
# 最大
max-active: 30
# 配置获取连接等待超时的时间
max-wait: 6000
cloud:
nacos:
discovery:
# nacos地址
server-addr: 192.168.110.40:8848
group: SEATA_GROUP
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: seata_tx_group
service:
vgroup-mapping:
seata_tx_group: seata-server-cluster
# 配置中心地址
config:
type: nacos
nacos:
serverAddr: 192.168.110.40:8848
group: SEATA_GROUP
namespace:
username: nacos
password: nacos
# 注册中心地址
registry:
type: nacos
nacos:
# Server和Client端的值需一致
application: seata-server
server-addr: 192.168.110.40:8848
group : SEATA_GROUP
namespace:
username: nacos
password: nacos
mybatis:
typeAliasesPackage: com.shpun.model
mapper-locations: classpath:mapper/**.xml
Model,Mapper,Service等省略。
AccountController
@RequestMapping("/api/account")
@RestController
public class AccountController {
@Autowired
private AccountService accountService;
@GetMapping("/decrease")
public void decrease(@RequestParam("userId") Integer userId, @RequestParam("money") BigDecimal money) {
accountService.decrease(userId,money);
}
}
在启动类上添加@EnableDiscoveryClient
注解表明是一个服务发现的客户端。
3.2.2 storage-service
创建子模块 storage-service
修改pom继承
<parent>
<groupId>com.shpun</groupId>
<artifactId>spring-cloud-seata-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
再添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
修改application.yml
server:
port: 8203
spring:
application:
name: storage-service
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:4306/seata-storage?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root
# 使用阿里的Druid连接池
type: com.alibaba.druid.pool.DruidDataSource
# druid配置
druid:
# 初始化大小
initial-size: 5
# 最小
min-idle: 10
# 最大
max-active: 30
# 配置获取连接等待超时的时间
max-wait: 6000
cloud:
nacos:
discovery:
# nacos地址
server-addr: 192.168.110.40:8848
group: SEATA_GROUP
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: seata_tx_group
service:
vgroup-mapping:
# 分组名,值为TC集群名
seata_tx_group: seata-server-cluster
# 配置中心地址
config:
type: nacos
nacos:
serverAddr: 192.168.110.40:8848
group: SEATA_GROUP
namespace:
username: nacos
password: nacos
# 注册中心地址
registry:
type: nacos
nacos:
# Server和Client端的值需一致
application: seata-server
server-addr: 192.168.110.40:8848
group : SEATA_GROUP
namespace:
username: nacos
password: nacos
mybatis:
typeAliasesPackage: com.shpun.model
mapper-locations: classpath:mapper/**.xml
Model,Mapper,Service等省略。
StorageController
@RequestMapping("/api/storage")
@RestController
public class StorageController {
@Autowired
private StorageService storageService;
@GetMapping("/decrease")
public void decrease(@RequestParam("productId") Integer productId, @RequestParam("count") Integer count) {
storageService.decrease(productId, count);
}
}
在启动类上添加@EnableDiscoveryClient
注解表明是一个服务发现的客户端。
3.2.3 order-service
创建子模块 order-service
修改pom继承
<parent>
<groupId>com.shpun</groupId>
<artifactId>spring-cloud-seata-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
再添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
修改application.yml
server:
port: 8202
spring:
application:
name: order-service
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:4306/seata-order?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root
# 使用阿里的Druid连接池
type: com.alibaba.druid.pool.DruidDataSource
# druid配置
druid:
# 初始化大小
initial-size: 5
# 最小
min-idle: 10
# 最大
max-active: 30
# 配置获取连接等待超时的时间
max-wait: 6000
cloud:
nacos:
discovery:
# nacos地址
server-addr: 192.168.110.40:8848
group: SEATA_GROUP
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: seata_tx_group
service:
vgroup-mapping:
# 分组名,值为TC集群名
seata_tx_group: seata-server-cluster
# 配置中心地址
config:
type: nacos
nacos:
serverAddr: 192.168.110.40:8848
group: SEATA_GROUP
namespace:
username: nacos
password: nacos
# 注册中心地址
registry:
type: nacos
nacos:
# Server和Client端的值需一致
application: seata-server
server-addr: 192.168.110.40:8848
group : SEATA_GROUP
namespace:
username: nacos
password: nacos
mybatis:
typeAliasesPackage: com.shpun.model
mapper-locations: classpath:mapper/**.xml
添加Feign客户端
AccountFeignService
@FeignClient("account-service")
public interface AccountFeignService {
@GetMapping("/api/account/decrease")
void decrease(@RequestParam("userId") Integer userId, @RequestParam("money") BigDecimal money);
}
StorageFeignService
@FeignClient("storage-service")
public interface StorageFeignService {
@GetMapping(value = "/api/storage/decrease")
void decrease(@RequestParam("productId") Integer productId, @RequestParam("count") Integer count);
}
OrderService
@Service("orderServiceImpl")
public class OrderServiceImpl implements OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class);
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountFeignService accountFeignService;
@Autowired
private StorageFeignService storageFeignService;
@Override
@GlobalTransactional(name = "seata-add-order", rollbackFor = Exception.class)
public void add(Order order) {
logger.info("seata XID:{}", RootContext.getXID());
logger.info("下单开始");
logger.info("创建订单开始");
orderMapper.insertSelective(order);
logger.info("创建订单结束");
logger.info("扣减商品库存开始");
storageFeignService.decrease(order.getProductId(), order.getCount());
logger.info("扣减商品库存结束");
logger.info("扣减用户余额开始");
accountFeignService.decrease(order.getUserId(), order.getMoney());
logger.info("扣减用户余额结束");
logger.info("修改订单状态开始");
orderMapper.completeOrder(order.getOrderId());
logger.info("修改订单状态结束");
logger.info("下单结束");
}
}
Model,Mapper,Service等省略。
OrderController
@RequestMapping("/api/order")
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/add")
public void add(@RequestBody Order order) {
orderService.add(order);
}
}
在启动类上添加@EnableDiscoveryClient
注解表明是一个服务发现的客户端,@EnableFeignClients
注解启用Feign的客户端功能。
3.3 测试
启动account-service,storage-service和order-service。
seata-server中可看到三个服务完成注册。
下单测试
账户金额和商品库存完成扣减,下单完成。
回滚测试
修改account-service中的AccountController,遇到报错,会回滚。
@RequestMapping("/api/account")
@RestController
public class AccountController {
@Autowired
private AccountService accountService;
@GetMapping("/decrease")
public void decrease(@RequestParam("userId") Integer userId, @RequestParam("money") BigDecimal money) {
int i = 10/0;
accountService.decrease(userId,money);
}
}
打断点可发现,在扣减账户金额之前,没报错时都是正常的。order表中有记录,storage表也正常扣减,undo表中也有记录。
下单到扣减账户余额报错后,order表中记录被删除,storage表记录恢复,undo表记录被删除。事务完成了回滚。
参考:
Spring Cloud入门-Seata处理分布式事务问题(Hoxton版本)
seata 官方文档
Spring-Cloud-Alibaba-Seata 组件学习使用
CLOUDALIBABA分布式事务SEATA1.2.0结合注册中心NACOS1.3.0案例
阿里的 Seata(原理 + 实战)