使用 Seata 作为分布式事务组件,配置中心和注册中心使用 Nacos,使用 MySQL 数据库和 MyBatis,同时使用 Nacos 作为 Seata 的配置中心,使用 Dubbo 作为 RPC 框架
- 业务表
DROP DATABASE IF EXISTS seata;
CREATE DATABASE seata;
USE seata;
CREATE TABLE account
(
id INT AUTO_INCREMENT PRIMARY KEY,
balance INT,
last_update_time DATETIME DEFAULT now() ON UPDATE now()
);
CREATE TABLE product
(
id INT AUTO_INCREMENT PRIMARY KEY,
price INT,
stock INT,
last_update_time DATETIME DEFAULT now() ON UPDATE now()
);
CREATE TABLE orders
(
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
product_id INT,
pay_amount INT,
status VARCHAR(100),
add_time DATETIME DEFAULT now(),
last_update_time DATETIME DEFAULT now() ON UPDATE now()
);
INSERT INTO account (id, balance)
VALUES (1, 10);
INSERT INTO product (id, price, stock)
VALUES (1, 5, 10);
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,
ext VARCHAR(100) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8;
docker run --name nacos -p 8848:8848 -e MODE=standalone nacos/nacos-server
- 在 Seata Release 下载相应版本的 Seata Server 并解压
- 修改
conf/registry.conf
配置,将 type 改为nacos
registry {
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = ""
cluster = ""
group = "SEATA_GROUP"
}
}
config {
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = ""
group = "SEATA_GROUP"
}
}
- 修改
conf/nacos-config.txt
配置
修改 service.vgroupMapping
为自己应用对应的名称;如果有多个服务,添加相应的配置
如
service.vgroupMapping.my_test_tx_group=default
- 将 Seata 配置添加到 Nacos 中
cd conf
sh nacos-config.sh localhost
成功后会提示
init nacos config finished, please start seata-server
在 Nacos 管理页面应该可以看到有 47 个 Group 为SEATA_GROUP
的配置
- 启动 Seata Server
cd ..
sh ./bin/seata-server.sh -p 8091 -m file
启动后在 Nacos 的服务列表下面可以看到一个名为serverAddr
的服务
- 启动应用
先启动 Pay-Service 和 Storage-Service,待启动完成后再启动Order-Service
- 测试成功场景
调用 placeOrder 接口,将 price 设置为 1,此时余额为 10,可以下单成功
curl -X POST \
http://localhost:8081/order/placeOrder \
-H 'Content-Type: application/json' \
-d '{
"userId": 1,
"productId": 1,
"price": 1
}'
此时返回结果为:
{"success":true,"message":null,"data":null}
- 测试失败场景
设置 price 为 100,此时余额不足,会下单失败,pay-service会抛出异常,事务会回滚
curl -X POST \
http://localhost:8081/order/placeOrder \
-H 'Content-Type: application/json' \
-d '{
"userId": 1,
"productId": 1,
"price": 100
}'
查看 undo_log 的日志或者主键,可以看到在执行过程中有保存数据
如查看主键自增的值,在执行前后的值会发生变化,在执行前是 1,执行后是 5
SELECT
auto_increment
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'seata'
AND TABLE_NAME = 'undo_log'
这里是尤其需要注意的,Seata 是通过代理数据源实现事务分支,所以需要配置 io.seata.rm.datasource.DataSourceProxy
的 Bean,且是 @Primary
默认的数据源,否则事务不会回滚,无法实现分布式事务
@Configuration
public class DataSourceProxyConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
@Primary
@Bean
public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
return sqlSessionFactoryBean.getObject();
}
}
如果使用的是 Hikari 数据源,需要修改数据源的配置,以及注入的 Bean 的配置前缀
spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.jdbc-url=jdbc:mysql://localhost:3306/seata?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
spring.datasource.hikari.username=root
spring.datasource.hikari.password=123456
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public DataSource dataSource() {
return new HikariDataSource();
}
需要注意的是当前Spring Boot 和 Spring Cloud 以及 Spring Cloud Alibaba 的版本号需要互相对应,否则可能会存在各种问题;具体可以参考版本说明