seata官网地址
1、TM请求TC开始一个新的全局事务,TC生成一个表示全局事务的XID。
2、XID通过微服务的调用链传播。
3、RM将本地事务注册为XID对应全局事务的分支到TC。
4、TM请求TC提交或回滚XID对应的全局事务。
5、TC驱动XID对应全局事务下的所有分支事务来完成分支提交或回滚。
file模式:适合单机部署,全局事务和分支事务的数据持久化到本地文件。
db模式:适合集群部署,全局事务和分支事务的数据通过db进行共享。
打开 seata下载页面,选择最新的版本下载
# 创建seata目录
$ mkdir -p /usr/seata
$ cd /usr/seata
# 下载
$ wget https://www.github.com//seata/seata/releases/download/v1.4.0/seata-server-1.4.0.tar.gz
# 解压文件
$ tar -zxvf seata-server-1.4.0.tar.gz
# 查看seata目录
$ cd seata
$ ll
总用量 48
drwxr-xr-x. 2 root root 53 1月 19 10:21 bin # 启动TC的shell脚本
drwxr-xr-x. 3 root root 141 11月 2 17:30 conf # 配置文件
drwxr-xr-x. 3 root root 8192 11月 2 17:30 lib # 依赖的jar包
-rw-r--r--. 1 root root 11365 5月 13 2019 LICENSE
# 后台启动 tc server
$ nohup sh bin/seata-server.sh &
# 查看nohup.out日志,出现Server started表示启动成功,默认端口是8091
$ tail -f nohup.out
1:21:52.477 INFO --- [ main] io.seata.config.FileConfiguration : The file name of the operation is registry
11:21:52.486 INFO --- [ main] io.seata.config.FileConfiguration : The configuration file used is /usr/seata/seata/conf/registry.conf
11:21:53.094 INFO --- [ main] io.seata.config.FileConfiguration : The file name of the operation is file.conf
11:21:53.094 INFO --- [ main] io.seata.config.FileConfiguration : The configuration file used is file.conf
11:22:05.452 INFO --- [ main] i.s.core.rpc.netty.NettyServerBootstrap : Server started, listen port: 8091
# 再次查看seata目录,发现多了nohup.out文件和sessionStore目录
$ ll
drwxr-xr-x. 2 root root 53 1月 19 10:21 bin
drwxr-xr-x. 3 root root 141 11月 2 17:30 conf
drwxr-xr-x. 3 root root 8192 11月 2 17:30 lib
-rw-r--r--. 1 root root 11365 5月 13 2019 LICENSE
-rw-------. 1 root root 32966 1月 19 11:22 nohup.out
drwxr-xr-x. 2 root root 23 1月 19 10:26 sessionStore
# 查看sessionStore目录,file模式下持久化的文件就是 root.data
$ cd sessionStore
$ ll
-rw-r--r--. 1 root root 0 1月 19 10:26 root.data
1、使用 mysql.sql 脚本初始化TC Server 数据库,脚本内容如下:
-- -------------------------------- 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;
2、创建seata数据库,执行上述sql脚本,完成后的结果如下图所示:
3、修改 conf/file.conf 配置文件,设置为db模式,内容如下所示:
## transaction log store, only used in seata-server
store {
## store mode: file、db、redis
mode = "db"
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.cj.jdbc.Driver"
url = "jdbc:mysql://192.168.0.118:3306/seata?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8"
user = "root"
password = "123456"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
service {
vgroupMapping.my_test_tx_group = "seata-tc-server"
}
4、修改 conf/registry.conf 配置文件,设置使用Eureka注册中心,application表示TC Server 注册到eureka显示的服务名称。
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "eureka"
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10
eureka {
serviceUrl = "http://192.168.0.118:8761/eureka"
application = "seata-tc-server"
weight = "1"
}
}
5、指定端口号、节点编号启动TC Server,先后执行如下shell命令启动两个节点
# 启动第一个tc server节点
$ nohup sh bin/seata-server.sh -p 18090 -n 1 &
# 启动第二个tc server节点
$ nohup sh bin/seata-server.sh -p 28090 -n 2 &
打印 nohup.out 文件里面的日志
$ tail -f nohup.out
14:23:49.581 INFO --- [ main] io.seata.config.FileConfiguration : The file name of the operation is registry
14:23:49.623 INFO --- [ main] io.seata.config.FileConfiguration : The configuration file used is /usr/seata/seata/conf/registry.conf
14:23:52.019 INFO --- [ main] io.seata.config.FileConfiguration : The file name of the operation is file.conf
14:23:52.027 INFO --- [ main] io.seata.config.FileConfiguration : The configuration file used is file.conf
# 初始化连接池
14:24:06.970 INFO --- [ main] com.alibaba.druid.pool.DruidDataSource : {
dataSource-1} inited
# 节点启动成功
14:24:11.591 INFO --- [ main] i.s.core.rpc.netty.NettyServerBootstrap : Server started, listen port: 28090
......
# 注册中心连接成功
14:24:42.806 INFO --- [ main] com.netflix.discovery.DiscoveryClient : Discovery Client initialized at timestamp 1611037482804 with initial instances count: 1
14:24:42.807 INFO --- [ main] com.netflix.discovery.DiscoveryClient : Saw local status change event StatusChangeEvent [timestamp=1611037482807, current=UP, previous=STARTING]
14:24:42.810 INFO --- [-InstanceInfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_SEATA-TC-SERVER/192.168.0.120:seata-tc-server:28090: registering service...
14:24:42.936 INFO --- [-InstanceInfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_SEATA-TC-SERVER/192.168.0.120:seata-tc-server:28090 - registration status: 204
浏览器访问Eureka注册中心,可以看到两个TC Server节点注册成功!
分别创建seata_order、seata_storage、seata_account三个数据库,因为使用的是Seata AT模式,所以每个数据库必须创建 undo_log 表,主要用于分支事务的回滚,sql脚本如下:
# 订单库
CREATE DATABASE seata_order;
CREATE TABLE seata_order.orders
(
id INT(11) NOT NULL AUTO_INCREMENT,
product_id INT(11) DEFAULT NULL COMMENT '商品id',
pay_amount DECIMAL(10, 2) DEFAULT NULL COMMENT '支付金额',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建日期',
last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新日期',
PRIMARY KEY (id)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
CREATE TABLE seata_order.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';
# 仓储库
CREATE DATABASE seata_storage;
CREATE TABLE seata_storage.product
(
id INT(11) NOT NULL AUTO_INCREMENT,
stock INT(11) DEFAULT NULL COMMENT '库存数量',
last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新日期',
PRIMARY KEY (id)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
# 初始化商品的库存
INSERT INTO seata_storage.product (id, stock) VALUES (1, 10);
CREATE TABLE seata_storage.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';
# 账户库
CREATE DATABASE seata_account;
CREATE TABLE seata_account.account
(
id INT(11) NOT NULL AUTO_INCREMENT COMMENT '账户id',
balance DECIMAL(10, 2) DEFAULT NULL COMMENT '账户余额',
last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新日期',
PRIMARY KEY (id)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
# 初始化账户金额
INSERT INTO seata_account.account (id, balance) VALUES (1,100);
CREATE TABLE seata_account.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';
<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.0modelVersion>
<groupId>com.examplegroupId>
<artifactId>seata-order-demoartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>seata-order-demoname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
<spring-boot-parent.version>2.3.0.RELEASEspring-boot-parent.version>
<spring-cloud.version>Hoxton.SR9spring-cloud.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>${spring-boot-parent.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
<version>2.2.4.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.22version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.1.4version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<finalName>seata-order-demofinalName>
<resources>
<resource>
<directory>src/main/javadirectory>
<includes>
<include>com/**/dao/*.xmlinclude>
includes>
resource>
<resource>
<directory>src/main/resourcesdirectory>
<includes>
<include>**/*.ymlinclude>
<include>**/*.propertiesinclude>
<include>**/*.xmlinclude>
includes>
<filtering>falsefiltering>
resource>
resources>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<mainClass>com.example.seataorderdemo.SeataOrderDemoApplicationmainClass>
configuration>
<executions>
<execution>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
plugin>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<configuration>
<source>1.8source>
<target>1.8target>
<encoding>UTF-8encoding>
configuration>
plugin>
plugins>
build>
project>
2、application.properties配置文件
spring.application.name=seata-order-demo
server.port=3010
# 注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8761/eureka
# 客户端给eureka server发送心跳的频率
eureka.instance.lease-renewal-interval-in-seconds=10
# 客户端的ip注册到eureka server
eureka.instance.prefer-ip-address=true
# 自定义instance-id
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}
# 连接数据库
spring.datasource.url=jdbc:mysql://192.168.0.118:3306/seata_order?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 连接池名称
spring.datasource.hikari.pool-name=seata_order_demo_hikari_pool
# 最小空闲连接数
spring.datasource.hikari.minimum-idle=5
# 空闲连接存活最大时间,默认600000(10分钟)
spring.datasource.hikari.idle-timeout=180000
# 连接池中的最大连接数
spring.datasource.hikari.maximum-pool-size=20
# 客户端获取连接池连接的超时时长,默认30秒
spring.datasource.hikari.connection-timeout=30000
# 设置要执行的SQL查询,测试连接的有效性
spring.datasource.hikari.connection-test-query=SELECT 1
# 下划线的数据库字段映射成驼峰风格的实体对象字段
mybatis.configuration.map-underscore-to-camel-case=true
# 打印数据库日志
logging.level.com.example.seataorderdemo.dao=debug
# seata应用编号
seata.application-id=${spring.application.name}
# seata事务组编号
seata.tx-service-group=my_test_tx_group
# seata虚拟组映射
seata.service.vgroup-mapping.my_test_tx_group=seata-tc-server
# 指定注册中心的类型
seata.registry.type=eureka
# 注册中心的地址
seata.registry.eureka.service-url=http://localhost:8761/eureka
# 指定seata tc server集群的名字
seata.registry.eureka.application=seata-tc-server
这里需要特别注意的是,seata事务组编号要和TC Server的file.conf文件里面的事务组编号保持一致!!
@RestController
@RequestMapping("/order")
public class OrderController {
private Logger logger = LoggerFactory.getLogger(OrderController.class);
@Autowired
private OrderService orderService;
@PostMapping("/createOrder")
public void createOrder(Integer accountId, Integer productId,
BigDecimal payAmount, Integer buyNumber) {
logger.info("生成订单,账户id:{},商品id:{},支付金额:{},购买数量:{}",
accountId, productId, payAmount, buyNumber);
orderService.createOrder(accountId, productId, payAmount, buyNumber);
}
}
4、OrderService 接口
public interface OrderService {
/**
* 生成订单
*
* @param accountId 账户id
* @param productId 商品id
* @param payAmount 支付金额
* @param buyNumber 购买数量
*/
void createOrder(Integer accountId, Integer productId,
BigDecimal payAmount, Integer buyNumber);
}
5、OrderServiceImpl类,方法使用 @GlobalTransactional 注解声明全局事务
@Service
public class OrderServiceImpl implements OrderService {
private Logger logger = LoggerFactory.getLogger(OrderServiceImpl.class);
@Autowired
private OrderMapper orderMapper;
@Autowired
private StorageFeignClient storageFeignClient;
@Autowired
private AccountFeignClient accountFeignClient;
@Override
@GlobalTransactional(rollbackFor = Exception.class)
public void createOrder(Integer accountId, Integer productId,
BigDecimal payAmount, Integer buyNumber) {
logger.info("全局事务XID:{}", RootContext.getXID());
// 扣减库存
storageFeignClient.reduceStock(productId, buyNumber);
// 扣减账户余额
accountFeignClient.reduceBalance(accountId, payAmount);
// 生成订单
orderMapper.saveOrder(productId, payAmount);
logger.info("订单生成成功!");
}
}
6、OrderMapper 接口,保存订单
@Mapper
@Repository
public interface OrderMapper {
/**
* 保存订单
*
* @param productId 商品id
* @param payAmount 支付金额
* @return Integer
*/
@Insert("INSERT INTO orders(product_id,pay_amount,create_time) " +
"VALUES(#{productId},#{payAmount},NOW())")
Integer saveOrder(@Param("productId") Integer productId,
@Param("payAmount") BigDecimal payAmount);
}
7、AccountFeignClient接口,使用feign声明式调用账户服务
@FeignClient("seata-account-demo")
public interface AccountFeignClient {
/**
* 扣减余额
*
* @param accountId 账户id
* @param amount 扣减金额
*/
@PostMapping("/account/reduceBalance")
void reduceBalance(@RequestParam("accountId") Integer accountId,
@RequestParam("amount") BigDecimal amount);
}
8、StorageFeignClient接口,使用feign声明式调用仓储服务
@FeignClient("seata-storage-demo")
public interface StorageFeignClient {
/**
* 扣减库存
*
* @param productId 商品id
* @param number 扣减数量
*/
@PostMapping("/storage/reduceStock")
void reduceStock(@RequestParam("productId") Integer productId,
@RequestParam("number") Integer number);
}
9、SeataOrderDemoApplication 启动类
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan(basePackages = "com.example.seataorderdemo.dao")
@EnableFeignClients
public class SeataOrderDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SeataOrderDemoApplication.class, args);
}
}
注解 | 作用 |
---|---|
@EnableDiscoveryClient | 开启服务发现,自动注册到注册中心 |
@MapperScan | 自动扫描指定目录下面的Mapper接口 |
@EnableFeignClients | 启用feign声明式调用 |
@RestController
@RequestMapping("/storage")
public class StorageController {
private Logger logger = LoggerFactory.getLogger(StorageController.class);
@Autowired
private ProductService productService;
@PostMapping("/reduceStock")
public void reduceStock(Integer productId, Integer number) throws Exception {
logger.info("扣减库存,商品id:{},数量:{}", productId, number);
productService.reduceStock(productId, number);
}
}
4、ProductService 接口
public interface ProductService {
/**
* 扣减库存数量
*
* @param productId 商品id
* @param number 扣减数量
* @throws Exception
*/
void reduceStock(Integer productId, Integer number) throws Exception;
}
5、ProductServiceImpl 类,注解@Transactional表示开启分支事务
@Service
public class ProductServiceImpl implements ProductService {
private Logger logger = LoggerFactory.getLogger(ProductServiceImpl.class);
@Autowired
private ProductDao productDao;
@Override
@Transactional(rollbackFor = Exception.class)
public void reduceStock(Integer productId, Integer number) throws Exception {
logger.info("全局事务XID:{}", RootContext.getXID());
// 检查库存是否充足
Integer stock = productDao.getProductStock(productId);
if (number.compareTo(stock) > 0) {
logger.error("库存不足,商品id:{},扣减数量:{}", productId, number);
throw new Exception("库存不足");
}
// 更新库存数量
Integer result = productDao.updateProductStock(productId, number);
if (result <= 0) {
logger.error("更新商品库存失败,商品id:{},扣减数量:{}", productId, number);
throw new Exception("更新商品库存失败");
}
logger.info("更新商品库存成功,商品id:{},扣减数量:{}", productId, number);
}
}
6、ProductDao 接口
@Mapper
@Repository
public interface ProductDao {
/**
* 查询商品的库存数量
*
* @param productId 商品id
* @return Integer
*/
@Select("SELECT stock FROM product where id=#{productId}")
Integer getProductStock(@Param("productId") Integer productId);
/**
* 更新商品的库存数量
*
* @param productId 商品id
* @param number 扣减数量
* @return Integer
*/
@Update("UPDATE product SET stock=stock-#{number} WHERE id=#{productId}")
Integer updateProductStock(@Param("productId") Integer productId,
@Param("number") Integer number);
}
7、SeataStorageDemoApplication 启动类
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan(basePackages = "com.example.seatastoragedemo.dao")
public class SeataStorageDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SeataStorageDemoApplication.class, args);
}
}
@RestController
@RequestMapping("/account")
public class AccountController {
private Logger logger = LoggerFactory.getLogger(AccountController.class);
@Autowired
private AccountService accountService;
@PostMapping("/reduceBalance")
public void reduceBalance(Integer accountId, BigDecimal amount) throws Exception {
logger.info("扣减账户余额,账户Id:{},扣减金额:{}", accountId, amount);
accountService.reduceBalance(accountId, amount);
}
}
4、AccountService 接口
public interface AccountService {
/**
* 扣减账户余额
*
* @param accountId 账户id
* @param amount 金额
* @return Boolean
*/
void reduceBalance(Integer accountId, BigDecimal amount) throws Exception;
}
5、AccountServiceImpl 类,注解@Transactional表示开启分支事务
@Service
public class AccountServiceImpl implements AccountService {
private Logger logger = LoggerFactory.getLogger(AccountServiceImpl.class);
@Autowired
private AccountMapper accountMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void reduceBalance(Integer accountId, BigDecimal amount) throws Exception {
logger.info("全局事务XID:{}", RootContext.getXID());
// 检查账户余额是否充足
BigDecimal balance = accountMapper.getAccountBlance(accountId);
if (amount.compareTo(balance) > 0) {
logger.error("账户余额不足,扣减失败!accountId:{},amount:{}", accountId, amount);
throw new Exception("账户余额不足!");
}
// 更新账户余额
Integer result = accountMapper.updateAccountBlance(accountId, amount);
if (result <= 0) {
logger.error("更新账户余额失败!accountId:{},amount:{}", accountId, amount);
throw new Exception("更新账户余额失败!");
}
logger.info("更新账户余额成功!accountId:{},amount:{}", accountId, amount);
}
}
6、AccountMapper 接口
@Mapper
@Repository
public interface AccountMapper {
/**
* 查询账户余额
*
* @param accountId 账户id
* @return BigDecimal
*/
@Select("SELECT t.balance FROM account t WHERE t.id=#{accountId}")
BigDecimal getAccountBlance(@Param("accountId") Integer accountId);
/**
* 更新账户余额
*
* @param accountId 账户id
* @param amount 金额
* @return Integer
*/
@Update("UPDATE account SET balance=balance-#{amount} WHERE id=#{accountId}")
Integer updateAccountBlance(@Param("accountId") Integer accountId,
@Param("amount") BigDecimal amount);
}
7、SeataAccountDemoApplication 启动类
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan(basePackages = "com.example.seataaccountdemo.dao")
public class SeataAccountDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SeataAccountDemoApplication.class, args);
}
}
# seata 数据源代理
2021-01-21 12:50:49.037 INFO 15840 --- [ main] .s.s.a.d.SeataAutoDataSourceProxyCreator : Auto proxy of [dataSource]
# seata 全局事务扫描
2021-01-21 12:50:49.684 INFO 15840 --- [ main] i.s.s.a.GlobalTransactionScanner : Bean[com.example.seataorderdemo.service.impl.OrderServiceImpl] with name [orderServiceImpl] would use interceptor [io.seata.spring.annotation.GlobalTransactionalInterceptor]
# 注册 Seata Resource Manager 到端口为28090的Seata TC Server 成功
2021-01-21 12:50:56.150 INFO 15840 --- [ main] i.s.c.r.netty.NettyClientChannelManager : will connect to 192.168.0.120:28090
2021-01-21 12:50:56.150 INFO 15840 --- [ main] i.s.c.rpc.netty.RmNettyRemotingClient : RM will register :jdbc:mysql://192.168.0.118:3306/seata_order
2021-01-21 12:50:56.157 INFO 15840 --- [ main] i.s.core.rpc.netty.NettyPoolableFactory : NettyPool create channel to transactionRole:RMROLE,address:192.168.0.120:28090,msg:< RegisterRMRequest{
resourceIds='jdbc:mysql://192.168.0.118:3306/seata_order', applicationId='seata-order-demo', transactionServiceGroup='my_test_tx_group'} >
2021-01-21 12:51:03.812 INFO 15840 --- [ main] i.s.c.rpc.netty.RmNettyRemotingClient : register RM success. client version:1.3.0, server version:1.4.0,channel:[id: 0x7a38a004, L:/192.168.0.118:60927 - R:/192.168.0.120:28090]
2021-01-21 12:51:03.819 INFO 15840 --- [ main] i.s.core.rpc.netty.NettyPoolableFactory : register success, cost 3205 ms, version:1.4.0,role:RMROLE,channel:[id: 0x7a38a004, L:/192.168.0.118:60927 - R:/192.168.0.120:28090]
# 注册 Seata Resource Manager 到端口为18090的Seata TC Server 成功
2021-01-21 12:51:03.820 INFO 15840 --- [ main] i.s.c.r.netty.NettyClientChannelManager : will connect to 192.168.0.120:18090
2021-01-21 12:51:03.820 INFO 15840 --- [ main] i.s.c.rpc.netty.RmNettyRemotingClient : RM will register :jdbc:mysql://192.168.0.118:3306/seata_order
2021-01-21 12:51:03.820 INFO 15840 --- [ main] i.s.core.rpc.netty.NettyPoolableFactory : NettyPool create channel to transactionRole:RMROLE,address:192.168.0.120:18090,msg:< RegisterRMRequest{
resourceIds='jdbc:mysql://192.168.0.118:3306/seata_order', applicationId='seata-order-demo', transactionServiceGroup='my_test_tx_group'} >
2021-01-21 12:51:05.406 INFO 15840 --- [ main] i.s.c.rpc.netty.RmNettyRemotingClient : register RM success. client version:1.3.0, server version:1.4.0,channel:[id: 0x1a022a55, L:/192.168.0.118:60930 - R:/192.168.0.120:18090]
2021-01-21 12:51:05.406 INFO 15840 --- [ main] i.s.core.rpc.netty.NettyPoolableFactory : register success, cost 1585 ms, version:1.4.0,role:RMROLE,channel:[id: 0x1a022a55, L:/192.168.0.118:60930 - R:/192.168.0.120:18090]
2、使用postman请求创建订单的接口
3、各个服务控制台打印日志
2、使用postman生成订单,支付金额为1000元,账户余额不足会抛异常
spring-cloud-seata-demo
至此,Spring Cloud Alibaba 分布式事务 Seata 入门分享完毕