分布式应用下事务的解决方案之一:SpringBoot+Alibaba-Seata简单应用
本案例来自于 B站尚硅谷 视频
分布式微服务的事务处理非常重要,由于本人刚入行,其实现原理尚不清楚,本文只是介绍如何简单的体验一下seata
相关代码已经上传Github,需要可自行download
https://github.com/DouGuangJi/Seata/tree/master
1、环境准备:
Maven:3.6.1
数据库:Mysql
Nacos:1.1.3
Seata:1.0.0
操作系统: win10
开发工具:Idea 2019.3
JDK:1.8
2、Seata 官网:
官网:http://seata.io/zh-cn/index.html
Github:https://github.com/seata/seata
3、Seata 简介
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
相关术语:
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
4、Seata-Server 安装:
4.1、下载解压
下载地址:http://seata.io/zh-cn/blog/download.html
解压后目录如下
4.2、修改配置
建议修改之前养成备份文件的好习惯。
打开conf下file.conf做如下修改:
打开conf 下 registry.conf 做如下修改:
4.3、去自己的数据库建库建表
库名:seata
表sql:https://github.com/seata/seata/blob/1.0.0/script/server/db/mysql.sql
5、启动nacos
......................................................................
6、启动seata
bin/seata-server.bat 双击即可
完成后可以看到 服务已经注册到nacos上
7、业务说明
我们创建三个服务,一个订单服务,一个库存服务、一个账户服务
当客户下单,会在订单服务创建一个订单,然后远程调用库存服务来扣减下单商品的库存,再通过远程调账户服务来扣减用户账户中的余额,最后在订单服务中修改订单状态为已经完成。
该操作跨越三个库,有两次远程调用,可以演示分布式事务。
8、建库SQL
--存储订单的库
CREATE DATABASE seata_order;
--存储库存的库
CREATE DATABASE seata_storage;
--存储账户信息的库
CREATE DATABASE seata_account;
9、建表SQL
--在seata_order下新建:
CREATE TABLE t_order(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`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:已完结'
) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;SELECT * FROM t_order;
--在seata_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=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_storage.t_storage(`id`,`product_id`,`total`,`used`,`residue`)
VALUES('1','1','100','0','100');
--在seata_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=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_account.t_account(`id`,`user_id`,`total`,`used`,`residue`) VALUES('1','1','1000','0','1000')
然后分别在这三个库下建seata的日志表:
db_undo_log.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, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`))
ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
数据库如下图所示:
10、Idea新建工程
采用父工程pom聚合方式构建
在父工程下新建三个module
Cloud-Alibaba-Seata-OrderService-2001
Cloud-Alibaba-Seata-StorageService-2002
Cloud-Alibaba-Seata-AccountService-2003
相关代码已经上传Github,需要可自行download
https://github.com/DouGuangJi/Seata/tree/master
贴部分代码(order模块):
@Service
@Slf4j
public class OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private StorageService storageService;
@Autowired
private AccountService accountService;
/**
* 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
*/
//切记,第一次启动将该注解注释调
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
public void create(Order order){
log.info("----->开始新建订单");
//新建订单,调用本服务接口
orderDao.create(order);
//扣减库存,通过openFeign远程调用库存服务
log.error("----->订单微服务开始调用库存,做扣减Count");
storageService.decrease(order.getProductId(),order.getCount());
log.error("----->订单微服务开始调用库存,做扣减end");
//扣减账户。通过openFeign远程调用账户服务
log.error("----->订单微服务开始调用账户,做扣减Money");
accountService.decrease(order.getUserId(),order.getMoney());
log.error("----->订单微服务开始调用账户,做扣减end");
//修改订单状态,从零到1代表已经完成,调用本服务
log.error("----->修改订单状态开始");
orderDao.update(order.getUserId(),0);
log.error("----->修改订单状态结束");
log.error("----->下订单结束了");
}}
controller:
@RestController
@Slf4j
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public CommonResult create(Order order) {
orderService.create(order);
return new CommonResult(200,"订单创建成功");
}}
主启动类:
@EnableDiscoveryClient
@EnableFeignClients
/**
* 取消数据源自动创建的配置
*/
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SeataOrderApplication {
public static void main(String[] args) {
SpringApplication.run(SeataOrderApplication.class, args);
}}
11、测试
### 11.1、三个模块启动之后,GET请求 :(注意,此时的测试是没有异常的,业务顺利执行完成,对应看到表数据)
http://localhost:2001/order/create?userId=1&productId=1&count=1&money=100
该请求首先会在t_order 表中创建一个订单
然后在t_storage表处理库存
然后在t_account表处理账户
最后在t_order修改订单状态(t_order中的status已经变为1)
### 11.2、模拟异常,openFeign超时异常,在account 模块模拟
此时我们再去请求:
http://localhost:2001/order/create?userId=1&productId=1&count=1&money=100,返回超时异常
我们的需求是:发生异常,要保证数据的一致性。
此时再去数据库看表,发现数据出现问题,订单创建了,库存减少了,账户没扣钱,状态未改变,此时就出现了分布式事务问题
11.3、解决方案:
此时我们须在我们order服务加:
然后再去请求:
http://localhost:2001/order/create?userId=1&productId=1&count=1&money=100
会发现返回超时异常,但数据没有改变,也解决了我们的分布式事务问题。
至此seata的最基本最简单应用ok,但是seata的学习还远远未结束,如此优秀的框架,很值得我们去深入学习,搞明白其原理之时,回来继续完善。