Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
Seata相关名词
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
软件 |
版本 |
地址 |
JDK |
1.8.0_271 |
Java Downloads | Oracle |
Spring Boot |
2.5.6 |
Spring Boot |
Spring Cloud |
2020.0.4 |
Spring Cloud |
Seata |
1.4.2 |
https://github.com/seata/seata/releases |
代码地址(Seata 和 SQL 脚本在resource目录下):
spring-cloud-seata
下载seata-server-1.4.2.zip,地址:https://github.com/seata/seata/releases
解压seata-server-1.4.2.zip
创建数据库spring-cloud-seata-eureka ,并创建seata服务需要的表:
global_table、branch_table、lock_table
seata-server 需要的数据库脚本:
https://github.com/seata/seata/tree/v1.4.2/script/server/db
seata-client 需要的数据库脚本:
https://github.com/seata/seata/tree/v1.4.2/script/client/at/db
Seata AT 模式,客户端只需要 undo_log表,下面要新建业务表t_account、t_order、t_storage 三张业务表
如果三张表放到一个数据库里面,只需要新建一个 undo_log 表
如果将三张表拆分到三个数据库里面,则每个数据库都需要创建 undo_log 表
我们需要修改 seata-server-1.4.2/conf 目录里面file.conf和registry.conf,这里我们使用的是eureka注册中心,存储使用的MySQL数据库,配置如下
file.conf 配置如下:
## transaction log store, only used in seata-server
store {
## store mode: file、db、redis
mode = "db"
## rsa decryption public key
publicKey = ""
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.cj.jdbc.Driver"
## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
url = "jdbc:mysql://127.0.0.1:3306/spring-cloud-seata-eureka?rewriteBatchedStatements=true&characterEncoding=utf8&useSSL=false"
user = "root"
password = "root"
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
}
}
registry.conf 配置如下
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "eureka"
eureka {
serviceUrl = "http://127.0.0.1:8761/eureka"
application = "default"
weight = "1"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
file {
name = "file.conf"
}
}
eureka配置如下:
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
启动eureka
执行 seata-server-1.4.2\bin 目录下的 seata-server.bat (windows)/ seata-server.sh (linux或mac)启动seata服务 。
打开 http://localhost:8761/,我们可以看出,seata 服务已经成功注册到eureka注册中心。
用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:
初始化订单、库存、账户三张表
-- ----------------------------
-- Table structure for t_account
-- ----------------------------
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL DEFAULT 0,
`money` bigint(20) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4;
-- ----------------------------
-- Records of t_account
-- ----------------------------
INSERT INTO `t_account` VALUES (1, 10001, 10000);
-- ----------------------------
-- Table structure for t_order
-- ----------------------------
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL DEFAULT 0,
`commodity_code` varchar(20) CHARACTER SET utf8mb4 NOT NULL DEFAULT '',
`count` int(10) NOT NULL DEFAULT 0,
`money` bigint(20) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4;
-- ----------------------------
-- Records of t_order
-- ----------------------------
-- ----------------------------
-- Table structure for t_storage
-- ----------------------------
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(50) CHARACTER SET utf8mb4 NOT NULL DEFAULT '',
`commodity_name` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '',
`count` int(11) NOT NULL DEFAULT 0,
`price` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `commodity_code`(`commodity_code`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4;
-- ----------------------------
-- Records of t_storage
-- ----------------------------
INSERT INTO `t_storage` VALUES (1, '10001', '苹果手机', 100, 1000);
order-service、storage-service、account-service三个服务的pom.xml的依赖一样,business-service服务不需要连接数据库,所以不需要引用mybatis-plus-boot-starter和mysql-connector-java
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-loadbalancer
org.springframework.cloud
spring-cloud-starter-openfeign
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
com.baomidou
mybatis-plus-boot-starter
mysql
mysql-connector-java
order-service、storage-service、account-service三个服务的application.yml基本一致,其中需要修改的有以下几个地方:
server.port
spring.application.name
mybatis-plus.type-aliases-package
seata.application-id
order-service 服务yml配置
server:
port: 9001
spring:
application:
name: order-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/spring-cloud-seata-eureka?characterEncoding=utf-8
username: root
password: root
eureka:
instance:
instance-id: order-service
prefer-ip-address: true
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://127.0.0.1:8761/eureka
mybatis-plus:
global-config:
banner: false
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志
map-underscore-to-camel-case: true # 开启驼峰
type-aliases-package: com.seata.order.entity #定义所有操作类的别名所在包
mapper-locations: classpath:mapper/*Mapper.xml
# Seata Config
seata:
application-id: order-service
tx-service-group: my_test_tx_group
service:
vgroup-mapping:
# 此处配置对应Server端配置registry.eureka.application的值
my_test_tx_group: default
registry:
type: eureka
eureka:
service-url: http://localhost:8761/eureka
weight: 1
business-service服务不需要连接数据库,所以不用配置datasource和mybatis-plus节点
server:
port: 9000
spring:
application:
name: business-service
eureka:
instance:
instance-id: business-service
prefer-ip-address: true
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://127.0.0.1:8761/eureka
# Seata Config
seata:
application-id: business-service
tx-service-group: my_test_tx_group
service:
vgroup-mapping:
# 此处配置对应Server端配置registry.eureka.application的值
my_test_tx_group: default
registry:
type: eureka
eureka:
service-url: http://localhost:8761/eureka
weight: 1
registry.conf中内容已经配置到 application.yml 中,因此不需要引用registry.conf文件
如果不想正application.yml中配置seata相关内容,只需要将seata相关的脚本file.conf、registry.conf拷贝到项目resource目录下
文件地址:https://github.com/seata/seata/tree/v1.4.2/script/client/conf
其中 registry.conf 修改后如下:
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa、custom
type = "eureka"
eureka {
serviceUrl = "http://localhost:8761/eureka"
weight = "1"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3、springCloudConfig、custom
type = "file"
file {
name = "file.conf"
}
}
/**
* 扣减库存
*/
int deductStorage(String commodityCode, int count);
/**
* 扣减库存
*
* @param commodityCode 商品编码
* @param count 数量
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int deductStorage(String commodityCode, int count) {
log.info("[库存服务]>------>扣减库存开始");
storageMapper.deductStorage(commodityCode, count);
log.info("[库存服务]>------>扣减库存结束");
return count;
}
/**
* 创建订单
*/
Long createOrder(Long userId, String commodityCode, int count);
/**
* 创建订单
*
* @param userId
* @param commodityCode
* @param count
* @return
*/
@Override
public Long createOrder(Long userId, String commodityCode, int count) {
log.info("[订单服务]>------>创建订单开始");
//扣减账户余额
Long price = storageService.selectPrice(commodityCode);
Long money = price * count;
// 创建订单
Order order = new Order();
order.setUserId(userId);
order.setCommodityCode(commodityCode);
order.setMoney(money);
order.setCount(count);
orderMapper.insert(order);
// 扣减账户
log.info("[订单服务]>------>扣减账户开始");
accountService.deductAccount(order.getUserId(), money);
log.info("[订单服务]>------>扣减账户结束");
log.info("[订单服务]>------>创建订单结束");
return order.getId();
}
/**
* 扣减账户
*/
Long deductAccount(Long userId, Long money);
/**
* 扣减账户
* @param userId
* @param money
* @return
*/
@Override
public Long deductAccount(Long userId, Long money) {
log.info("[账户服务]>------>扣减账户开始");
if (10000 == userId) {
throw new RuntimeException("[库存服务]>------>扣减库存异常");
}
accountMapper.deductAccount(userId, money);
log.info("[账户服务]>------>扣减账户结束");
return money;
}
/**
* 扣减库存-》创建订单
*
* @param userId 用户Id
* @param commodityCode 商品编码
* @param count 数量
*/
@Override
@GlobalTransactional(timeoutMills = 10000, name = "spring-cloud-seata", rollbackFor = Exception.class)
public Long purchase(Long userId, String commodityCode, int count) {
log.info("开始全局事务,XID = " + RootContext.getXID());
log.info("[采购服务]>------>扣减库存开始");
storageService.deductStorage(commodityCode, count);
log.info("[采购服务]>------>扣减库存结束");
log.info("[采购服务]>------>创建订单开始");
Long orderId = orderService.createOrder(userId, commodityCode, count);
log.info("[采购服务]>------>创建订单结束");
return orderId;
}
启动business服务,“register RM success. ”,表示RM已经注册到TC。
查看order服务的启动情况:
打开eureka注册中心,我们会清楚的看到各个服务的健康状态:
我们也可以从Seata服务端查看各个RM是否注册成功,如下图所示:
postman测试:
请求地址: http://localhost:9000/business/purchase?userId=10001&commodityCode=10001&count=1
数据库中:订单表已经有一条数据,库存表商品库存数量减1
发起创建订单的请求后,日志里面可以清楚的看到本次全局事务的情况,
xid=172.20.97.98:8091:5395550782355771430,branchId=5395550782355771432,branchType=AT
在Seata服务端,我们也能清楚的看到xid=172.20.97.98:8091:5395550782355771430这个全局事务的提交情况
账户服务中,如果userId = 1000,就抛出异常,进行异常测试
@Override
public Long deductAccount(Long userId, Long money) {
log.info("[账户服务]>------>扣减账户开始");
if (10000L == userId) {
throw new RuntimeException("[账户服务]>------>扣减账户异常");
}
accountMapper.deductAccount(userId, money);
log.info("[账户服务]>------>扣减账户结束");
return money;
}
http://localhost:9000/business/purchase?userId=10000&commodityCode=10001&count=1
这次全局事务ID 172.20.97.98:8091:5395550782355771442,开启全局事务
创建订单的时候,在order服务调用account服务的时候发生了异常 ,下面日志可以看出,数据已经开始回滚。
Branch Rollbacking: 172.20.97.98:8091:5395550782355771442 5395550782355771447 jdbc:mysql://127.0.0.1:3306/spring-cloud-seata-eureka
xid 172.20.97.98:8091:5395550782355771442 branch 5395550782355771447, undo_log deleted with GlobalFinished
Branch Rollbacked result: PhaseTwo_Rollbacked
回滚的时候,是将之前保存的undo_log 取出来,进行数据恢复,在发生异常的地方,我们打上断点,再次请求,会发现undo_log表会将前面执行的日志保存下来
undo_log保存之前的日志
源码地址:
GitHub - jeespring/spring-cloud-seata