分布式事务管理:Seata

目录

  • 第一章 Seata的介绍
    • 1.1、分布式事务
    • 1.2、Seata是什么
    • 1.3、Seata的术语
  • 第二章 Seata单机版部署:TC Server
    • 2.1、下载Seata
    • 2.2、解压Seata
    • 2.3、运行Seata
  • 第三章 Seata的常用模式:AT
    • 3.1、使用前提
    • 3.2、整体机制
    • 3.3、读写隔离
      • 3.3.1、写隔离
      • 3.3.2、读隔离
    • 3.4、工作机制
      • 3.4.1、一阶段
      • 3.4.2、二阶段-回滚
      • 3.4.3、二阶段-提交
    • 3.5、附录章节
  • 第四章 单体版多数据源事务管理:AT
    • 4.1、导入数据
      • 4.1.1、创建账户数据库
      • 4.1.2、创建库存数据库
      • 4.1.3、创建订单数据库
    • 4.2、导入工程
    • 4.3、正常测试
    • 4.4、异常测试
    • 4.5、添加新表
    • 4.6、添加依赖
    • 4.7、添加配置
    • 4.8、添加注解
    • 4.9、异常测试
  • 第五章 分布式单数据源事务管理:AT
    • 5.1、导入数据
      • 5.1.1、重置账户数据库
      • 5.1.2、重置库存数据库
      • 5.1.3、重置订单数据库
    • 5.2、导入工程
    • 5.3、正常测试
    • 5.4、异常测试
    • 5.5、添加新表
    • 5.6、添加依赖
    • 5.7、添加配置
    • 5.8、添加注解
    • 5.9、异常测试
  • 第六章 Seata集群版部署:TC Cluster
    • 6.1、关闭单机版
    • 6.2、创建数据库
    • 6.3、修改存储模式
    • 6.4、修改注册中心
    • 6.5、启动两个实例
    • 6.6、查看注册中心
  • 第七章 Seata的常用模式:TCC
  • 第八章 单体版多数据源事务管理:TCC
    • 8.1、导入数据
      • 8.1.1、重置账户数据库
      • 8.1.2、重置库存数据库
      • 8.1.3、重置订单数据库
    • 8.2、导入工程
    • 8.3、正常测试
    • 8.4、异常测试
    • 8.5、添加新表
    • 8.6、添加依赖
    • 8.7、添加配置
    • 8.8、添加注解
    • 8.9、账户服务
    • 8.10、异常测试
  • 第九章 分布式单数据源事务管理:TCC
    • 9.1、导入数据
      • 9.1.1、重置账户数据库
      • 9.1.2、重置库存数据库
      • 9.1.3、重置订单数据库
    • 9.2、导入工程
    • 9.3、正常测试
    • 9.4、异常测试
    • 9.5、添加新表
    • 9.6、添加依赖
    • 9.7、添加配置
    • 9.8、添加注解
    • 9.9、账户服务
    • 9.10、异常测试


配套资料,免费下载
链接:https://pan.baidu.com/s/1-eRFozbFIShqbqNRFD9KDw
提取码:rt3w
复制这段内容后打开百度网盘手机App,操作更方便哦

第一章 Seata的介绍

1.1、分布式事务

事务是数据库的概念,数据库事务(ACID:原子性、一致性、隔离性和持久性)。

分布式事务的产生,是由于数据库的拆分和分布式架构(微服务)带来的,在常规情况下,我们在一个进程中操作一个数据库,这属于本地事务,如果在一个进程中操作多个数据库,或者在多个进程中操作一个或多个数据库,就产生了分布式事务;

(1)数据库分库分表就产生了分布式事务;

分布式事务管理:Seata_第1张图片

(2)项目拆分服务化也产生了分布式事务;

分布式事务管理:Seata_第2张图片

1.2、Seata是什么

Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。

Seata为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的分布式解决方案。

四种事务模式中,目前使用的流行度情况是:AT > TCC > Saga、XA。我们可以参看Seata各公司使用列表:https://github.com/seata/seata/issues/1246

