配套资料,免费下载
链接:https://pan.baidu.com/s/1-eRFozbFIShqbqNRFD9KDw
提取码:rt3w
复制这段内容后打开百度网盘手机App,操作更方便哦
事务是数据库的概念,数据库事务(ACID:原子性、一致性、隔离性和持久性)。
分布式事务的产生,是由于数据库的拆分和分布式架构(微服务)带来的,在常规情况下,我们在一个进程中操作一个数据库,这属于本地事务,如果在一个进程中操作多个数据库,或者在多个进程中操作一个或多个数据库,就产生了分布式事务;
(1)数据库分库分表就产生了分布式事务;
(2)项目拆分服务化也产生了分布式事务;
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
Seata为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的分布式解决方案。
四种事务模式中,目前使用的流行度情况是:AT > TCC > Saga、XA。我们可以参看Seata各公司使用列表:https://github.com/seata/seata/issues/1246
因此,我们教学的重点和学习的重点将会放到AT模式和TCC模式的讲解上,Seata默认就是AT模式,简单的一句话来说这两种模式的区别(后边会深入讲解):
从上边直观的来看,感觉TCC模式更厉害一点,实际上,Seata默认的AT模式,事务失败回滚并不用程序员自己来做,而是由Seata框架本身来完成的,而TCC模式的事务失败回滚等操作,全部需要手动实现,因此,AT模式在实际生产环境中用的更多一点,也更方便一点,除了特定场景下的特殊需要,AT模式基本都能满足。
当然了,这里提前说一下,我们接下来会学习Seata的单机版部署和高可用集群版部署,而正好,我们要学习AT和TCC两种模式,我们在学习AT模式的时候,使用Seata单机版环境、而在学习TCC模式的时候,使用Seata的高可用集群版的环境,一定注意,这么安排存粹是为了教学方便,实际上AT也能用高可用集群版环境。
官方文档:http://seata.io/zh-cn/
在Seata的架构中,一共有三个角色:
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
其中TC为单独部署的 Server 服务端,TM和RM为嵌入到应用中的 Client 客户端,除了以上三种角色外,还有一个全局事务id:Transaction ID XID 。
在Seata中,一个分布式事务的生命周期如下:
TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;
XID 在微服务调用链路的上下文中传播;
RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;
TM 向 TC 发起针对 XID 的全局提交或回滚决议;
TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
我们先部署单机环境的 Seata TC Server,用于学习或测试,在生产环境中要部署集群环境;
因为TC需要进行全局事务和分支事务的记录,所以需要对应的存储,目前,TC有三种存储模式( store.mode ):
我们先采用file模式,最终我们部署单机TC Server如下图所示:
截止到2021年2月24日,官方最新发布的版本为1.4.1
,但是我们不能下载最新的这个版本来进行学习,因为在引入spring-cloud-starter-alibaba-seata
依赖以后,内置自带的版本为1.3.0
,因此,我们需要保持版本一致,所以,我们需要下载1.3.0
。
下载地址:https://github.com/seata/seata/releases/download/v1.3.0/seata-server-1.3.0.zip
双击运行:bin\seata-server.bat
两阶段提交协议的演变:
反向补偿:简单说就是给某一个字段加了10,反向补偿就减去10,这样数据保持不变。新增一条记录,反向补偿就删除以前新增的那条记录。
以一个示例来说明:
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。
tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。
如果不考虑事务的隔离性,可能会引发读安全性问题:
隔离级别 | 中文说明 | 说明 |
---|---|---|
READ UNCOMMITTED | 读未提交 | 不能解决以上所有读问题,效率最高,安全性最低,一般不用 |
READ COMMITTED | 读已提交 | 避免脏读,不可重复读和幻读有可能发生,Oracle默认的隔离级别 |
REPEATABLE READ | 可重复读 | 避免脏读、不可重复读,幻读有可能发生,MySQL默认的隔离级别 |
SERIALIZABLE | 串行化 | 可以解决以上所有读问题,效率最差,安全性最高,一般不用 |
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE
语句的代理。
SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
以一个示例来说明整个 AT 分支的工作过程。
业务表:product
Field | Type | Key |
---|---|---|
id | bigint(20) | PRI |
name | varchar(100) | |
since | varchar(100) |
AT 分支事务的业务逻辑:
update product set name = 'GTS' where name = 'TXC';
过程:
select id, name, since from product where name = 'TXC';
得到前镜像:
id | name | since |
---|---|---|
1 | TXC | 2014 |
select id, name, since from product where id = 1`;
得到后镜像:
id | name | since |
---|---|---|
1 | GTS | 2014 |
UNDO_LOG
表中。{
"branchId": 641789253,
"undoItems": [{
"afterImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"beforeImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"sqlType": "UPDATE"
}],
"xid": "xid:xxx"
}
product
表中,主键值等于 1 的记录的 全局锁 。update product set name = 'TXC' where id = 1;
回滚日志表
UNDO_LOG Table:不同数据库在类型上会略有差别。
以 MySQL 为例:
Field | Type |
---|---|
branch_id | bigint PK |
xid | varchar(100) |
context | varchar(128) |
rollback_info | longblob |
log_status | tinyint |
log_created | datetime |
log_modified | datetime |
-- 注意此处0.7.0+ 增加字段 context
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
数据库环境为mysql 5.7.33
,请重新导入运行以下sql语句:
DROP DATABASE IF EXISTS `seata_account`;
CREATE DATABASE `seata_account`;
USE `seata_account`;
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT 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 '剩余额度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='账户表';
insert into `t_account`(`id`,`user_id`,`total`,`used`,`residue`) values (1,1,'1000','0','1000');
SELECT * FROM `seata_account`.`t_account` LIMIT 0, 1000;
数据库环境为mysql 5.7.33
,请重新导入运行以下sql语句:
DROP DATABASE IF EXISTS `seata_storage`;
CREATE DATABASE `seata_storage`;
USE `seata_storage`;
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`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 '剩余库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='库存表';
insert into `t_storage`(`id`,`product_id`,`total`,`used`,`residue`) values (1,1,100,0,100);
SELECT * FROM `seata_storage`.`t_storage` LIMIT 0, 1000;
数据库环境为mysql 5.7.33
,请重新导入运行以下sql语句:
DROP DATABASE `seata_order`;
CREATE DATABASE `seata_order`;
USE `seata_order`;
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`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 '状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单表';
SELECT * FROM `seata_order`.`t_order` LIMIT 0, 1000;
在桌面重新创建一个文件夹,名字无所谓,拷贝配套资料中的单体版\single-app-seata-at-study
到此文件夹,然后使用idea
打开即可。
single-app-seata-at-study
已经实现了基本的代码流程,非常简单,相信你一定能看懂:
single-app-seata-at-study
是一个纯粹的Spring Boot
单体应用,连接着三个数据源:
唯一需要你注意的是,打开application.yaml
,查看连接数据源的账户密码是否正确,如下:
请启动当前工程,然后输入下单地址测试:http://localhost:9000/order/create?userId=1&productId=1&count=10&money=100&status=1
请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表
修改com.caochenlei.service.impl.AccountServiceImpl
代码添加异常,代码如下:
@Override
public void decrease(Long userId, BigDecimal money) {
int i = 1 / 0; //模拟异常出错
accountMapper.decrease(userId, money);
}
请重启当前工程,然后输入下单地址测试:http://localhost:9000/order/create?userId=1&productId=1&count=10&money=100&status=1
请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表
我们发现订单下单失败,订单数据库中多了一条订单记录,实际上这条记录不应该有,而且账户对应的余额和库存并没有减少,这个问题是十分可怕的。因为没有事务的支持,不能做到要不全部执行,要不全部失败。
在账户数据库
添加回滚日志表
,sql语句如下:
USE `seata_account`;
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
在库存数据库
添加回滚日志表
,sql语句如下:
USE `seata_storage`;
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
在订单数据库
添加回滚日志表
,sql语句如下:
USE `seata_order`;
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
最终完成以后的效果如下图:
在pom.xml
中新增依赖:
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
<version>1.3.0version>
dependency>
在application.yaml
中新增配置:
server:
port: 9000
management:
endpoints:
web:
exposure:
include: '*'
spring:
application:
name: single-app-seata-at-study
datasource:
dynamic:
primary: ordere-ds
datasource:
#账户数据源(account-ds自定义的)
account-ds:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useUnicode=true&useSSL=false
username: root #数据库用户,请根据实际填写
password: 123456 #数据库密码,请根据实际填写
#库存数据源(storage-ds自定义的)
storage-ds:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useUnicode=true&useSSL=false
username: root #数据库用户,请根据实际填写
password: 123456 #数据库密码,请根据实际填写
#订单数据源(ordere-ds自定义的)
ordere-ds:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useUnicode=true&useSSL=false
username: root #数据库用户,请根据实际填写
password: 123456 #数据库密码,请根据实际填写
seata: true #开启seata支持
seata-mode: at #选择seata模式:at、xa
#seata配置
seata:
application-id: myapp #应用的编号
tx-service-group: myapp-group #事务组名称
service:
vgroup-mapping:
myapp-group: guangzhou #当前事务组使用 guangzhou 机房的seata服务
group-list:
guangzhou: 127.0.0.1:8091 #主机房,部署seata tc server/seata tc cluster
shanghai: 127.0.0.1:8091 #备用机房,部署seata tc server/seata tc cluster
config:
type: file #配置文件使用文件存储方式
registry:
type: file #注册中心使用文件存储方式
事务分组与高可用,最佳实践1:TC的异地多机房容灾如下,更多最佳实践请访问:http://seata.io/zh-cn/docs/user/txgroup/transaction-group-and-ha.html
其中,projectA所有微服务的事务分组tx-transaction-group设置为:projectA,projectA正常情况下使用guangzhou的TC集群(主)
那么正常情况下,client端的配置如下所示:
seata.tx-service-group=projectA
seata.service.vgroup-mapping.projectA=Guangzhou
假如此时guangzhou集群分组整个down掉,或者因为网络原因projectA暂时无法与Guangzhou机房通讯,那么我们将配置中心中的Guangzhou集群分组改为Shanghai,如下:
seata.service.vgroup-mapping.projectA=Shanghai
并推送到各个微服务,便完成了对整个projectA项目的TC集群动态切换。
修改com.caochenlei.service.impl.OrderServiceImpl
开启全局事务管理,代码如下:
@DS("order-ds")
@Slf4j
@Service
@GlobalTransactional //开启seata全局事务注解,支持放在类上、方法上
public class OrderServiceImpl implements OrderService {
...
...
}
请重启当前工程,然后输入下单地址测试:http://localhost:9000/order/create?userId=1&productId=1&count=10&money=100&status=1
请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表
我们发现虽然下单失败了,但是并没有往订单数据库插入订单信息,账户余额和库存也没有减少,这符合我们的业务逻辑,多数据源的事务管理完美解决。
注意:测试完毕,请关闭当前工程,防止影响其他项目。
数据库环境为mysql 5.7.33
,请重新导入运行以下sql语句:
DROP DATABASE IF EXISTS `seata_account`;
CREATE DATABASE `seata_account`;
USE `seata_account`;
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT 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 '剩余额度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='账户表';
insert into `t_account`(`id`,`user_id`,`total`,`used`,`residue`) values (1,1,'1000','0','1000');
SELECT * FROM `seata_account`.`t_account` LIMIT 0, 1000;
数据库环境为mysql 5.7.33
,请重新导入运行以下sql语句:
DROP DATABASE IF EXISTS `seata_storage`;
CREATE DATABASE `seata_storage`;
USE `seata_storage`;
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`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 '剩余库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='库存表';
insert into `t_storage`(`id`,`product_id`,`total`,`used`,`residue`) values (1,1,100,0,100);
SELECT * FROM `seata_storage`.`t_storage` LIMIT 0, 1000;
数据库环境为mysql 5.7.33
,请重新导入运行以下sql语句:
DROP DATABASE `seata_order`;
CREATE DATABASE `seata_order`;
USE `seata_order`;
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`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 '状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单表';
SELECT * FROM `seata_order`.`t_order` LIMIT 0, 1000;
在桌面重新创建一个文件夹,名字无所谓,拷贝配套资料中的分布式\distributed-seata-at-study
到此文件夹,然后使用idea
打开即可。
distributed-seata-at-study
已经实现了基本的代码流程,非常简单,相信你一定能看懂:
distributed-seata-at-study
是一个典型的微服务应用,一共有三个服务,每个服务都实现了基本的代码逻辑,并且都对应一个数据源,架构如下图:
唯一需要你注意的是,打开application.yaml
,查看连接数据源的账户密码是否正确,并且我们需要你启动nacos
服务注册中心,如下:
service-account
service-storage
service-order
确保启动nacos
注册中心。
请启动当前工程,然后输入下单地址测试:http://localhost:9003/order/create?userId=1&productId=1&count=10&money=100&status=1
请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表
打开工程service-account
修改com.caochenlei.service.impl.AccountServiceImpl
代码添加异常,代码如下:
@Override
public void decrease(Long userId, BigDecimal money) {
int i = 1 / 0; //模拟异常出错
accountMapper.decrease(userId, money);
}
请重启当前工程,然后输入下单地址测试:http://localhost:9003/order/create?userId=1&productId=1&count=10&money=100&status=1
请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表
我们发现订单下单失败,订单数据库中多了一条订单记录,实际上这条记录不应该有,而且账户对应的余额和库存并没有减少,这个问题是十分可怕的。因为没有事务的支持,不能做到要不全部执行,要不全部失败。
在账户数据库
添加回滚日志表
,sql语句如下:
USE `seata_account`;
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
在库存数据库
添加回滚日志表
,sql语句如下:
USE `seata_storage`;
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
在订单数据库
添加回滚日志表
,sql语句如下:
USE `seata_order`;
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
最终完成以后的效果如下图:
在service-order
的pom.xml
中新增依赖:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
在service-order
的application.yaml
中新增配置:
server:
port: 9003
management:
endpoints:
web:
exposure:
include: '*'
spring:
application:
name: service-order
cloud:
nacos:
discovery:
server-addr: http://localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useUnicode=true&useSSL=false
username: root #数据库用户,请根据实际填写
password: 123456 #数据库密码,请根据实际填写
#防止feign调用超时对测试结果有影响
feign:
client:
config:
default:
connect-timeout: 5000
read-timeout: 5000
#seata配置
seata:
application-id: myapp #应用的编号
tx-service-group: myapp-group #事务组名称
service:
vgroup-mapping:
myapp-group: guangzhou #当前事务组使用 guangzhou 机房的seata服务
group-list:
guangzhou: 127.0.0.1:8091 #主机房,部署seata tc server/seata tc cluster
shanghai: 127.0.0.1:8091 #备用机房,部署seata tc server/seata tc cluster
config:
type: file #配置文件使用文件存储方式
registry:
type: file #注册中心使用文件存储方式
打开service-order
修改com.caochenlei.service.impl.OrderServiceImpl
开启全局事务管理,代码如下:
@Slf4j
@Service
@GlobalTransactional //开启seata全局事务注解,支持放在类上、方法上
public class OrderServiceImpl implements OrderService {
...
...
}
请重启当前工程,然后输入下单地址测试:http://localhost:9003/order/create?userId=1&productId=1&count=10&money=100&status=1
请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表
我们发现虽然下单失败了,但是并没有往订单数据库插入订单信息,账户余额和库存也没有减少,这符合我们的业务逻辑,分布式下的事务管理完美解决。
注意:测试完毕,请关闭当前工程,防止影响其他项目。
关闭单机版的命令行窗口。
首先初始化数据库:CREATE DATABASE seata; USE seata;
获取运行脚本地址:https://github.com/seata/seata/tree/develop/script/server/db
-- -------------------------------- 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;
找到seata\conf\file.conf
,把存储模式修改为db
并修改数据源连接,修改后保存,如下:
## transaction log store, only used in seata-server
store {
## store mode: file、db、redis
mode = "db"
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
maxBranchSessionSize = 16384
# globe session size , if exceeded throws exceptions
maxGlobalSessionSize = 512
# file buffer size , if exceeded allocate new buffer
fileWriteBufferCacheSize = 16384
# when recover batch read size
sessionReloadReadSize = 100
# async, sync
flushDiskMode = async
}
## 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://127.0.0.1:3306/seata?characterEncoding=utf8&useUnicode=true&useSSL=false"
user = "root"
password = "123456"
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
## redis store property
redis {
host = "127.0.0.1"
port = "6379"
password = ""
database = "0"
minConn = 1
maxConn = 10
queryLimit = 100
}
}
找到seata\conf\registry.conf
,把注册中心修改为nacos
并修改登录配置,修改后保存,如下:
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = "public"
cluster = "default"
username = "nacos"
password = "nacos"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = 0
password = ""
cluster = "default"
timeout = 0
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
...
...
...
...
启动第一个实例:C:\DevTools\seata\bin>seata-server.bat -p 18901 -n 1
启动第二个实例:C:\DevTools\seata\bin>seata-server.bat -p 28901 -n 2
打开注册中心:http://localhost:8848/nacos/,登录账户:nacos,登录密码:nacos
回顾总览中的描述:一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 TCC (Branch) Transaction Mode.
AT 模式(参考链接 TBD)基于 支持本地 ACID 事务 的 关系型数据库:
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
我们其实可以理解,TCC模式所有阶段的代码都是自己实现的,所以它能够更加灵活的回滚各种关系型数据库、非关系型数据库、消息中间件等。
数据库环境为mysql 5.7.33
,请重新导入运行以下sql语句:
DROP DATABASE IF EXISTS `seata_account`;
CREATE DATABASE `seata_account`;
USE `seata_account`;
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT 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 '剩余额度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='账户表';
insert into `t_account`(`id`,`user_id`,`total`,`used`,`residue`) values (1,1,'1000','0','1000');
SELECT * FROM `seata_account`.`t_account` LIMIT 0, 1000;
数据库环境为mysql 5.7.33
,请重新导入运行以下sql语句:
DROP DATABASE IF EXISTS `seata_storage`;
CREATE DATABASE `seata_storage`;
USE `seata_storage`;
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`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 '剩余库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='库存表';
insert into `t_storage`(`id`,`product_id`,`total`,`used`,`residue`) values (1,1,100,0,100);
SELECT * FROM `seata_storage`.`t_storage` LIMIT 0, 1000;
数据库环境为mysql 5.7.33
,请重新导入运行以下sql语句:
DROP DATABASE `seata_order`;
CREATE DATABASE `seata_order`;
USE `seata_order`;
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`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 '状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单表';
SELECT * FROM `seata_order`.`t_order` LIMIT 0, 1000;
在桌面重新创建一个文件夹,名字无所谓,拷贝配套资料中的单体版\single-app-seata-tcc-study
到此文件夹,然后使用idea
打开即可。
single-app-seata-tcc-study
已经实现了基本的代码流程,非常简单,相信你一定能看懂:
single-app-seata-tcc-study
是一个纯粹的Spring Boot
单体应用,连接着三个数据源:
唯一需要你注意的是,打开application.yaml
,查看连接数据源的账户密码是否正确,如下:
请启动当前工程,然后输入下单地址测试:http://localhost:9000/order/create?userId=1&productId=1&count=10&money=100&status=1
请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表
修改com.caochenlei.service.impl.OrderServiceImpl
代码添加异常,代码如下:
@DS("order-ds")
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private AccountService accountService;
@Resource
private StorageService storageService;
@Override
public void create(Order order) {
// 1、新建订单
log.info("----->新建订单开始");
orderMapper.create(order);
log.info("----->新建订单结束");
// 2、扣减余额
log.info("----->扣减余额开始");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("----->扣减余额结束");
int i = 1 / 0; //模拟异常出错
// 3、扣减库存
log.info("----->扣减库存开始");
storageService.decrease(order.getProductId(), order.getCount());
log.info("----->扣减库存结束");
}
}
请重启当前工程,然后输入下单地址测试:http://localhost:9000/order/create?userId=1&productId=1&count=10&money=100&status=1
请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表
我们发现订单下单失败,订单数据库中多了一条订单记录,实际上这条记录不应该有,同时,账户的余额也减少了,但是,库存并没有减少,这个问题是十分可怕的。因为没有事务的支持,不能做到要不全部执行,要不全部失败。
在账户数据库
添加回滚日志表
,sql语句如下:
USE `seata_account`;
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
在库存数据库
添加回滚日志表
,sql语句如下:
USE `seata_storage`;
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
在订单数据库
添加回滚日志表
,sql语句如下:
USE `seata_order`;
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
最终完成以后的效果如下图:
在pom.xml
中新增依赖:
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-spring-boot-starterartifactId>
<version>1.3.0version>
dependency>
<dependency>
<groupId>com.alibaba.nacosgroupId>
<artifactId>nacos-clientartifactId>
<version>1.3.2version>
dependency>
在application.yaml
中新增配置:
server:
port: 9000
management:
endpoints:
web:
exposure:
include: '*'
spring:
application:
name: single-app-seata-tcc-study
datasource:
dynamic:
primary: ordere-ds
datasource:
#账户数据源(account-ds自定义的)
account-ds:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useUnicode=true&useSSL=false
username: root #数据库用户,请根据实际填写
password: 123456 #数据库密码,请根据实际填写
#库存数据源(storage-ds自定义的)
storage-ds:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useUnicode=true&useSSL=false
username: root #数据库用户,请根据实际填写
password: 123456 #数据库密码,请根据实际填写
#订单数据源(ordere-ds自定义的)
ordere-ds:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useUnicode=true&useSSL=false
username: root #数据库用户,请根据实际填写
password: 123456 #数据库密码,请根据实际填写
seata: true #开启seata支持
seata-mode: at #选择seata模式:at、xa,不支持tcc,我们需要手动编码实现tcc第二阶段
#seata配置
seata:
application-id: myapp #应用的编号
tx-service-group: myapp-group #事务组名称
service:
vgroup-mapping:
myapp-group: default #当前事务组使用 cluster: default
config:
type: file #配置文件使用文件存储方式
registry:
type: nacos #注册中心使用nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace: public
cluster: default
username: nacos
password: nacos
修改com.caochenlei.service.impl.OrderServiceImpl
开启全局事务管理,代码如下:
@DS("order-ds")
@Slf4j
@Service
@GlobalTransactional //开启seata全局事务注解,支持放在类上、方法上
public class OrderServiceImpl implements OrderService {
...
...
}
修改接口:com.caochenlei.service.AccountService
@LocalTCC//此注解标识TCC为本地模式,即该事务是本地调用
public interface AccountService {
//第一阶段:尝试扣减余额
@TwoPhaseBusinessAction(name = "decreaseMoney", commitMethod = "commitDecreaseMoney", rollbackMethod = "rollbackDecreaseMoney")
void decrease(@BusinessActionContextParameter(paramName = "userId") Long userId,
@BusinessActionContextParameter(paramName = "money") BigDecimal money);
//第二阶段:提交处理方法
boolean commitDecreaseMoney(BusinessActionContext context);
//第二阶段:回滚处理方法
boolean rollbackDecreaseMoney(BusinessActionContext context);
}
修改实现:com.caochenlei.service.impl.AccountServiceImpl
@DS("account-ds")
@Service
public class AccountServiceImpl implements AccountService {
@Resource
private AccountMapper accountMapper;
@Override
public void decrease(Long userId, BigDecimal money) {
accountMapper.decrease(userId, money);
}
@Override
public boolean commitDecreaseMoney(BusinessActionContext context) {
return true;//可以直接返回true,即空确认
}
@Override
public boolean rollbackDecreaseMoney(BusinessActionContext context) {
//TODO 这里可以实现中间件、非关系型数据库的回滚操作
//通过业务动作上下文获取指定参数的参数值
String userId = context.getActionContext("userId").toString();
String money = context.getActionContext("money").toString();
//手动进行数据库回滚,把减去的余额加回去
accountMapper.increase(new Long(userId), new BigDecimal(money));
//我们手动输出一句话,代表回滚使用我们的
System.out.println("数据回滚了,这可真的好");
return true;
}
}
修改映射:com.caochenlei.mapper.AccountMapper
@Mapper
public interface AccountMapper {
//扣减余额
@Update("update t_account set used=used+#{money},residue=residue-#{money} where user_id=#{userId};")
int decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
//加回余额
@Update("update t_account set used=used-#{money},residue=residue+#{money} where user_id=#{userId};")
int increase(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
请重启当前工程,然后输入下单地址测试:http://localhost:9000/order/create?userId=1&productId=1&count=10&money=100&status=1
请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表
我们发现虽然下单失败了,但是并没有往订单数据库插入订单信息,账户余额和库存也没有减少,这符合我们的业务逻辑,可以通过这种方式实现中间件、非关系型数据库的回滚操作。
而账户余额的回滚操作则是使用的是TCC模式下,我们自定义的第二阶段回滚方法。
注意:测试完毕,请关闭当前工程,防止影响其他项目。正常情况下,每一个服务都需要配置seata,我这里偷懒了,大家要注意一下!
数据库环境为mysql 5.7.33
,请重新导入运行以下sql语句:
DROP DATABASE IF EXISTS `seata_account`;
CREATE DATABASE `seata_account`;
USE `seata_account`;
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT 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 '剩余额度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='账户表';
insert into `t_account`(`id`,`user_id`,`total`,`used`,`residue`) values (1,1,'1000','0','1000');
SELECT * FROM `seata_account`.`t_account` LIMIT 0, 1000;
数据库环境为mysql 5.7.33
,请重新导入运行以下sql语句:
DROP DATABASE IF EXISTS `seata_storage`;
CREATE DATABASE `seata_storage`;
USE `seata_storage`;
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`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 '剩余库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='库存表';
insert into `t_storage`(`id`,`product_id`,`total`,`used`,`residue`) values (1,1,100,0,100);
SELECT * FROM `seata_storage`.`t_storage` LIMIT 0, 1000;
数据库环境为mysql 5.7.33
,请重新导入运行以下sql语句:
DROP DATABASE `seata_order`;
CREATE DATABASE `seata_order`;
USE `seata_order`;
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`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 '状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单表';
SELECT * FROM `seata_order`.`t_order` LIMIT 0, 1000;
在桌面重新创建一个文件夹,名字无所谓,拷贝配套资料中的分布式\distributed-seata-tcc-study
到此文件夹,然后使用idea
打开即可。
distributed-seata-tcc-study
已经实现了基本的代码流程,非常简单,相信你一定能看懂:
distributed-seata-tcc-study
是一个典型的微服务应用,一共有三个服务,每个服务都实现了基本的代码逻辑,并且都对应一个数据源,架构如下图:
唯一需要你注意的是,打开application.yaml
,查看连接数据源的账户密码是否正确,并且我们需要你启动nacos
服务注册中心,如下:
service-account
service-storage
service-order
确保启动nacos
注册中心。
请启动当前工程,然后输入下单地址测试:http://localhost:9003/order/create?userId=1&productId=1&count=10&money=100&status=1
请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表
打开工程service-order
修改com.caochenlei.service.impl.OrderServiceImpl
代码添加异常,代码如下:
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Resource
private AccountService accountService;
@Resource
private StorageService storageService;
@Override
public void create(Order order) {
// 1、新建订单
log.info("----->新建订单开始");
orderMapper.create(order);
log.info("----->新建订单结束");
// 2、扣减余额
log.info("----->扣减余额开始");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("----->扣减余额结束");
int i = 1 / 0; //模拟异常出错
// 3、扣减库存
log.info("----->扣减库存开始");
storageService.decrease(order.getProductId(), order.getCount());
log.info("----->扣减库存结束");
}
}
请重启当前工程,然后输入下单地址测试:http://localhost:9003/order/create?userId=1&productId=1&count=10&money=100&status=1
请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表
我们发现订单下单失败,订单数据库中多了一条订单记录,实际上这条记录不应该有,同时,账户的余额也减少了,但是,库存并没有减少,这个问题是十分可怕的。因为没有事务的支持,不能做到要不全部执行,要不全部失败。
在账户数据库
添加回滚日志表
,sql语句如下:
USE `seata_account`;
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
在库存数据库
添加回滚日志表
,sql语句如下:
USE `seata_storage`;
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
在订单数据库
添加回滚日志表
,sql语句如下:
USE `seata_order`;
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
最终完成以后的效果如下图:
在service-order
、service-account
的pom.xml
中新增依赖:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
在service-order
、service-account
的application.yaml
中新增配置:
...
...
#seata配置
seata:
application-id: myapp #应用的编号
tx-service-group: myapp-group #事务组名称
service:
vgroup-mapping:
myapp-group: default #当前事务组使用 cluster: default
config:
type: file #配置文件使用文件存储方式
registry:
type: nacos #注册中心使用nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace: public
cluster: default
username: nacos
password: nacos
打开service-order
修改com.caochenlei.service.impl.OrderServiceImpl
开启全局事务管理,代码如下:
@Slf4j
@Service
@GlobalTransactional //开启seata全局事务注解,支持放在类上、方法上
public class OrderServiceImpl implements OrderService {
...
...
}
打开service-account
修改接口:com.caochenlei.service.AccountService
@LocalTCC//此注解标识TCC为本地模式,即该事务是本地调用
public interface AccountService {
//第一阶段:尝试扣减余额
@TwoPhaseBusinessAction(name = "decreaseMoney", commitMethod = "commitDecreaseMoney", rollbackMethod = "rollbackDecreaseMoney")
void decrease(@BusinessActionContextParameter(paramName = "userId") Long userId,
@BusinessActionContextParameter(paramName = "money") BigDecimal money);
//第二阶段:提交处理方法
boolean commitDecreaseMoney(BusinessActionContext context);
//第二阶段:回滚处理方法
boolean rollbackDecreaseMoney(BusinessActionContext context);
}
打开service-account
修改实现:com.caochenlei.service.impl.AccountServiceImpl
@Service
public class AccountServiceImpl implements AccountService {
@Resource
private AccountMapper accountMapper;
@Override
public void decrease(Long userId, BigDecimal money) {
accountMapper.decrease(userId, money);
}
@Override
public boolean commitDecreaseMoney(BusinessActionContext context) {
return true;//可以直接返回true,即空确认
}
@Override
public boolean rollbackDecreaseMoney(BusinessActionContext context) {
//TODO 这里可以实现中间件、非关系型数据库的回滚操作
//通过业务动作上下文获取指定参数的参数值
String userId = context.getActionContext("userId").toString();
String money = context.getActionContext("money").toString();
//手动进行数据库回滚,把减去的余额加回去
accountMapper.increase(new Long(userId), new BigDecimal(money));
//我们手动输出一句话,代表回滚使用我们的
System.out.println("数据回滚了,这可真的好");
return true;
}
}
打开service-account
修改映射:com.caochenlei.mapper.AccountMapper
@Mapper
public interface AccountMapper {
//扣减余额
@Update("update t_account set used=used+#{money},residue=residue-#{money} where user_id=#{userId};")
int decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
//加回余额
@Update("update t_account set used=used-#{money},residue=residue+#{money} where user_id=#{userId};")
int increase(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
请重启service-order
、service-account
工程
然后输入下单地址测试:http://localhost:9003/order/create?userId=1&productId=1&count=10&money=100&status=1
请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表
我们发现虽然下单失败了,但是并没有往订单数据库插入订单信息,账户余额和库存也没有减少,这符合我们的业务逻辑,分布式下的事务管理完美解决。
而账户余额的回滚操作则是使用的是TCC模式下,我们自定义的第二阶段回滚方法。
注意:测试完毕,请关闭当前工程,防止影响其他项目。正常情况下,每一个服务都需要配置seata,我这里偷懒了,大家要注意一下!