距离上一次学习Spring Cloud全家桶已经是一年前,由于平时工作着CRUD,相关的知识点都忘却了,正好趁着这次复习,温固而知新。
为了模拟分布式事务的实际应用场景,以三个微服务来作为示例
分别是订单服务、库存服务、账户服务
用户下单,会在订单微服务创建一个订单,然后通过feign远程调用库存服务接口来扣减下单的库存,再通过远程调用账户微服务来扣减用户余额
简单来说,下单->扣库存->扣余额->更新订单状态
整个下单流程涉及到3个不同的数据库的操作,2次远程调用,明显会产生分布式事务问题
2.1创建seata数据库
建表
-- -------------------------------- 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.2创建业务数据库
seata_order:存储订单的数据库;
seata_storage:存储库存的数据库;
seata_account:存储账户信息的数据库。
建表
seata_order库中新建t_order表
CREATE TABLE `t_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
`product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
`count` int(11) DEFAULT NULL COMMENT '数量',
`money` decimal(11,0) DEFAULT NULL COMMENT '金额',
`status` int(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
SELECT * FROM t_order;
seata_storage库中新建t_storage表
CREATE TABLE t_storage (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`used` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '100', '0','100');
SELECT * FROM t_storage;
seata_account库中新建t_account表
CREATE TABLE t_account(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额',
`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '1000', '0', '1000');
SELECT * FROM t_account;
给上述3个库分别建对应的回滚日志表
-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库)
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
drop table `undo_log`;
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;
至此数据库准备工作完成
2.3安装并启动Seata服务
下载地址:http://seata.io/zh-cn/blog/download.html
我这里本地用的1.3.0版本
1、解压到本地目录
unzip seata-server-1.3.0.zip
2、修改配置文件
vim conf/file.conf
····
#只修改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.jdbc.Driver"
url = "jdbc:mysql://localhost:3306/seata" #修改url
user = "root" #修改用户名
password = "password" #修改密码
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
····
vim conf/registy.conf
·····
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos" #type改为nacos
nacos {
application = "seata-server"
serverAddr = "centos1:8848" #修改nacos地址
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos" #nacos账号
password = "nacos" #nacos密码
}
····
启动seata(注意这里要先启动nacos,否则会报错)
nohup sh seata-server.sh > ./seata.log 2>&1 &
在nacos服务列表看到seata-server实例,说明启动成功
使用代码生成器生成通用的增删查改操作,这里不再赘述,如有需要自行clone源码
Gitee地址
首先来看看下单前数据
order表
account表
storage表
下单业务类OrderServiceImpl,伪代码如下
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
* 简单说:下订单->扣库存->减余额->改状态
*/
@Override
public void create(Order order) {
log.info("----->开始新建订单");
//1 新建订单
orderDao.create(order);
//2 扣减库存
log.info("----->订单微服务开始调用库存,做扣减Count");
storageService.decrease(order.getProductId(),order.getCount());
log.info("----->订单微服务开始调用库存,做扣减end");
//3 扣减账户
log.info("----->订单微服务开始调用账户,做扣减Money");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("----->订单微服务开始调用账户,做扣减end");
//4 修改订单状态,从零到1,1代表已经完成
log.info("----->修改订单状态开始");
orderDao.update(order.getUserId(),0);
log.info("----->修改订单状态结束");
log.info("----->恭喜你,下单成功!~");
}
}
可以看到,业务代码基本与上描的流程一致
调用下单接口创建一个订单
localhost:2001/order/create?userId=1&productId=1&count=10&money=100
提示下单成功
来看数据
可以看到,订单已被成功创建,对应库存及余额也得到正确的扣减,但这只是最理想的情况,
此时来模拟出现一下异常情况,看看数据是否仍然符合我们的预期
在扣减账户余额处添加异常代码,如下
public void decrease(Long userId, BigDecimal money) {
int i = 10/0;
LOGGER.info("------->account-service中扣减账户余额开始");
accountDao.decrease(userId,money);
LOGGER.info("------->account-service中扣减账户余额结束");
}
再次创建订单
localhost:2001/order/create?userId=1&productId=1&count=10&money=100
由数据可以看出,订单创建成功库存也减了,钱却没有成功扣减,相当于商品免费送给客户···这可是大问题啊
使用Seata全局事务注解 @GlobalTransactional
找到业务的入口:订单服务的OrderServiceImpl,用@GlobalTransactional标注create()方法
/**
* @GlobalTransactional的属性说明
* --name:随便命名,只要名字不冲突
* --rollbackFor:发生什么异常的时候进行回滚,这里定义为Exception.class表示发生任何异常都进行回滚
*/
@Override
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
public void create(Order order) {
log.info("----->开始新建订单");
//1 新建订单
orderDao.create(order);
//2 扣减库存
log.info("----->订单微服务开始调用库存,做扣减Count");
storageService.decrease(order.getProductId(),order.getCount());
log.info("----->订单微服务开始调用库存,做扣减end");
//3 扣减账户
log.info("----->订单微服务开始调用账户,做扣减Money");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("----->订单微服务开始调用账户,做扣减end");
//4 修改订单状态,从零到1,1代表已经完成
log.info("----->修改订单状态开始");
orderDao.update(order.getUserId(),0);
log.info("----->修改订单状态结束");
log.info("----->gongxini xiadan chenggong ");
}
再次创建订单,报错页面依然提示500,但此时查看数据库,发现三个表的数据照旧,没有任何变动,那么说明该事务已经被成功被回滚。