因此,我们教学的重点和学习的重点将会放到AT模式和TCC模式的讲解上,Seata默认就是AT模式,简单的一句话来说这两种模式的区别(后边会深入讲解):

  • AT模式:可以对数据源是mysql、oracle等关系型数据库的情况进行失败回滚。
  • TCC模式:不仅可以对数据源是mysql、oracle等关系型数据库的情况进行失败回滚。还可以对消息中间件、非关系型数据库如:redis、mongodb等数据库进行失败回滚。

从上边直观的来看,感觉TCC模式更厉害一点,实际上,Seata默认的AT模式,事务失败回滚并不用程序员自己来做,而是由Seata框架本身来完成的,而TCC模式的事务失败回滚等操作,全部需要手动实现,因此,AT模式在实际生产环境中用的更多一点,也更方便一点,除了特定场景下的特殊需要,AT模式基本都能满足。

当然了,这里提前说一下,我们接下来会学习Seata的单机版部署和高可用集群版部署,而正好,我们要学习AT和TCC两种模式,我们在学习AT模式的时候,使用Seata单机版环境、而在学习TCC模式的时候,使用Seata的高可用集群版的环境,一定注意,这么安排存粹是为了教学方便,实际上AT也能用高可用集群版环境。

1.3、Seata的术语

官方文档:http://seata.io/zh-cn/

在Seata的架构中,一共有三个角色:

分布式事务管理:Seata_第3张图片

TC (Transaction Coordinator) - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM (Transaction Manager) - 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM (Resource Manager) - 资源管理器

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

其中TC为单独部署的 Server 服务端,TM和RM为嵌入到应用中的 Client 客户端,除了以上三种角色外,还有一个全局事务id:Transaction ID XID

在Seata中,一个分布式事务的生命周期如下:

分布式事务管理:Seata_第4张图片

  1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID;

  2. XID 在微服务调用链路的上下文中传播;

  3. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖;

  4. TM 向 TC 发起针对 XID 的全局提交或回滚决议;

  5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。

第二章 Seata单机版部署:TC Server

2.1、下载Seata

我们先部署单机环境的 Seata TC Server,用于学习或测试,在生产环境中要部署集群环境;

因为TC需要进行全局事务和分支事务的记录,所以需要对应的存储,目前,TC有三种存储模式( store.mode ):

  • file模式:适合单机模式,全局事务会话信息在内存中读写,并持久化本地文件 root.data,性能较高;( 默认
  • db模式:适合集群模式,全局事务会话信息通过 db 共享,相对性能差点;
  • redis模式:解决db存储的性能问题;

我们先采用file模式,最终我们部署单机TC Server如下图所示:

分布式事务管理:Seata_第5张图片

截止到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

2.2、解压Seata

分布式事务管理:Seata_第6张图片

2.3、运行Seata

双击运行:bin\seata-server.bat

第三章 Seata的常用模式:AT

3.1、使用前提

  • 基于支持本地 ACID 事务的关系型数据库。例如:mysql、oracle
  • Java 应用,通过 JDBC 访问数据库。

3.2、整体机制

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录(一张单独的数据表)在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。

反向补偿:简单说就是给某一个字段加了10,反向补偿就减去10,这样数据保持不变。新增一条记录,反向补偿就删除以前新增的那条记录。

3.3、读写隔离

3.3.1、写隔离

  • 一阶段本地事务提交前,需要确保先拿到 全局锁
  • 拿不到 全局锁 ,不能提交本地事务。
  • 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

以一个示例来说明:

两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。

tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁

分布式事务管理:Seata_第7张图片

tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。

分布式事务管理:Seata_第8张图片

如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。

此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。

因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。

3.3.2、读隔离

如果不考虑事务的隔离性,可能会引发读安全性问题:

  • 脏读:一个事务读到了另一个事务未提交的数据
  • 不可重复读:一个事务读到了另一个事务已经提交的 update 的数据,导致多次查询结果不一致
  • 幻读 / 虚读:一个事务读到了另一个事务已经提交的 insert 的数据,导致多次查询结果不一致
隔离级别 中文说明 说明
READ UNCOMMITTED 读未提交 不能解决以上所有读问题,效率最高,安全性最低,一般不用
READ COMMITTED 读已提交 避免脏读,不可重复读和幻读有可能发生,Oracle默认的隔离级别
REPEATABLE READ 可重复读 避免脏读、不可重复读,幻读有可能发生,MySQL默认的隔离级别
SERIALIZABLE 串行化 可以解决以上所有读问题,效率最差,安全性最高,一般不用

在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted)

