微服务分布式事务组件 Seata(二)
一、Seata 快速开始
1.1、Seata Server(TC)环境搭建
这里为了组件版本统一,使用 1.3.0 版本
Server端存储模式(store.mode) 支持三种:
- file(默认)单机模式,全局事务会话信息内存中读写并持久化本地文件 root.data,目录为
bin/sessionStore/root.data
,性能较高 - db:数据库(5.7+),高可用模式,全局事务会话信息通过db共享,相应性能差些
- redis: Seata-Server 1.3及以上版本支持,性能较高,存在事务信息丢失风险请提前配置适合当前场景的redis持久化配置
1.2、部署高可用集群模式
db + Nacos 的方式部署
vim /conf/file.conf
修改 mode
mode = "db"
并且修改 db 对应的相关数据库连接信息
1.3、创建数据库
数据库表需要参考 seata 官网的资源目录说明
资源目录介绍
- client
存放client端sql脚本 (包含 undo_log表) ,参数配置
- config-center
各个配置中心参数导入脚本,config.txt(包含server和client,原名nacos-config.txt)为通用参数文件
- server (服务器)
server (服务器) 端数据库脚本 (包含 lock_table、branch_table 与 global_table) 及各个容器配置
执行 seata-1.3.0/script/server/db/mysql.sql 文件
1.4、修改注册中心
为了微服务与 seata 服务进行通信
vim /config/registry.conf
修改 type = "nacos"
并且配置 nacos 服务地址以及用户名密码等信息
并且可以配置 loadBanance 策略
1.5、修改配置中心
为 seata 服务配置进行统一管理,默认存储在 config.txt 文件中。
vim /config/registry.conf
修改 config 中的 type = "nacos"
并且配置 nacos 的服务地址等信息
1.6、将配置注册到注册中心
注意:如果配置了seata server 使用nacos作为配置中心,则配置信息会从 naco s读取, file.conf 可以不用配置。客户端配置 registry.conf 使用 nacos 时也要注意 group 要和 seata server中的 group 一致,默认group是”DEFAULT_GROUP”
配置事务分组,要与客户端配置的事务分组一致
my_test_tx_group 要与客户端保持一致 default 需要跟客户端和 regisry.conf 中 registry 中的 cluster 保持一致
事务分组:异地机房停电容错机制
参考官方文档(事务分组与高可用)
my
my_test_tx_group 可以自定义 比如:(guagnzhou、shanghai),对应的客户端也要去设置
seata.service.vgroup-mapping.projectA=guagnzhou
default 必须要等于 registry.conf 注册中心的配置 cluster=”default”
运行脚本文件,将配置存入注册中心,脚本文件位置/script/config-center/nacos/nacos-config.sh
,如果要指定 nacos 服务地址 sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t xxxx
参数说明:
-h:host,默认值为 localhost
-p:port,默认值为 8848
-g:配置分组,默认值为 “SEATA_GROUP”
-t:租户信息,对应nacos的命名空间 ID 字段,默认为空
-u:用户名
-w:密码
可以看到配置都注册进来了
1.7、启动 seata 服务
启动命令bin/seata-server.sh -h 127.0.0.1 -p 8091 -m db -n 1 -e test
参数 | 全写 | 作用 | 备注 |
---|---|---|---|
-h | –host | 指定在注册中心注册的IP | 不指定时获取当前的IP,外部访问部署在云环境和容器中的server建议指定 |
-p | –port | 指定 server 启动的端口 | 默认为8901 |
-m | –storeMode | 事务日志存储方式 | 支持file ,db ,redis ,默认为file ,注:redis 需要 seata-server 1.3 版本及以上 |
-n | –serverNode | 用于指定 seata-server节点ID | 如1 ,2 ,3 …,默认为1 |
-e | –seataEnv | 指定 seata-server 运行环境 | 如 dev ,test 等,服务启动时或使用 registry-dev.conf 这样的配置 |
在注册中心可以看到 seata-server 注册成功
如果是部署集群的话,seata-server.sh -p 8091 -n 1
,seata-server.sh -p 8092 -n 2
,seata-server.sh -p 8093 -n 3
。。。,不需要通过其它进行负载均衡。
二、Seata Client 快速开始
声明式事务实现(@GlobalTransactional)
接入微服务应用
业务场景:
用户下单,这个那个业务逻辑由三个微服务构成:
- 订单服务:根据采购需求创建订单
- 库存服务:对给定的商品扣除库存数量
创建数据库,语句如下
seata_order.sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for order_tbl
-- ----------------------------
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`product_id` int NOT NULL COMMENT '商品id',
`total_amount` int NOT NULL COMMENT '总金额',
`status` int NOT NULL COMMENT '0:待付款,1:待发货',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ----------------------------
-- Records of order_tbl
-- ----------------------------
BEGIN;
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
seata_stock.sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for stock_tbl
-- ----------------------------
DROP TABLE IF EXISTS `stock_tbl`;
CREATE TABLE `stock_tbl` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`product_id` int DEFAULT NULL,
`count` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ----------------------------
-- Records of stock_tbl
-- ----------------------------
BEGIN;
INSERT INTO `stock_tbl` (`id`, `product_id`, `count`) VALUES (1, 9, 100);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
父工程引入依赖
<dependencies>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!--mysql 驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--druid数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.11</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
</dependencies>
项目流程大致如下
项目结构如下
2.1、测试本地事务是否可行
订单服务
实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("order_tbl")
public class Order {
@TableId
private Integer id;
//商品id
private Integer productId;
//总金额
private Integer totalAmount;
//0:待付款,1:待发货
private Integer status;
}
dao 层
@Repository
public interface OrderDao extends BaseMapper<Order> {
}
业务层
@Service
public class OrderServiceImpl extends ServiceImpl<OrderDao, Order> implements OrderService {
@Autowired
private RestTemplate restTemplate;
@Transactional
@Override
public void create(Order order) {
// 插入是否成功
save(order);
// 扣减库存是否成功
LinkedMultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
paramMap.add("productId",order.getProductId());
String response = restTemplate.postForObject("http://localhost:8071/stock/reduce", paramMap, String.class);
// 异常
int a = 1/0;
}
}
控制层
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
// 插入订单信息
@RequestMapping("/add")
public String add() {
Order order = new Order();
order.setProductId(9);
order.setStatus(0);
order.setTotalAmount(100);
orderService.create(order);
return "下单成功";
}
}
启动类
@SpringBootApplication
@EnableTransactionManagement
@MapperScan("com.hudu.dao")
public class SeataOrderApplication {
public static void main(String[] args) {
SpringApplication.run(SeataOrderApplication.class, args);
}
}
application.yml 配置文件
server:
port: 8070
spring:
datasource:
url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
# 初始化时运行 sql 脚本
# schema: classpath:sql/seata_order.sql
# initialization-mode: always
mybatis-plus:
mapper-locations: classpath*:/mapper/*.xml
global-config:
db-config:
id-type: auto
库存服务
实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("stock_tbl")
public class Stock {
@TableId
private Integer id;
private Integer productId;
private Integer count;
}
dao 层
@Repository
public interface StockDao extends BaseMapper<Stock> {
void reduce(Integer productId);
}
映射层
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.hudu.dao.StockDao">
<update id="reduce">
update stock_tbl set `count` = `count` - 1
where product_id = #{product}
</update>
</mapper>
业务层
@Service("StockService")
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockDao, Stock> implements StockService {
@Autowired
private StockDao stockDao;
@Override
public void reduce(Integer productId) {
log.info("更新商品:{}",productId);
stockDao.reduce(productId);
}
}
控制层
@RestController
@RequestMapping("/stock")
public class StockController {
@Autowired
private StockService stockService;
@RequestMapping("/reduce")
public String reduce(@RequestParam(value = "productId") Integer productId) {
stockService.reduce(productId);
return "扣减库存";
}
}
启动类
@SpringBootApplication
@MapperScan("com.hudu.dao")
public class SeataStockApplication {
public static void main(String[] args) {
SpringApplication.run(SeataStockApplication.class,args);
}
}
application.yml 配置文件
server:
port: 8071
spring:
datasource:
url: jdbc:mysql://localhost:3306/seata_stock?characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
mybatis-plus:
mapper-locations: classpath*:/mapper/*.xml
global-config:
db-config:
id-type: auto
请求新增订单接口http://localhost:8070/order/add
,在除零异常之前请求了扣减库存服务。
订单并没有插入,由于事务的回滚,但是库存已经减少了。所以使用本地事务的@Transactional
注解是无法解决分布式事务的场景的。
2.2、集成 openfeign 以及 nacos
订单服务引入 openfeign 和 Nacos 依赖
<dependencies>
<!-- Nacos 服务注册与发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--添加 openfeign 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
库存服务添加依赖
<dependencies>
<!-- Nacos 服务注册与发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
配置注册中心地址
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.33.62:8847
username: hudu
password: 123456
network-interface: en2
将之前的 restTemplate 调用改为 OpenFeign 远程调用
启动类添加@EnabldOpenFeignClients
注解
@SpringBootApplication
@EnableTransactionManagement
@MapperScan("com.hudu.dao")
@EnableFeignClients
public class AlibabaSeataOrderApplication {
public static void main(String[] args) {
SpringApplication.run(AlibabaSeataOrderApplication.class, args);
}
}
创建一个接口,添加@FeignClient
注解,并且指定服务名称,以及路径地址,添加需要请求的接口的地址
@FeignClient(value = "alibaba-stock-seata",path = "/stock")
public interface StockService {
@RequestMapping("/reduce")
String reduce(@RequestParam(value = "productId") Integer productId);
}
将原先业务层的 restTemplate 调用改为 openFeign 调用
@Service
public class OrderServiceImpl extends ServiceImpl<OrderDao, Order> implements OrderService {
// @Autowired
// private RestTemplate restTemplate;
@Autowired
private StockService stockService;
@Transactional
@Override
public void create(Order order) {
// 插入是否成功
save(order);
// 扣减库存是否成功
// LinkedMultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
// paramMap.add("productId",order.getProductId());
// String response = restTemplate.postForObject("http://localhost:8071/stock/reduce", paramMap, String.class);
stockService.reduce(order.getProductId());
// 异常
int a = 1/0;
}
}
启动服务,可以看到两个服务都注册到了注册中心。
请求下单接口http://localhost:8070/order/add
,由于依然使用的是本地事务,所以无法解决分布式事务问题。
三、搭建 seata client
3.1、引入 seata 依赖
<!--seata 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
3.2、各微服务对应数据库中添加 undo_log 表
文件地址为seata/script/client/at/db/mysql.sql
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;
3.3、配置事务分组
配置事务分组,与之前添加的配置保持一致
spring:
cloud:
alibaba:
seata:
tx-service-group: guangzhou
3.4、配置与 seata 的通信方式
seata:
registry:
# 配置 seata 注册中心,告诉 seata client 如何访问 seata server
type: nacos
nacos:
# seata server 所在的 nacos 服务地址
server-addr: 192.168.33.62:8847
# seata server 的服务名称,默认为 seata-server
application: seata-server
username: nacos
password: nacos
# seata server 的分组,默认为 SEATA_GROUP
group: SEATA_GROUP
config:
# 配置 seata 的配置中心,可以读取关于 seata client 的配置
type: nacos
nacos:
server-addr: 192.168.33.62:8847
username: nacos
password: nacos
namespace: public
3.5、使用@GlobalTransactional
注解
@GlobalTransactional
@Override
public void create(Order order) {
// 插入是否成功
save(order);
stockService.reduce(order.getProductId());
// 异常
int a = 1/0;
}
启动服务之后,服务都注册到了注册中心,并且可以看到 seata server 日志
再次请求生成订单接口http://localhost:8070/order/add
,可以看到此时,订单没有生产,库存也没有减少
可以看到异常之后,事务回滚日志
四、Seata 运行原理总结
1.TM 请求 TC 开启一个全局事务。TC 会生成一个 XID 作为该全局事务的编号。XID 会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。
请求生产订单接口http://localhost:8070/order/add
,当一进入到事务方法 crate() 中就会产生 XID,会注册全局事务信息,通过全局事务的 XID 进行关联。
global_table 存储的是全局事务信息
2.RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联。
当执行到 sava() 和 调用远程的扣减库存时,即运行数据库操作方式时,会为事务参与者创建事务分支信息,存储在 branch_table 中
lock_table 是锁表的信息,可以看到在执行到 sava() 方法时,锁的是 order_tbl 表,锁的主键为 7
并且brefore image 和 after image 信息都保存在了 undo_log 表中的 rollback_info 字段中。
可以通过SELECT CONVERT(rollback_info USING utf8) FROM undo_log
sql 语句将内容转换为 utf8 进行查看。可以看到 brefore image 以及 after image。before image 之所以没数据是因为,before image 执行的是插入数据操作,如果需要回滚,只需要将插入之后的 after image 的数据进行删除即可。可以看到 after image 中的信心,id主键,值为 7,product_id,商品 id 为 9 等信息。
本作品采用《CC 协议》,转载必须注明作者和本文链接