如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。

分布式事务管理:Seata_第9张图片

SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。

出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。

3.4、工作机制

以一个示例来说明整个 AT 分支的工作过程。

业务表:product

Field Type Key
id bigint(20) PRI
name varchar(100)
since varchar(100)

AT 分支事务的业务逻辑:

update product set name = 'GTS' where name = 'TXC';

3.4.1、一阶段

过程:

  • 1、解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息。
  • 2、查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
select id, name, since from product where name = 'TXC';

得到前镜像:

id name since
1 TXC 2014
  • 3、执行业务 SQL:更新这条记录的 name 为 ‘GTS’。
  • 4、查询后镜像:根据前镜像的结果,通过 主键 定位数据。
select id, name, since from product where id = 1`;

得到后镜像:

id name since
1 GTS 2014
  • 5、插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 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"
}
  • 6、提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁
  • 7、本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
  • 8、将本地事务提交的结果上报给 TC。

3.4.2、二阶段-回滚

  • 1、收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
  • 2、通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
  • 3、数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
  • 4、根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
update product set name = 'TXC' where id = 1;
  • 5、提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。

3.4.3、二阶段-提交

  • 1、收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
  • 2、异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。

3.5、附录章节

回滚日志表

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;

第四章 单体版多数据源事务管理:AT

4.1、导入数据

4.1.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; 

4.1.2、创建库存数据库

数据库环境为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; 

4.1.3、创建订单数据库

数据库环境为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; 

4.2、导入工程

在桌面重新创建一个文件夹,名字无所谓,拷贝配套资料中的单体版\single-app-seata-at-study到此文件夹,然后使用idea打开即可。

分布式事务管理:Seata_第10张图片

single-app-seata-at-study已经实现了基本的代码流程,非常简单,相信你一定能看懂:

分布式事务管理:Seata_第11张图片

single-app-seata-at-study是一个纯粹的Spring Boot单体应用,连接着三个数据源:

分布式事务管理:Seata_第12张图片

唯一需要你注意的是,打开application.yaml,查看连接数据源的账户密码是否正确,如下:

分布式事务管理:Seata_第13张图片

4.3、正常测试

请启动当前工程,然后输入下单地址测试:http://localhost:9000/order/create?userId=1&productId=1&count=10&money=100&status=1

请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表

4.4、异常测试

修改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

分布式事务管理:Seata_第14张图片

请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表

我们发现订单下单失败,订单数据库中多了一条订单记录,实际上这条记录不应该有,而且账户对应的余额和库存并没有减少,这个问题是十分可怕的。因为没有事务的支持,不能做到要不全部执行,要不全部失败。

4.5、添加新表

账户数据库添加回滚日志表,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;

最终完成以后的效果如下图:

分布式事务管理:Seata_第15张图片

4.6、添加依赖

pom.xml中新增依赖:

<dependency>
    <groupId>io.seatagroupId>
    <artifactId>seata-spring-boot-starterartifactId>
    <version>1.3.0version>
dependency>

4.7、添加配置

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

  • 假定TC集群部署在两个机房:guangzhou机房(主)和shanghai机房(备)各两个实例
  • 一整套微服务架构项目:projectA
  • projectA内有微服务:serviceA、serviceB、serviceC 和 serviceD

其中,projectA所有微服务的事务分组tx-transaction-group设置为:projectA,projectA正常情况下使用guangzhou的TC集群(主)

那么正常情况下,client端的配置如下所示:

seata.tx-service-group=projectA
seata.service.vgroup-mapping.projectA=Guangzhou

分布式事务管理:Seata_第16张图片

假如此时guangzhou集群分组整个down掉,或者因为网络原因projectA暂时无法与Guangzhou机房通讯,那么我们将配置中心中的Guangzhou集群分组改为Shanghai,如下:

seata.service.vgroup-mapping.projectA=Shanghai

并推送到各个微服务,便完成了对整个projectA项目的TC集群动态切换。

分布式事务管理:Seata_第17张图片

4.8、添加注解

修改com.caochenlei.service.impl.OrderServiceImpl开启全局事务管理,代码如下:

@DS("order-ds")
@Slf4j
@Service
@GlobalTransactional //开启seata全局事务注解,支持放在类上、方法上
public class OrderServiceImpl implements OrderService {
     
    ...
    ...
}

4.9、异常测试

请重启当前工程,然后输入下单地址测试:http://localhost:9000/order/create?userId=1&productId=1&count=10&money=100&status=1

分布式事务管理:Seata_第18张图片

请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表

我们发现虽然下单失败了,但是并没有往订单数据库插入订单信息,账户余额和库存也没有减少,这符合我们的业务逻辑,多数据源的事务管理完美解决。

注意:测试完毕,请关闭当前工程,防止影响其他项目。

第五章 分布式单数据源事务管理:AT

5.1、导入数据

5.1.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; 

5.1.2、重置库存数据库

数据库环境为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; 

5.1.3、重置订单数据库

数据库环境为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; 

5.2、导入工程

在桌面重新创建一个文件夹,名字无所谓,拷贝配套资料中的分布式\distributed-seata-at-study到此文件夹,然后使用idea打开即可。

分布式事务管理:Seata_第19张图片

distributed-seata-at-study已经实现了基本的代码流程,非常简单,相信你一定能看懂:

分布式事务管理:Seata_第20张图片

distributed-seata-at-study是一个典型的微服务应用,一共有三个服务,每个服务都实现了基本的代码逻辑,并且都对应一个数据源,架构如下图:

分布式事务管理:Seata_第21张图片

唯一需要你注意的是,打开application.yaml,查看连接数据源的账户密码是否正确,并且我们需要你启动nacos服务注册中心,如下:

service-account

分布式事务管理:Seata_第22张图片

service-storage

分布式事务管理:Seata_第23张图片

service-order

分布式事务管理:Seata_第24张图片

5.3、正常测试

确保启动nacos注册中心。

请启动当前工程,然后输入下单地址测试:http://localhost:9003/order/create?userId=1&productId=1&count=10&money=100&status=1

请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表

5.4、异常测试

打开工程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

分布式事务管理:Seata_第25张图片

请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表

我们发现订单下单失败,订单数据库中多了一条订单记录,实际上这条记录不应该有,而且账户对应的余额和库存并没有减少,这个问题是十分可怕的。因为没有事务的支持,不能做到要不全部执行,要不全部失败。

5.5、添加新表

账户数据库添加回滚日志表,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;

最终完成以后的效果如下图:

分布式事务管理:Seata_第26张图片

5.6、添加依赖

service-orderpom.xml中新增依赖:

<dependency>
    <groupId>com.alibaba.cloudgroupId>
    <artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>

5.7、添加配置

service-orderapplication.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 #注册中心使用文件存储方式

5.8、添加注解

打开service-order修改com.caochenlei.service.impl.OrderServiceImpl开启全局事务管理,代码如下:

@Slf4j
@Service
@GlobalTransactional //开启seata全局事务注解,支持放在类上、方法上
public class OrderServiceImpl implements OrderService {
     
    ...
    ...
}

5.9、异常测试

请重启当前工程,然后输入下单地址测试:http://localhost:9003/order/create?userId=1&productId=1&count=10&money=100&status=1

分布式事务管理:Seata_第27张图片

请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表

我们发现虽然下单失败了,但是并没有往订单数据库插入订单信息,账户余额和库存也没有减少,这符合我们的业务逻辑,分布式下的事务管理完美解决。

注意:测试完毕,请关闭当前工程,防止影响其他项目。

第六章 Seata集群版部署:TC Cluster

6.1、关闭单机版

关闭单机版的命令行窗口。

6.2、创建数据库

首先初始化数据库: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;

6.3、修改存储模式

找到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
  }
}

6.4、修改注册中心

找到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"
  }
}
...
...
...
...

6.5、启动两个实例

启动第一个实例:C:\DevTools\seata\bin>seata-server.bat -p 18901 -n 1

启动第二个实例:C:\DevTools\seata\bin>seata-server.bat -p 28901 -n 2

  • -p:Seata TC Server 监听的端口;
  • -n:Server node,在多个 TC Server 时,需区分各自节点,用于生成不同区间的 transactionId 事务编号,以免冲突;

6.6、查看注册中心

打开注册中心:http://localhost:8848/nacos/,登录账户:nacos,登录密码:nacos

分布式事务管理:Seata_第28张图片

第七章 Seata的常用模式:TCC

回顾总览中的描述:一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:

  • 一阶段 prepare 行为
  • 二阶段 commit 或 rollback 行为

分布式事务管理:Seata_第29张图片

根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction ModeTCC (Branch) Transaction Mode.

AT 模式(参考链接 TBD)基于 支持本地 ACID 事务关系型数据库

  • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
  • 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
  • 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。

相应的,TCC 模式,不依赖于底层数据资源的事务支持:

  • 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
  • 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
  • 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。

所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。

我们其实可以理解,TCC模式所有阶段的代码都是自己实现的,所以它能够更加灵活的回滚各种关系型数据库、非关系型数据库、消息中间件等。

第八章 单体版多数据源事务管理:TCC

8.1、导入数据

8.1.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; 

8.1.2、重置库存数据库

数据库环境为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; 

8.1.3、重置订单数据库

数据库环境为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; 

8.2、导入工程

在桌面重新创建一个文件夹,名字无所谓,拷贝配套资料中的单体版\single-app-seata-tcc-study到此文件夹,然后使用idea打开即可。

分布式事务管理:Seata_第30张图片

single-app-seata-tcc-study已经实现了基本的代码流程,非常简单,相信你一定能看懂:

分布式事务管理:Seata_第31张图片

single-app-seata-tcc-study是一个纯粹的Spring Boot单体应用,连接着三个数据源:

分布式事务管理:Seata_第32张图片

唯一需要你注意的是,打开application.yaml,查看连接数据源的账户密码是否正确,如下:

分布式事务管理:Seata_第33张图片

8.3、正常测试

请启动当前工程,然后输入下单地址测试:http://localhost:9000/order/create?userId=1&productId=1&count=10&money=100&status=1

请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表

8.4、异常测试

修改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

分布式事务管理:Seata_第34张图片

请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表

我们发现订单下单失败,订单数据库中多了一条订单记录,实际上这条记录不应该有,同时,账户的余额也减少了,但是,库存并没有减少,这个问题是十分可怕的。因为没有事务的支持,不能做到要不全部执行,要不全部失败。

8.5、添加新表

账户数据库添加回滚日志表,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;

最终完成以后的效果如下图:

分布式事务管理:Seata_第35张图片

8.6、添加依赖

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>

8.7、添加配置

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

8.8、添加注解

修改com.caochenlei.service.impl.OrderServiceImpl开启全局事务管理,代码如下:

@DS("order-ds")
@Slf4j
@Service
@GlobalTransactional //开启seata全局事务注解,支持放在类上、方法上
public class OrderServiceImpl implements OrderService {
     
    ...
    ...
}

8.9、账户服务

修改接口: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);
}

8.10、异常测试

请重启当前工程,然后输入下单地址测试:http://localhost:9000/order/create?userId=1&productId=1&count=10&money=100&status=1

分布式事务管理:Seata_第36张图片

请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表

我们发现虽然下单失败了,但是并没有往订单数据库插入订单信息,账户余额和库存也没有减少,这符合我们的业务逻辑,可以通过这种方式实现中间件、非关系型数据库的回滚操作。

分布式事务管理:Seata_第37张图片

而账户余额的回滚操作则是使用的是TCC模式下,我们自定义的第二阶段回滚方法。

注意:测试完毕,请关闭当前工程,防止影响其他项目。正常情况下,每一个服务都需要配置seata,我这里偷懒了,大家要注意一下!

第九章 分布式单数据源事务管理:TCC

9.1、导入数据

9.1.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; 

9.1.2、重置库存数据库

数据库环境为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; 

9.1.3、重置订单数据库

数据库环境为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; 

9.2、导入工程

在桌面重新创建一个文件夹,名字无所谓,拷贝配套资料中的分布式\distributed-seata-tcc-study到此文件夹,然后使用idea打开即可。

分布式事务管理:Seata_第38张图片

distributed-seata-tcc-study已经实现了基本的代码流程,非常简单,相信你一定能看懂:

分布式事务管理:Seata_第39张图片

distributed-seata-tcc-study是一个典型的微服务应用,一共有三个服务,每个服务都实现了基本的代码逻辑,并且都对应一个数据源,架构如下图:

分布式事务管理:Seata_第40张图片

唯一需要你注意的是,打开application.yaml,查看连接数据源的账户密码是否正确,并且我们需要你启动nacos服务注册中心,如下:

service-account

分布式事务管理:Seata_第41张图片

service-storage

分布式事务管理:Seata_第42张图片

service-order

分布式事务管理:Seata_第43张图片

9.3、正常测试

确保启动nacos注册中心。

请启动当前工程,然后输入下单地址测试:http://localhost:9003/order/create?userId=1&productId=1&count=10&money=100&status=1

请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表

9.4、异常测试

打开工程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

分布式事务管理:Seata_第44张图片

请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表

我们发现订单下单失败,订单数据库中多了一条订单记录,实际上这条记录不应该有,同时,账户的余额也减少了,但是,库存并没有减少,这个问题是十分可怕的。因为没有事务的支持,不能做到要不全部执行,要不全部失败。

9.5、添加新表

账户数据库添加回滚日志表,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;

最终完成以后的效果如下图:

分布式事务管理:Seata_第45张图片

9.6、添加依赖

service-orderservice-accountpom.xml中新增依赖:

<dependency>
    <groupId>com.alibaba.cloudgroupId>
    <artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>

9.7、添加配置

service-orderservice-accountapplication.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

9.8、添加注解

打开service-order修改com.caochenlei.service.impl.OrderServiceImpl开启全局事务管理,代码如下:

@Slf4j
@Service
@GlobalTransactional //开启seata全局事务注解,支持放在类上、方法上
public class OrderServiceImpl implements OrderService {
     
    ...
    ...
}

9.9、账户服务

打开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);
}

9.10、异常测试

请重启service-orderservice-account工程

然后输入下单地址测试:http://localhost:9003/order/create?userId=1&productId=1&count=10&money=100&status=1

分布式事务管理:Seata_第46张图片

请打开数据库表,查看下单之后数据的变化,依次是:订单数据库表、账户数据库表、库存数据库表

我们发现虽然下单失败了,但是并没有往订单数据库插入订单信息,账户余额和库存也没有减少,这符合我们的业务逻辑,分布式下的事务管理完美解决。

而账户余额的回滚操作则是使用的是TCC模式下,我们自定义的第二阶段回滚方法。

注意:测试完毕,请关闭当前工程,防止影响其他项目。正常情况下,每一个服务都需要配置seata,我这里偷懒了,大家要注意一下!

你可能感兴趣的:(Spring,Cloud,Alibaba)