1. 事务
1.1 什么是事务
事务是逻辑上的一组操作,而这种操作是不可分割的执行单元。单元中的所有操作,要么全部执行成功,要么全部执行失败。
1.2 事务的特性
- 原子性(Atomic):指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做
- 一致性(Consistency):指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。
- 隔离性(Isolation):事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
- 持久性(Durability):指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
事务四大特性
1.3 事务分类
1.3.1 扁平事务(Flat Transaction)
扁平事务是事务类型中最简单的一种,也可能是使用最为频繁的事务。一般以 start transaction 或者 begin 开始,以 rollback 或者 commit结束,中间的所有的操作都在一个事务中。
特点:只能是提交或者回滚事务所有的操作,但无法提交或者回滚整个事务中的部分事务操作
示例:在MySQL中演示扁平事务
-
准备阶段
MySQL规划:- 创建ft_demo数据库和t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- t_account数据库表中插入两条数据分别是(1,'张三',200),(2,'李四',200)
-- 创建ft_demo数据库 CREATE DATABASE ft_demo; USE ft_demo; -- 创建t_account数据库表 CREATE TABLE `t_account` ( `id` bigint NOT NULL COMMENT '主键', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `money` int DEFAULT NULL COMMENT '金额' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息'; -- t_account数据库表中插入两条数据 INSERT INTO `t_account` (`id`,`name`,`money`) VALUES (1,'张三',200),(2,'李四',200);
执行效果截图:
-
在MySQL中演示扁平事务
-- 开启事务 start transaction; -- 事务:张三给李四转50 update t_account set money=money-50 where id=1; update t_account set money=money+50 where id=2; -- 提交事务 commit; -- 回滚事务 rollback;
执行事务回滚操作截图:
执行事务提交操作截图:
示例:在IDEA中演示扁平事务
-
准备阶段
MySQL规划:- 创建ft_demo数据库和t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- t_account数据库表中插入两条数据分别是(1,'张三',200),(2,'李四',200)
-- 创建ft_demo数据库 CREATE DATABASE ft_demo; USE ft_demo; -- 创建t_account数据库表 CREATE TABLE `t_account` ( `id` bigint NOT NULL COMMENT '主键', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `money` int DEFAULT NULL COMMENT '金额' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息'; -- t_account数据库表中插入两条数据 INSERT INTO `t_account` (`id`,`name`,`money`) VALUES (1,'张三',200),(2,'李四',200);
项目规划:
- 打开IDEA,创建一个Empty Project空工程,Name输入Seata-Code
- 在刚创建的Seata-Code空工程上,右键创建模块,Name输入jdbc-tx-demo
执行效果截图:
-
在IDEA中演示扁平事务
-
在jdbc-tx-demo模块中,找到pom.xml文件,加入MySQL的JDBC连接依赖,并指定编译版本
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> </dependencies>
-
在jdbc-tx-demo模块中,创建JdbcTx类,编写张三给李四转50的逻辑代码并运行
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class JdbcTx { /** * 张三给李四转50 */ public static void main(String[] args) throws SQLException { String db1Url = "jdbc:mysql://localhost:3306/ft_demo?severTimezone=UTC"; String user = "root"; String password = "ZYMzym111"; Connection connection1 = DriverManager.getConnection(db1Url, user, password); String db1Sql = "update t_account set money=money-50 where id=1"; String db2Sql = "update t_account set money=money+50 where id=2"; connection1.prepareStatement(db1Sql).execute(); connection1.prepareStatement(db2Sql).execute(); // 释放资源 connection1.close(); } }
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原本数据
-- 将张三数据重置为200 update t_account set money=200 where id=1; -- 将李四数据重置为200 update t_account set money=200 where id=2;
执行结果:
-
在JdbcTx类文件中,加入事务和异常业务逻辑
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class JdbcTx { /** * 张三给李四转50 */ public static void main(String[] args) throws SQLException { String db1Url = "jdbc:mysql://localhost:3306/ft_demo?severTimezone=UTC"; String user = "root"; String password = "ZYMzym111"; Connection connection1 = DriverManager.getConnection(db1Url, user, password); String db1Sql = "update t_account set money=money-50 where id=1"; String db2Sql = "update t_account set money=money+50 where id=2"; // 关闭事务自动提交 connection1.setAutoCommit(false); try { connection1.prepareStatement(db1Sql).execute(); // 异常信息 int i = 10 / 0; connection1.prepareStatement(db2Sql).execute(); // 事务提交 connection1.commit(); } catch (Exception e) { System.out.println(e); // 事务回滚 connection1.rollback(); } finally { // 释放资源 connection1.close(); } } }
代码执行截图:
-
查看数据库数据变化
执行之前
执行之后
-
1.3.2 带保存点的扁平事务(Flat Transactions with Savepoint)
保存点:用来通知系统应该记住事务当前的状态,以便之后发生错误时,事务能够回到保存点当时的状态。
除了对扁平事务支持外,允许在事物执行过程中回滚到同一事物中较早的状态。因为某些事务可能在执行过程出现的错误并不会导致所有的操作都无效,放弃整个事务不合乎要求,开销也太大。
保存点操作:
- 设置保存点:savepoint 保存点名称
- 回滚到保存点:rollback to 保存点名称
- 删除保存点:release savepoint 保存点的名称
示例:在MySQL中演示带保存点的扁平事务
-
准备阶段
MySQL规划:- 创建ftws_demo数据库和t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- t_account数据库表中插入两条数据分别是(1,'张三',200),(2,'李四',200)
-- 创建ftws_demo数据库 CREATE DATABASE ftws_demo; USE ftws_demo; -- 创建t_account数据库表 CREATE TABLE `t_account` ( `id` bigint NOT NULL COMMENT '主键', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `money` int DEFAULT NULL COMMENT '金额' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息'; -- t_account数据库表中插入两条数据 INSERT INTO `t_account` (`id`,`name`,`money`) VALUES (1,'张三',200),(2,'李四',200);
执行效果截图:
-
在MySQL中演示带保存点的扁平事务
-- 开启事物 start transaction; -- 事务:张三给李四转钱50 update t_account set money=money-50 where id=1; -- 插入保存点 savepoint point1; update t_account set money=money+50 where id=2; -- 回滚到point1保存点 rollback to point1;
执行效果截图:
带保存点的扁平事务,当系统发生崩溃时,所有的保存点都将消失,因此其保存点是易失的(Volatile),而非持久的(Persistent)。意味着,当系统发生崩溃进行恢复时,事务需要从开始处重新执行,而不能从最近的一个保存点执行。
1.3.3 链式事务(Chained Transaction)
链式事物,可以视为保存点模式的一种变种。在提交事务时,释放不需要的数据对象,将必要的处理上下文隐式传给下一个要开始的事务。
应当注意的是:
- 提交事务操作和开始下一个事务操作将合并为一个原子操作。意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行一样。
- 在上个事务提交时,会把隐式上下文传递给下一个事务,但同时也会释放当前事物的锁和保存点,当前事务的操作是持久化的,下个事务无法回滚到上个已经提交事物的保存点,因为上个事务的保存点在提交事务时已经释放。
示例:在MySQL中演示链式事务
-
准备阶段
MySQL规划:- 创建ct_demo数据库和t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- t_account数据库表中插入两条数据分别是(1,'张三',200),(2,'李四',200)
-- 创建ct_demo数据库 CREATE DATABASE ct_demo; USE ct_demo; -- 创建t_account数据库表 CREATE TABLE `t_account` ( `id` bigint NOT NULL COMMENT '主键', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `money` int DEFAULT NULL COMMENT '金额' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息'; -- t_account数据库表中插入两条数据 INSERT INTO `t_account` (`id`,`name`,`money`) VALUES (1,'张三',200),(2,'李四',200);
执行效果截图:
-
在MySQL中演示链式事务
-- 开启事务1 begin; -- 给张三减50 update t_account set money=money-50 where id=1; -- 设置保存点 savepoint point1; -- 提交事务 commit; -- 开启事务2 begin; -- 给张三加50 update t_account set money=money+50 where id=1; -- 设置保存点 savepoint point2; -- 给张三加50 update t_account set money=money+50 where id=1; -- 回滚到point1保存点 ; 无法回滚到已经提交事务的保存点上,回滚失败(SAVEPOINT point1 does not exist) rollback to point1;
执行效果截图:
链式事务与带有保存点的扁平事务不同的是:
- 带有保存点的扁平事务能回滚到任意正确的保存点。而链式事务中的回滚只限于当前事务,即只能恢复到最近一个保存点。
- 链式事务在执行commit后即释放了当前事务所持有的锁,而带保存点的扁平事务不影响所有的锁
1.3.4 嵌套事务(Nested Transaction)
嵌套事务是一个层次结构架构,由一个顶层事务(top-level transaction)控制着各个层次的事务。顶层事务之下嵌套的事务被称为子事务(subtransaction),其控制着每个局部的变换。
嵌套事务由多个事务嵌套,多个事务共同完成一项任务,嵌套事务有点类似树形结构,外部有个顶层事务, 顶层事务控制内部子事务,内部子事务提交,整体事务并不会提交,整体十五的提交主要是看顶层事务,顶层事务提交,整个事务才是真正的提交。
应当注意的是:
- 嵌套事务的提交是从内向外的(从子事务到顶层事务)
- 子事务任何一个事务回滚,这个事务包含的所有子事务都会回滚;当顶层事务回滚时,则嵌套事务的所有事务都会回滚,哪怕子事务已经提交也会进行回滚
- MySQL默认不支持嵌套事务,但可以模拟实现类似的嵌套事务
示例:在MySQL中模拟类似的嵌套事务
-
准备阶段
MySQL规划:- 创建nt_demo数据库和t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- t_account数据库表中插入两条数据分别是(1,'张三',200),(2,'李四',200)
-- 创建nt_demo数据库 CREATE DATABASE nt_demo; USE nt_demo; -- 创建t_account数据库表 CREATE TABLE `t_account` ( `id` bigint NOT NULL COMMENT '主键', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `money` int DEFAULT NULL COMMENT '金额' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息'; -- t_account数据库表中插入两条数据 INSERT INTO `t_account` (`id`,`name`,`money`) VALUES (1,'张三',200),(2,'李四',200);
执行效果截图:
-
在MySQL中模拟类似的嵌套事务
-- 开启事务(顶层事务) begin; -- 通过保存点模拟第一个嵌套子事务 savepoint point1; -- 张三减钱50 update t_account set money=money-50 where id=1; -- 通过保存点模拟第二个嵌套子事务 savepoint point2; -- 张三减钱100 update t_account set money=money-100 where id=1; -- 回滚第二个嵌套子事务 rollback to point2; -- 查看回滚到point2时的数据 select * from t_account; -- 回滚到第一个嵌套子事务 rollback to point1; -- 查看回滚到point1时的数据 select * from t_account; -- 整体回滚 rollback; -- 查看回滚后的数据 select * from t_account;
执行效果截图:
1.3.5 分布式事务(Distributed Transactions)
分布式事务指事物的参与者、事务所在的服务器,以及涉及的资源服务器、事务管理器分别位于不同的服务或者不同的数据库节点上。
示例:在IDEA中演示分布式事务-多数据源
-
准备工作
MySQL规划:- 创建db1数据库、db2数据库
- 在db1和db2数据库中分别创建t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- 在db1数据库创建的t_account数据库表中插入数据(1,'张三',200)
- 在db2数据库创建的t_account数据库表中插入数据(2,'李四',200)
-- 创建db1数据库、db2数据库 CREATE DATABASE db1; CREATE DATABASE db2; USE db1; USE db2; -- 在db1和db2数据库中分别创建t_account数据库表 CREATE TABLE `t_account` ( `id` bigint NOT NULL COMMENT '主键', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `money` int DEFAULT NULL COMMENT '金额' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息'; -- 在db1数据库创建的t_account数据库表中插入数据(1,'张三',200) INSERT INTO db1.`t_account` (`id`,`name`,`money`) VALUES (1,'张三',200); -- 在db2数据库创建的t_account数据库表中插入数据(2,'李四',200) INSERT INTO db2.`t_account` (`id`,`name`,`money`) VALUES (2,'李四',200);
项目规划:
- 打开IDEA,创建一个Empty Project空工程,Name输入Seata-Code
- 在刚创建的Seata-Code空工程上,右键创建模块,Name输入jdbc-tx-demo
执行效果截图:
-
在IDEA中演示分布式事务-多数据源
-
在jdbc-tx-demo模块中,找到pom.xml文件,加入MySQL的JDBC连接依赖,并制定编译版本
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> </dependencies>
-
在jdbc-tx-demo模块中,创建JdbcTxDemo类,编写张三给李四转50的逻辑代码并运行
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class JdbcTxDemo { /** * 张三给李四转50 */ public static void main(String[] args) throws SQLException { String db1Url = "jdbc:mysql://localhost:3306/db1?severTimezone=UTC"; String db2Url = "jdbc:mysql://localhost:3306/db2?severTimezone=UTC"; String user = "root"; String password = "ZYMzym111"; // 张三减50 Connection connection1 = DriverManager.getConnection(db1Url, user, password); String db1Sql = "update t_account set money=money-50 where id=1"; connection1.prepareStatement(db1Sql).execute(); // 李四加50 Connection connection2 = DriverManager.getConnection(db2Url, user, password); String db2Sql = "update t_account set money=money+50 where id=2"; connection2.prepareStatement(db2Sql).execute(); // 释放资源 connection1.close(); connection2.close(); } }
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
-- 将张三数据重置为200 update db1.t_account set money=200 where id=1; -- 将李四数据重置为200 update db2.t_account set money=200 where id=2;
执行结果截图:
-
在JdbcTxDemo类中异常业务逻辑
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; /** * @author admin */ public class JdbcTxDemo { /** * 张三给李四转50 */ public static void main(String[] args) throws SQLException { String db1Url = "jdbc:mysql://localhost:3306/db1?severTimezone=UTC"; String db2Url = "jdbc:mysql://localhost:3306/db2?severTimezone=UTC"; String user = "root"; String password = "ZYMzym111"; // 张三减50 Connection connection1 = DriverManager.getConnection(db1Url, user, password); String db1Sql = "update t_account set money=money-50 where id=1"; connection1.prepareStatement(db1Sql).execute(); /*由于种种原因出错*/ int i=10/0; // 李四加50 Connection connection2 = DriverManager.getConnection(db2Url, user, password); String db2Sql = "update t_account set money=money+50 where id=2"; connection2.prepareStatement(db2Sql).execute(); // 释放资源 connection1.close(); connection2.close(); } }
代码执行截图:
-
查看数据库数据变化
执行之前
执行之后
-
2. 分布式事务
2.1 什么是分布式事务
分布式事务是指在分布式系统或分布式数据库环境中,涉及多个参与者、服务器、资源服务器和事物管理器的事务。在这种情境下,事务必须满足原子性,即所有操作要么全部成功,要么全部失败。分布式事务需要确保在不同服务器和项目中的事务能够保持一致,从而实现整体成功或整体失败。
2.2 分布式事务产生的背景
-
数据库的数据量逐渐增大,最终拆库拆表。把一个数据库中的数据,放到多台数据库中
- 分库:Springboot配置多数据库源
- 分表:ShardingSphere
-
项目架构的变化:单体架构 → 垂直架构 → 分布式架构 → SOA架构 → 微服务架构
- 单体架构:Web应用程序发展的早期,将所有的功能模块打包到一起并放在一个web容器中运行,所有功能模块使用同一个数据库。
- 垂直架构:根据业务把项目垂直切割成多个项目。
- 分布式架构:单体系统按业务垂直拆分为若干系统,系统之间通过网络交互来完成用户的业务处理,每个系统可分布式部署。
- SOA架构:SOA是一种面向服务的架构,基于分布式架构,它将不同业务功能按服务进行拆分,并通过这些服务之间定义良好的接口和协议联系起来。
各层的作用:- 应用层(展示层):用户最近的一层。接受用户请求,并使用下层提供的接口返回数据,并且该层禁止访问数据库
- 业务服务层:根据具体业务场景演变而来的模块
- 基础业务层:业务的核心
- 基础服务层:这一层是与业务无关的模块,是一些通用的服务,这类服务的特点:请求量大,逻辑简单,特征明显,功能独立
- 存储层:不同的存储类型
- 微服务:基于SOA架构的思想,为了满足移动互联网对大型项目及多客户端的需求,对服务层进行细粒度的拆分,所拆分的每个服务只完成某个特定的业务功能,服务的粒度很小,所以称为微服务架构。
2.3 分布式事务产生的场景
- 跨数据库
单体项目中,数据层进行拆库拆表,或者多数据源情况。 - 跨进程(情况很少见)
多个服务访问同一个数据库。 - 跨JVM进程 跨数据库
微服务项目,一个事务的执行,需要牵扯多个服务,每个服务都有自己的数据库。
不论是跨进程,还是跨数据库,还是说多服务访问单数据库。都有一个共同的特点,操作时可能存在多个数据库session会话,即如果一组操作中会产生多个数据库session会话,此时就会出现分布式事务。
2.4 分布式事务理论
2.4.1 CAP理论
CAP理论指在一个相互连接且共享数据的分布式系统中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition tolerance)三者中的两个,三者不可兼得。即,要么满足CP,要么满足AP;但无法满足CA,更无法满足CAP
- 一致性(Consistence):对指定的客户端来说,读操作保证能够返回最新的写操作的结果。
- 可用性(Availability):非故障节点在合理的时间内返回合理的响应。合理的响应,就是不存在超时和响应错误的情况
- 分区容错性(Partition tolerance):当网络分区后,系统可以继续正常的工作。网络分区,网络设备出现的丢包、阻塞、超时等问题。
实际考量:
- 一般系统中会优先使用AP,不强调一致性,允许数据在某个一个时间点不一致,但是数据经过一段时间间隔最终会一致
- 对一致性要求比较高的系统可能会选择CP,强调一致性。
2.4.2 Base理论
Base理论时对可用性和一致性的权衡。Base理论由Basically Available(基本可用),Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写构成。
- 基本可用(Basically Available):分布式系统中,如果出现故障,允许系统损失部分功能,但是要保证系统基本可用,而不是整个系统死掉
- 软状态(Soft state):允许系统存在中间状态,中间状态不会影响系统的整体可用性,允许系统各个节点中数据同步延迟。
- 最终一致性(Eventually consistent):允许系统中各个节点的数据存在不一致的情况,但是一段时间后,各个节点上的数据,最终还是一致的。
2.5 分布式事务解决方案
- 遵循CP 强一致性
- XA 协议:数据库级别的规范,需要数据库的支持
- 遵循AP 弱一致性
- TCC
- AT
- Saga
- 可靠消息最终一致性
2.6 分布式事务解决模型
2.6.1 DTP模型(Distributed Transaction Processing)
DTP(Distributed Transaction Processing)模型是一个用于描述分布式事务处理系统的理论模型,它定义了分布式事务处理的基本组件和交互方式。
DTP模型包括以下几个主要组件:
- AP(应用程序,Application Program):发起分布式事务的应用程序。
- RM(资源管理器,Resource Manager):负责管理分布式事务中所涉及的资源,例如数据库、消息队列等。RM负责处理事务操作并保证操作的原子性。
- TM(事务管理器,Transaction Manager):负责协调分布式事务的整个生命周期,包括开始事务、提交事务和回滚事务。TM与各个RM进行通信,确保分布式事务的一致性。
- CICS(通信控制器,Communication Controller):负责在各个RM和TM之间传递事务信息,例如提交请求、回滚请求等。CICS可以视为通信中间件。
DTP模型的工作流程如下:
- AP发起一个分布式事务,并请求TM开始事务。
- TM向涉及的所有RM发送开始事务请求。
- RM收到开始事务请求后,执行相应的操作,并向TM报告操作结果。
- 当所有RM都报告操作成功时,TM通知所有RM提交事务。如果某个RM操作失败,TM通知所有RM回滚事务。
2.6.2 两阶段体提交模型(2PC)
两阶段提交(Two-Phase Commit,2PC)是一种用于确保分布式事务原子性和一致性的经典协议。在2PC中,事务管理器(Transaction Manager,TM)负责协调参与事务的所有资源管理器(Resource Manager,RM)。
两阶段提交分为两个阶段:
- 请求阶段(Prepare Phase):
在请求阶段,事务管理器向所有涉及的资源管理器发送一个准备请求。资源管理器在本地执行事务,但不提交,并将结果(例如,成功或失败)返回给事务管理器。如果所有资源管理器都报告成功,则进入提交阶段。如果有任何一个资源管理器报告失败,则进入中止阶段。 - 提交阶段(Commit Phase):
在提交阶段,事务管理器根据请求阶段的结果来决定是提交还是中止事务。如果所有资源管理器在请求阶段都报告成功,则事务管理器向所有资源管理器发送提交请求。资源管理器此时提交本地事务,并释放资源。如果任何资源管理器在请求阶段报告失败,则事务管理器向所有资源管理器发送中止请求。资源管理器回滚本地事务,释放资源。
两阶段提交协议可以确保分布式事务的原子性和一致性,但在某些情况下可能面临一些问题,如单点故障、阻塞、性能问题和数据不一致。
两阶段提交成功模型图:
两阶段提交失败模型图:
2.7 分布式事务理论、模型、解决方案三者间关系
- 通过理论获取解决方案,有2种(强一致性、弱一致性)
- 模型是统一解决方案的实现
总结:分布式事务解决思想来自于理论,分布式事务解决落地来自于模型
3. 强一致性:XA协议
3.1 什么是XA协议
XA协议是由X/OPEN提出的分布式事务处理规范。XA规范了TM和RM之间的通信接口,在TM与多个RM之间形成了一个双向通信桥梁,从而在多个数据库资源下保证ACID四个特性。想要使用XA的数据库必须要得要支持XA。
XA是数据库分布式事务,强一致性。在整个过程中,数据库一旦锁住状态,即从Prepare到Commit、Rollback的整个过程中,TM一致把持着数据库的锁。当其他人进行修改数据库数据库时,需要等待锁释放,存在长事务风险。
3.2 数据库查看是否支持XA协议
3.2.1 MySQL查看方式
在MySQL的命令行工具中,输入show engines,查看返回结果,在MySQL的InnoDB数据存储引擎下支持XA协议。
3.3 XA基本语法规范
- 开启XA事务:xa start
- 结束XA事务:xa end
- 准备提交XA事务:xa prepare
- 提交XA事务:xa commit
- 回滚XA事务:xa rollback
3.4 使用支持XA协议的数据库解决分布式事务
3.4.1 使用MySQL控制台解决
-
准备阶段
MySQL规划:- 创建db1数据库、db2数据库
- 在db1和db2数据库中分别创建t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- 在db1数据库创建的t_account数据库表中插入数据(1,'张三',200)
- 在db2数据库创建的t_account数据库表中插入数据(2,'李四',200)
-- 创建db1数据库、db2数据库 CREATE DATABASE db1; CREATE DATABASE db2; USE db1; USE db2; -- 在db1和db2数据库中分别创建t_account数据库表 CREATE TABLE `t_account` ( `id` bigint NOT NULL COMMENT '主键', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `money` int DEFAULT NULL COMMENT '金额' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息'; -- 在db1数据库创建的t_account数据库表中插入数据(1,'张三',200) INSERT INTO db1.`t_account` (`id`,`name`,`money`) VALUES (1,'张三',200); -- 在db2数据库创建的t_account数据库表中插入数据(2,'李四',200) INSERT INTO db2.`t_account` (`id`,`name`,`money`) VALUES (2,'李四',200);
执行效果截图:
-
MySQL控制台演示XA协议
-- 开启XA事务 xa start 事务ID xa start 'xa1'; -- 操作业务逻辑 张三给李四转50 update db1.t_account set money=money-50 where id=1; update db2.t_account set money=money+50 where id=2; -- 结束XA事务 xa end 事务ID xa end 'xa1'; -- prepare准备阶段 xa prepare 事务ID xa prepare 'xa1'; -- 查看业务逻辑的数据值 select * from db1.t_account; -- commit阶段 xa commit 事务ID xa commit 'xa1'; -- rollback阶段 xa rollback 事务ID xa rollback 'xa1';
执行效果截图:
执行回滚逻辑截图
执行提交逻辑截图
3.4.2 使用JDBC操作MySQL多数据源
-
准备阶段
MySQL规划:- 创建db1数据库、db2数据库
- 在db1和db2数据库中分别创建t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- 在db1数据库创建的t_account数据库表中插入数据(1,'张三',200)
- 在db2数据库创建的t_account数据库表中插入数据(2,'李四',200)
-- 创建db1数据库、db2数据库 CREATE DATABASE db1; CREATE DATABASE db2; USE db1; USE db2; -- 在db1和db2数据库中分别创建t_account数据库表 CREATE TABLE `t_account` ( `id` bigint NOT NULL COMMENT '主键', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `money` int DEFAULT NULL COMMENT '金额' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息'; -- 在db1数据库创建的t_account数据库表中插入数据(1,'张三',200) INSERT INTO db1.`t_account` (`id`,`name`,`money`) VALUES (1,'张三',200); -- 在db2数据库创建的t_account数据库表中插入数据(2,'李四',200) INSERT INTO db2.`t_account` (`id`,`name`,`money`) VALUES (2,'李四',200);
项目规划:
- 打开IDEA,创建一个Empty Project空工程,Name输入Seata-Code
- 在刚创建的Seata-Code空工程上,右键创建模块,Name输入jdbc-tx-demo
执行效果截图:
-
JDBC使用MySQL操作多数据源
-
在jdbc-tx-demo模块中,找到pom.xml文件,加入MySQL的JDBC连接依赖,并指定编译版本
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> </dependencies>
-
在jdbc-tx-demo模块中,创建JdbcTxMutiDataBase类,编写张三给李四转50的逻辑代码并运行
import com.mysql.cj.jdbc.MysqlXADataSource; import com.mysql.cj.jdbc.MysqlXid; import javax.sql.XAConnection; import javax.transaction.xa.XAException; import javax.transaction.xa.XAResource; import javax.transaction.xa.Xid; import java.sql.SQLException; public class JdbcTxMutiDataBase { public static void main(String[] args) throws SQLException, XAException { String db1Url = "jdbc:mysql://localhost:3306/db1?severTimezone=UTC"; String db2Url = "jdbc:mysql://localhost:3306/db2?severTimezone=UTC"; String user = "root"; String password = "ZYMzym111"; // 创建数据源(db1和db2) MysqlXADataSource db1DataSource = new MysqlXADataSource(); db1DataSource.setURL(db1Url); db1DataSource.setUser(user); db1DataSource.setPassword(password); MysqlXADataSource db2DataSource = new MysqlXADataSource(); db2DataSource.setURL(db2Url); db2DataSource.setUser(user); db2DataSource.setPassword(password); // 获取资源管理器(db1和db2) XAConnection db1Connection = db1DataSource.getXAConnection(); XAResource db1Resource = db1Connection.getXAResource(); XAConnection db2Connection = db2DataSource.getXAConnection(); XAResource db2Resource = db2Connection.getXAResource(); // 获取XA 事务ID Xid db1ID = new MysqlXid("demo".getBytes(), "db1".getBytes(), 1); Xid db2ID = new MysqlXid("demo".getBytes(), "db2".getBytes(), 1); //开启XA事务 db1Resource.start(db1ID, XAResource.TMNOFLAGS); db2Resource.start(db2ID, XAResource.TMNOFLAGS); try { String db1Sql = "update t_account set money=money-50 where id=1"; db1Connection.getConnection().prepareStatement(db1Sql).execute(); db1Resource.end(db1ID, XAResource.TMSUCCESS); String db2Sql = "update t_account set money=money+50 where id=2"; db2Connection.getConnection().prepareStatement(db2Sql).execute(); db2Resource.end(db2ID, XAResource.TMSUCCESS); //Prepare int db1prepare = db1Resource.prepare(db1ID); int db2prepare = db2Resource.prepare(db2ID); if (db1prepare == XAResource.XA_OK && db2prepare == XAResource.XA_OK) { db1Resource.commit(db1ID, false); db2Resource.commit(db2ID, false); } } catch (Exception e) { System.err.println("出现了异常"); db1Resource.rollback(db1ID); db2Resource.rollback(db2ID); } } }
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
-- 将张三数据重置为200 update db1.t_account set money=200 where id=1; -- 将李四数据重置为200 update db2.t_account set money=200 where id=2;
执行结果截图:
-
在JdbcTxMutiDataBase类中,加入异常业务逻辑
import com.mysql.cj.jdbc.MysqlXADataSource; import com.mysql.cj.jdbc.MysqlXid; import javax.sql.XAConnection; import javax.transaction.xa.XAException; import javax.transaction.xa.XAResource; import javax.transaction.xa.Xid; import java.sql.SQLException; public class JdbcTxMutiDataBase { public static void main(String[] args) throws SQLException, XAException { String db1Url = "jdbc:mysql://localhost:3306/db1?severTimezone=UTC"; String db2Url = "jdbc:mysql://localhost:3306/db2?severTimezone=UTC"; String user = "root"; String password = "ZYMzym111"; // 创建数据源(db1和db2) MysqlXADataSource db1DataSource = new MysqlXADataSource(); db1DataSource.setURL(db1Url); db1DataSource.setUser(user); db1DataSource.setPassword(password); MysqlXADataSource db2DataSource = new MysqlXADataSource(); db2DataSource.setURL(db2Url); db2DataSource.setUser(user); db2DataSource.setPassword(password); // 获取资源管理器(db1和db2) XAConnection db1Connection = db1DataSource.getXAConnection(); XAResource db1Resource = db1Connection.getXAResource(); XAConnection db2Connection = db2DataSource.getXAConnection(); XAResource db2Resource = db2Connection.getXAResource(); // 获取XA 事务ID Xid db1ID = new MysqlXid("demo".getBytes(), "db1".getBytes(), 1); Xid db2ID = new MysqlXid("demo".getBytes(), "db2".getBytes(), 1); //开启XA事务 db1Resource.start(db1ID, XAResource.TMNOFLAGS); db2Resource.start(db2ID, XAResource.TMNOFLAGS); try { String db1Sql = "update t_account set money=money-50 where id=1"; db1Connection.getConnection().prepareStatement(db1Sql).execute(); db1Resource.end(db1ID, XAResource.TMSUCCESS); String db2Sql = "update t_account set money=money+50 where id=2"; db2Connection.getConnection().prepareStatement(db2Sql).execute(); db2Resource.end(db2ID, XAResource.TMSUCCESS); // 异常逻辑 int i = 10 / 0; //Prepare int db1prepare = db1Resource.prepare(db1ID); int db2prepare = db2Resource.prepare(db2ID); if (db1prepare == XAResource.XA_OK && db2prepare == XAResource.XA_OK) { db1Resource.commit(db1ID, false); db2Resource.commit(db2ID, false); } } catch (Exception e) { System.err.println("出现了异常"); db1Resource.rollback(db1ID); db2Resource.rollback(db2ID); } } }
代码执行截图:
-
查看数据库数据变化
执行之前
执行之后
-
3.4.3 使用Mybatis框架操作MySQL多数据源
-
准备阶段
MySQL规划:- 创建db1数据库、db2数据库
- 在db1数据库中创建t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- 在db2数据库中创建t_user_cash数据库表
- t_user_cash数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- 在db1数据库创建的t_account数据库表中插入数据(1,'张三',200)
- 在db2数据库创建的t_user_cash数据库表中插入数据(2,'李四',200)
-- 创建db1数据库 CREATE DATABASE db1; USE db1; -- 在db1数据库中创建t_account数据库表 CREATE TABLE `t_account` ( `id` bigint NOT NULL COMMENT '主键', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `money` int DEFAULT NULL COMMENT '金额' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息'; -- 在db1数据库创建的t_account数据库表中插入数据(1,'张三',200) INSERT INTO db1.`t_account` (`id`,`name`,`money`) VALUES (1,'张三',200); -- 创建db2数据库 CREATE DATABASE db2; USE db2; -- 在db2数据库中创建t_user_cash数据库表 CREATE TABLE `t_user_cash` ( `id` bigint NOT NULL COMMENT '主键', `name` varchar(30) DEFAULT NULL COMMENT '姓名', `money` int DEFAULT NULL COMMENT '金额' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息'; -- 在db2数据库创建的t_user_cash数据库表中插入数据(2,'李四',200) INSERT INTO db2.`t_user_cash` (`id`,`name`,`money`) VALUES (2,'李四',200);
项目规划:
- 打开IDEA,创建一个Empty Project空工程,Name输入Seata-Code
- 在刚创建的Seata-Code工程上,右键创建模块,Name输入mybatis-java-demo
执行效果截图:
-
搭建基础环境
-
在mybatis-java-demo模块中,找到pom.xml文件,加入JDBC连接依赖和Mybatis插件依赖,并指定编译版本
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.10</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> </dependencies>
-
在mybatis-java-demo模块中,创建uitls包,新建DBFactoryUtils类,类中定义两个SqlSessionFactory数据源
import org.apache.ibatis.datasource.pooled.PooledDataSource; import org.apache.ibatis.mapping.Environment; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.apache.ibatis.transaction.TransactionFactory; import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; import org.example.mapper.db1.AccountMapper; import org.example.mapper.db2.UserCashMapper; import javax.sql.DataSource; public class DBFactoryUtils { public static SqlSessionFactory db1SessionFactory() { String dirverClassName = "com.mysql.cj.jdbc.Driver"; String db1Url = "jdbc:mysql://localhost:3306/db1?severTimezone=UTC"; String user = "root"; String password = "ZYMzym111"; DataSource dataSource = new PooledDataSource(dirverClassName, db1Url, user, password); TransactionFactory transactionFactory = new JdbcTransactionFactory(); Environment environment = new Environment("development", transactionFactory, dataSource); Configuration configuration = new Configuration(environment); configuration.addMapper(AccountMapper.class); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); return sqlSessionFactory; } public static SqlSessionFactory db2SessionFactory() { String dirverClassName = "com.mysql.cj.jdbc.Driver"; String db2Url = "jdbc:mysql://localhost:3306/db2?severTimezone=UTC"; String user = "root"; String password = "ZYMzym111"; DataSource dataSource = new PooledDataSource(dirverClassName, db2Url, user, password); TransactionFactory transactionFactory = new JdbcTransactionFactory(); Environment environment = new Environment("development", transactionFactory, dataSource); Configuration configuration = new Configuration(environment); configuration.addMapper(UserCashMapper.class); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); return sqlSessionFactory; } }
-
在DBFactoryUtils类中,两个SqlSessionFactory数据源分别声明:AccountMapper.class和UserCashMapper.class。在mybatis-java-demo模块中,创建mapper包,创建这两个被声明的接口类,编写对应的逻辑操作
import org.apache.ibatis.annotations.Update; public interface AccountMapper { @Update("update t_account set money=money-#{money} where id=1") public void reduceMoney(int money); }
import org.apache.ibatis.annotations.Update; public interface AccountMapper { @Update("update t_account set money=money-#{money} where id=1") public void reduceMoney(int money); }
-
在mybatis-java-demo模块中,找到Main类,编写张三向李四转账100的业务逻辑代码
import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.example.mapper.db1.AccountMapper; import org.example.mapper.db2.UserCashMapper; import org.example.utils.DBFactoryUtils; public class Main { public static void main(String[] args) { SqlSessionFactory db1Factory= DBFactoryUtils.db1SessionFactory(); SqlSessionFactory db2Factory= DBFactoryUtils.db2SessionFactory(); SqlSession sqlSession1 = db1Factory.openSession(true); sqlSession1.getMapper(AccountMapper.class).reduceMoney(100); SqlSession sqlSession2 = db2Factory.openSession(true); sqlSession2.getMapper(UserCashMapper.class).increMoney(100); sqlSession1.close(); sqlSession2.close(); } }
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
-- 将张三数据重置为200 update db1.t_account set money=200 where id=1; -- 将李四数据重置为200 update db2.t_user_cash set money=200 where id=2;
执行结果截图:
-
-
使用Mybatis框架操作MySQL多数据源
-
在mybatis-java-demo模块中,找到Main类,加入分布式事务处理逻辑
import com.mysql.cj.jdbc.JdbcConnection; import com.mysql.cj.jdbc.MysqlXAConnection; import com.mysql.cj.jdbc.MysqlXid; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.example.mapper.db1.AccountMapper; import org.example.mapper.db2.UserCashMapper; import org.example.utils.DBFactoryUtils; import javax.sql.XAConnection; import javax.transaction.xa.XAException; import javax.transaction.xa.XAResource; import javax.transaction.xa.Xid; import java.sql.SQLException; public class Main { public static void main(String[] args) throws SQLException, XAException { SqlSessionFactory db1Factory = DBFactoryUtils.db1SessionFactory(); SqlSessionFactory db2Factory = DBFactoryUtils.db2SessionFactory(); // 获取XAResource SqlSession sqlSession1 = db1Factory.openSession(true); XAConnection xaConnection1 = new MysqlXAConnection((JdbcConnection) sqlSession1.getConnection(), false); XAResource xaResource1 = xaConnection1.getXAResource(); SqlSession sqlSession2 = db2Factory.openSession(true); XAConnection xaConnection2 = new MysqlXAConnection((JdbcConnection) sqlSession2.getConnection(), false); XAResource xaResource2 = xaConnection2.getXAResource(); // 获取XA 事务ID Xid db1ID = new MysqlXid("demo".getBytes(), "db1".getBytes(), 1); Xid db2ID = new MysqlXid("demo".getBytes(), "db2".getBytes(), 1); // 开启XAResource xaResource1.start(db1ID, XAResource.TMNOFLAGS); xaResource2.start(db2ID, XAResource.TMNOFLAGS); try { // 业务逻辑张三向李四转100 sqlSession1.getMapper(AccountMapper.class).reduceMoney(100); sqlSession2.getMapper(UserCashMapper.class).increMoney(100); // 结束XAResource xaResource1.end(db1ID, XAResource.TMSUCCESS); xaResource2.end(db2ID, XAResource.TMSUCCESS); // 开启Prepare阶段 int prepare1 = xaResource1.prepare(db1ID); int prepare2 = xaResource2.prepare(db2ID); if (prepare1 == XAResource.XA_OK && prepare2 == XAResource.XA_OK) { // 事务提交 xaResource1.commit(db1ID, false); xaResource2.commit(db2ID, false); } } catch (Exception e) { System.err.println(e); // 事务回滚 xaResource1.rollback(db1ID); xaResource2.rollback(db2ID); } finally { sqlSession1.close(); sqlSession2.close(); } } }
-
总结:使⽤mybatis等⼀系列持久层框架操作数据时,屏蔽掉了底层的jdbc操作,如果想要原⽣的XA解决⽅式需要在mybatis编码中引⼊XA相关的API,此时使⽤框架将并不能带来开发的便捷和快速
4. Seata 分布式事务框架
4.1 Seata介绍
-
Seata是什么
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
-
Seata能做什么
开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务
-
Seata组成
- AP(Application)- 应用程序:分布式项目
- TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
-
Seata解决事务的方式
- 强一致性
- XA模式(底层基于数据库XA协议来完成的ACID)
- 弱一致性
- AT模式
- TCC模式
- Saga模式
- 强一致性
-
Seata组成角色之间的关系
TM和RM都会与TC进行通讯,默认使用Netty
4.2 Seata环境搭建
-
Windows环境下部署Seata
seata下载页面:https://seata.io/zh-cn/blog/download.html
-
点击进入seata下载页面
-
点击soure和binary,下载源码包和编译之后的包
-
解压seata-server-1.6.1.zip,进入seata的bin目录下
-
通过cmd进入当前目录,并执行seata-server.bat文件
-
运行完成之后,进入浏览器访问 localhost:7091 进入seata登录页面
-
-
Seata环境搭建(docker部署)
-
打开cmd命令行窗口,查询Seata镜像
docker search seata
-
拉取Seata镜像
docker pull seataio/seata-server:1.6.1
-
启动Seata容器
docker run -d --name seata -p7901:7901 -p8901:8901 seataio/seata-server:1.6.1
-
5. Seata分布式事务实现XA模式
5.1 原生API 实现 单体项目多数据源的分布式事务
5.1.1 准备阶段
MySQL规划:
- 创建db1数据库、db2数据库
- 在db1数据库中创建t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- 在db2数据库中创建t_user_cash数据库表
- t_user_cash数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- 在db1数据库创建的t_account数据库表中插入数据(1,'张三',200)
- 在db2数据库创建的t_user_cash数据库表中插入数据(2,'李四',200)
-- 创建db1数据库
CREATE DATABASE db1;
USE db1;
-- 在db1数据库中创建t_account数据库表
CREATE TABLE `t_account` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`money` int DEFAULT NULL COMMENT '金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息';
-- 在db1数据库创建的t_account数据库表中插入数据(1,'张三',200)
INSERT INTO db1.`t_account` (`id`,`name`,`money`) VALUES (1,'张三',200);
-- 创建db2数据库
CREATE DATABASE db2;
USE db2;
-- 在db2数据库中创建t_user_cash数据库表
CREATE TABLE `t_user_cash` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`money` int DEFAULT NULL COMMENT '金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息';
-- 在db2数据库创建的t_user_cash数据库表中插入数据(2,'李四',200)
INSERT INTO db2.`t_user_cash` (`id`,`name`,`money`) VALUES (2,'李四',200);
项目规划:
- 打开IDEA,创建一个Empty Project空工程,Name输入Seata-Code
- 在刚创建的Seata-Code工程上,右键创建模块,Name输入seata-java-demo
执行效果截图:
5.1.2 搭建基础环境
-
在seata-java-demo模块中,找到pom.xml文件,加入JDBC连接依赖和Mybatis插件依赖,并指定编译版本
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> </dependencies>
-
在seata-java-demo模块中,创建uitls包,新建DBFactoryUtils类,类中定义两个SqlSessionFactory数据源
import org.apache.ibatis.datasource.pooled.PooledDataSource; import org.apache.ibatis.mapping.Environment; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.apache.ibatis.transaction.TransactionFactory; import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; import org.example.mapper.db1.AccountMapper; import org.example.mapper.db2.UserCashMapper; import javax.sql.DataSource; public class DBFactoryUtils { public static SqlSessionFactory db1SessionFactory() { String dirverClassName = "com.mysql.cj.jdbc.Driver"; String db1Url = "jdbc:mysql://localhost:3306/db1?severTimezone=UTC"; String user = "root"; String password = "ZYMzym111"; DataSource dataSource = new PooledDataSource(dirverClassName, db1Url, user, password); TransactionFactory transactionFactory = new JdbcTransactionFactory(); Environment environment = new Environment("development", transactionFactory, dataSource); Configuration configuration = new Configuration(environment); configuration.addMapper(AccountMapper.class); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); return sqlSessionFactory; } public static SqlSessionFactory db2SessionFactory() { String dirverClassName = "com.mysql.cj.jdbc.Driver"; String db2Url = "jdbc:mysql://localhost:3306/db2?severTimezone=UTC"; String user = "root"; String password = "ZYMzym111"; DataSource dataSource = new PooledDataSource(dirverClassName, db2Url, user, password); TransactionFactory transactionFactory = new JdbcTransactionFactory(); Environment environment = new Environment("development", transactionFactory, dataSource); Configuration configuration = new Configuration(environment); configuration.addMapper(UserCashMapper.class); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); return sqlSessionFactory; } }
-
在DBFactoryUtils类中,两个SqlSessionFactory数据源分别声明:AccountMapper.class和UserCashMapper.class。在mybatis-java-demo模块中,创建mapper包,创建这两个被声明的接口类,编写对应的逻辑操作
import org.apache.ibatis.annotations.Update; public interface AccountMapper { @Update("update t_account set money=money-#{money} where id=1") public void reduceMoney(int money); }
import org.apache.ibatis.annotations.Update; public interface AccountMapper { @Update("update t_account set money=money-#{money} where id=1") public void reduceMoney(int money); }
-
在mybatis-java-demo模块中,找到Main类,编写张三向李四转账100的业务逻辑代码
import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.example.mapper.db1.AccountMapper; import org.example.mapper.db2.UserCashMapper; import org.example.utils.DBFactoryUtils; public class Main { public static void main(String[] args) { SqlSessionFactory db1Factory= DBFactoryUtils.db1SessionFactory(); SqlSessionFactory db2Factory= DBFactoryUtils.db2SessionFactory(); SqlSession sqlSession1 = db1Factory.openSession(true); sqlSession1.getMapper(AccountMapper.class).reduceMoney(100); SqlSession sqlSession2 = db2Factory.openSession(true); sqlSession2.getMapper(UserCashMapper.class).increMoney(100); sqlSession1.close(); sqlSession2.close(); } }
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
-- 将张三数据重置为200 update db1.t_account set money=200 where id=1; -- 将李四数据重置为200 update db2.t_user_cash set money=200 where id=2;
执行结果截图:
5.1.3 原生API 单体项目多数据源 改造
-
添加Seata依赖
在seata-java-demo模块中,找到pom.xml文件,引入Seata包依赖
<dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>1.6.1</version> </dependency>
-
编写Seata配置信息
在seata-java-demo模块的main/resources文件夹下,添加file.conf配置文件和registry.conf配置文件
service { #transaction service group mapping vgroupMapping.default_tx_group = "default" #only support when registry.type=file, please don't set multiple addresses default.grouplist = "127.0.0.1:8091" #disable seata disableGlobalTransaction = false }
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "file" file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" file { name = "file.conf" } }
-
修改数据源信息
在seata-java-demo模块中,找到DBFactoryUtils类,将原本的DataSource数据源加入到DataSourceProxyXA数据源中,使用DataSourceProxyXA数据源
import io.seata.rm.datasource.xa.DataSourceProxyXA; import org.apache.ibatis.datasource.pooled.PooledDataSource; import org.apache.ibatis.mapping.Environment; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.apache.ibatis.transaction.TransactionFactory; import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; import org.example.mapper.db1.AccountMapper; import org.example.mapper.db2.UserCashMapper; import javax.sql.DataSource; public class DBFactoryUtils { public static SqlSessionFactory db1SessionFactory() { String dirverClassName = "com.mysql.cj.jdbc.Driver"; String db1Url = "jdbc:mysql://localhost:3306/db1?severTimezone=UTC"; String user = "root"; String password = "ZYMzym111"; DataSource dataSource = new PooledDataSource(dirverClassName, db1Url, user, password); // 更改为Seata的XA数据源 DataSourceProxyXA dataSourceProxyXA = new DataSourceProxyXA(dataSource); TransactionFactory transactionFactory = new JdbcTransactionFactory(); Environment environment = new Environment("development", transactionFactory, dataSourceProxyXA); Configuration configuration = new Configuration(environment); configuration.addMapper(AccountMapper.class); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); return sqlSessionFactory; } public static SqlSessionFactory db2SessionFactory() { String dirverClassName = "com.mysql.cj.jdbc.Driver"; String db2Url = "jdbc:mysql://localhost:3306/db2?severTimezone=UTC"; String user = "root"; String password = "ZYMzym111"; DataSource dataSource = new PooledDataSource(dirverClassName, db2Url, user, password); // 更改为Seata的XA数据源 DataSourceProxyXA dataSourceProxyXA = new DataSourceProxyXA(dataSource); TransactionFactory transactionFactory = new JdbcTransactionFactory(); Environment environment = new Environment("development", transactionFactory, dataSourceProxyXA); Configuration configuration = new Configuration(environment); configuration.addMapper(UserCashMapper.class); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); return sqlSessionFactory; } }
-
初始化TM和RM
在Main类中编写static静态代码块,初始化TM和RM
static { String applicationID = "Main"; String groupName = "default_tx_group"; // 初始化TM TMClient.init(applicationID, groupName); // 初始化RM RMClient.init(applicationID, groupName); }
-
调整调用编码
在seata-java-demo模块中,找到Main类,加入GlobalTransaction事务处理:张三向李四转账100的业务逻辑代码
import io.seata.core.exception.TransactionException; import io.seata.rm.RMClient; import io.seata.tm.TMClient; import io.seata.tm.api.GlobalTransaction; import io.seata.tm.api.GlobalTransactionContext; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.example.mapper.db1.AccountMapper; import org.example.mapper.db2.UserCashMapper; import org.example.utils.DBFactoryUtils; import javax.transaction.xa.XAException; import java.sql.SQLException; public class Main { static { String applicationID = "Main"; String groupName = "default_tx_group"; // 初始化TM TMClient.init(applicationID, groupName); // 初始化RM RMClient.init(applicationID, groupName); } public static void main(String[] args) throws SQLException, XAException, TransactionException { SqlSessionFactory db1Factory = DBFactoryUtils.db1SessionFactory(); SqlSessionFactory db2Factory = DBFactoryUtils.db2SessionFactory(); SqlSession sqlSession1 = db1Factory.openSession(true); SqlSession sqlSession2 = db2Factory.openSession(true); // 声明全局事务 tx GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate(); // 开启事务 tx.begin(); try { sqlSession1.getMapper(AccountMapper.class).reduceMoney(100); sqlSession2.getMapper(UserCashMapper.class).increMoney(100); // 提交事务 tx.commit(); } catch (Exception e) { System.err.println(e); // 回滚事务md tx.rollback(); } finally { sqlSession1.close(); sqlSession2.close(); } } }
5.1.4 原生API 单体项目多数据源 测试
-
启动seata-server服务器端
Windows启动:seata-server.bat
-
执行Main类中的main方法,张三向李四转账100的业务逻辑代码
Java代码执行部分日志
Seata控制台部分日志
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
-- 将张三数据重置为200 update db1.t_account set money=200 where id=1; -- 将李四数据重置为200 update db2.t_user_cash set money=200 where id=2;
执行结果截图:
-
在seata-java-demo模块中,找到Main类,加入异常
import io.seata.core.exception.TransactionException; import io.seata.rm.RMClient; import io.seata.tm.TMClient; import io.seata.tm.api.GlobalTransaction; import io.seata.tm.api.GlobalTransactionContext; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.example.mapper.db1.AccountMapper; import org.example.mapper.db2.UserCashMapper; import org.example.utils.DBFactoryUtils; import javax.transaction.xa.XAException; import java.sql.SQLException; public class Main { static { String applicationID = "Main"; String groupName = "default_tx_group"; // 初始化TM TMClient.init(applicationID, groupName); // 初始化RM RMClient.init(applicationID, groupName); } public static void main(String[] args) throws SQLException, XAException, TransactionException { SqlSessionFactory db1Factory = DBFactoryUtils.db1SessionFactory(); SqlSessionFactory db2Factory = DBFactoryUtils.db2SessionFactory(); SqlSession sqlSession1 = db1Factory.openSession(true); SqlSession sqlSession2 = db2Factory.openSession(true); // 声明全局事务 tx GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate(); // 开启事务 tx.begin(); try { sqlSession1.getMapper(AccountMapper.class).reduceMoney(100); sqlSession2.getMapper(UserCashMapper.class).increMoney(100); //异常事件 int i = 10 / 0; // 提交事务 tx.commit(); } catch (Exception e) { System.err.println(e); // 回滚事务md tx.rollback(); } finally { sqlSession1.close(); sqlSession2.close(); } } }
-
执行Main类中的main方法,张三向李四转账100的业务逻辑代码
Java代码执行部分日志
Seata控制台部分日志
-
查看数据库数据变化
执行之前
执行之后
5.2 Springboot项目 实现 单体项目多数据源的分布式事务
5.2.1 准备阶段
Docker规划:
- 创建两个MySQL数据库容器,名称分别是MySQLDB1和MySQLDB2
- 容器名为MySQLDB1端口对外映射为3307
- 容器名为MySQLDB2端口对外映射为3308
# 查看MySQL镜像
docker search mysql
# 拉取MySQL镜像
docker pull mysql
# 查看拉取的镜像
docker images
# 运行名为MySQLDB1容器名的MySQL镜像
docker run -d --name MySQLDB1 -p3307:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 运行名为MySQLDB2容器名的MySQL镜像
docker run -d --name MySQLDB2 -p3308:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 查看运行的容器
docker ps
- 创建一个seata分布式容器,名称是seata-server
- 容器名为seata-server端口对外映射为7091和8091
# 查看MySQL镜像
docker search seata
# 拉取MySQL镜像
docker pull seataio/seata-server
# 查看拉取的镜像
docker images
# 运行名为seata-server容器名的seataio/seata-server镜像
docker run -d --name seata-server -p7091:7091 -p8091:8091 seataio/seata-server:latest
# 查看运行的容器
docker ps
MySQL规划:
- 在MySQLDB1容器运行的MySQL中创建db1数据库
- 在db1数据库中创建t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- 在db1数据库创建的t_account数据库表中插入数据(1,'张三',200)
- 在MySQLDB2容器运行的MySQL中创建db2数据库
- 在db2数据库中创建t_user_cash数据库表
- t_user_cash数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- 在db2数据库创建的t_user_cash数据库表中插入数据(2,'李四',200)
# 进入MySQLDB1容器的MySQL数据库
docker exec -it MySQLDB1 mysql -u root -p
-- 创建db1数据库
CREATE DATABASE db1;
USE db1;
-- 在db1数据库中创建t_account数据库表
CREATE TABLE `t_account` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`money` int DEFAULT NULL COMMENT '金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息';
-- 在db1数据库创建的t_account数据库表中插入数据(1,'张三',200)
INSERT INTO db1.`t_account` (`id`,`name`,`money`) VALUES (1,'张三',200);
# 进入MySQLDB2容器的MySQL数据库
docker exec -it MySQLDB2 mysql -u root -p
-- 创建db2数据库
CREATE DATABASE db2;
USE db2;
-- 在db2数据库中创建t_user_cash数据库表
CREATE TABLE `t_user_cash` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`money` int DEFAULT NULL COMMENT '金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息';
-- 在db2数据库创建的t_user_cash数据库表中插入数据(2,'李四',200)
INSERT INTO db2.`t_user_cash` (`id`,`name`,`money`) VALUES (2,'李四',200);
项目规划:
- 打开IDEA,创建一个Empty Project空工程,Name输入Seata-Code
- 在刚创建的Seata-Code工程上,右键创建模块,Name输入seata-xa
- 在seata-xa模块中,右键创建模块,Name输入seata-xa-multiresource-demo
执行效果截图:
5.2.2 搭建基础环境
-
在seata-xa模块中,找到pom.xml文件,引入SpringBoot的父工程依赖,并指定编译版本
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.8</version> <relativePath/> </parent> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
-
在seata-xa-multiresource-demo模块中,找到pom.xml文件,引入SpringBoot依赖、MySQL依赖、Mybatis-plus数据操作组件、druid数据库池,并指定编译版本
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.6</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-xa-multiresource-demo模块中,创建application.yml文件,配置端口和多数据源信息
server: port: 8100 spring: datasource: db1: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3307/db1?serverTimezone=UTC&auto username: root password: ZYMzym111 db2: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3308/db2?serverTimezone=UTC username: root password: ZYMzym111 application: name: seata-xa-mutilsource
-
在seata-xa-multiresource-demo模块下,创建config包,新建DBConfig类,初始化多数据源
import com.alibaba.druid.pool.DruidDataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScans; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import javax.sql.DataSource; @SpringBootConfiguration @MapperScans({ @MapperScan(basePackages = "org.example.mapper.db1", sqlSessionFactoryRef = "db1SessionFactory"), @MapperScan(basePackages = "org.example.mapper.db2", sqlSessionFactoryRef = "db2SessionFactory"), }) public class DBConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.db1") public DataSource db1Datasource() { return new DruidDataSource(); } @Bean @ConfigurationProperties(prefix = "spring.datasource.db2") public DataSource db2Datasource() { return new DruidDataSource(); } @Bean public SqlSessionFactory db1SessionFactory(@Qualifier("db1Datasource") DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean.getObject(); } @Bean public SqlSessionFactory db2SessionFactory(@Qualifier("db2Datasource") DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean.getObject(); } }
-
在seata-xa-multiresource-demo模块下,创建的config包中MapperScan中扫描到的两个包(mapper/db1,mapper/db2)。在mapper/db1中,新建AccountMapper接口类,实现转入逻辑;在mapper/db2中,新建UserCashMapper接口类,实现转出逻辑
import org.apache.ibatis.annotations.Update; public interface AccountMapper { @Update("update t_account set money=money-#{money} where id=1") public void reduceMoney(int money); }
import org.apache.ibatis.annotations.Update; public interface UserCashMapper { @Update("update t_user_cash set money=money+${money} where id=2") public void increMoney(int money); }
-
在seata-xa-multiresource-demo模块下,创建service包,新建MoneyService接口类,定义转的方法
public interface MoneyService { public void transfer(int money); }
-
在service包内创建impl包,新建MoneyServiceImpl类,继承接口MoneyService,并实现其接口方法
import org.example.mapper.db1.AccountMapper; import org.example.mapper.db2.UserCashMapper; import org.example.service.MoneyService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MoneyServiceImpl implements MoneyService { @Autowired(required = false) private AccountMapper accountMapper; @Autowired(required = false) private UserCashMapper userCashMapper; @Override public void transfer(int money) { accountMapper.reduceMoney(money); userCashMapper.increMoney(money); } }
-
在seata-xa-multiresource-demo模块下,创建controller包,新建MoneyController类,定义web请求接口的方法
import org.example.service.MoneyService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class MoneyController { @Autowired private MoneyService moneyService; @GetMapping("/transfer") public String transfer(int money) { moneyService.transfer(money); return "success"; } }
-
在seata-xa-multiresource-demo模块下,创建service包,新建MoneyService接口,定义转的方法
public interface MoneyService { public void transfer(int money); }
-
在根包(org.example)下,新建一个XAMutilApplication启动类,编写Springboot启动代码
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class XAMutilApplication { public static void main(String[] args) { SpringApplication.run(XAMutilApplication.class); } }
-
启动Springboot工程,在浏览器页面访问 localhost:8100/transfer?money=100
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB1容器的MySQL数据库 docker exec -it MySQLDB1 mysql -u root -p -- 将张三数据重置为200 update db1.t_account set money=200 where id=1; # 进入MySQLDB2容器的MySQL数据库 docker exec -it MySQLDB2 mysql -u root -p -- 将李四数据重置为200 update db2.t_user_cash set money=200 where id=2;
执行结果截图:
5.2.3 Springboot项目 单体项目多数据源 改造 自动配置(与手动配置,二者选其一)
-
在seata-xa-multiresource-demo模块下,添加 seata-spring-boot-start依赖
<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.6.1</version> </dependency>
-
在seata-xa-multiresource-demo模块下,找到application.yml配置文件,添加seata配置信息
server: port: 8100 spring: datasource: db1: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3307/db1?serverTimezone=UTC&auto username: root password: ZYMzym111 db2: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3308/db2?serverTimezone=UTC username: root password: ZYMzym111 application: name: seata-xa-mutilsource ## seata较为完整的配置信息 #seata: # registry: # type: file # 配置注册中心 没有注册中心,默认file 可以省略 # config: # type: file # 配置配置中心 没有注册中心,默认file 可以省略 # service: # vgroup-mapping: # default_tx_group: default # 分组名称: 集群名称 # grouplist: # 集群名称: seata地址 # default: localhost:8091 # disable-global-transaction: false # 开启全局事务,默认开启 可以省略 # application-id: seata-xa-mutilsource # 指定项目名称,默认取值为spring.application.name # tx-service-group: default_tx_group # 指定 分组名称,配合application-id 一起初始化TM和RM seata: service: vgroup-mapping: default_tx_group: default grouplist: default: localhost:8091 tx-service-group: default_tx_group enable-auto-data-source-proxy: true # 开启自动代理 默认值是true 可以不屑 data-source-proxy-mode: XA # 声明seata使用XA模式解决分布式
-
找到需要事务的方法,开启seata事务注解 @GlobalTransactional
import io.seata.spring.annotation.GlobalTransactional; import org.example.mapper.db1.AccountMapper; import org.example.mapper.db2.UserCashMapper; import org.example.service.MoneyService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MoneyServiceImpl implements MoneyService { @Autowired(required = false) private AccountMapper accountMapper; @Autowired(required = false) private UserCashMapper userCashMapper; @Override @GlobalTransactional public void transfer(int money) { accountMapper.reduceMoney(money); userCashMapper.increMoney(money); } }
5.2.4 Springboot项目 单体项目多数据源 改造 手动配置(与自动配置,二者选其一)
-
在seata-xa-multiresource-demo模块下,添加 seata-spring-boot-start依赖
<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.6.1</version> </dependency>
-
在seata-xa-multiresource-demo模块下,找到application.yml配置文件,添加seata配置信息
server: port: 8100 spring: datasource: db1: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3307/db1?serverTimezone=UTC&auto username: root password: ZYMzym111 db2: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3308/db2?serverTimezone=UTC username: root password: ZYMzym111 application: name: seata-xa-mutilsource seata: service: vgroup-mapping: default_tx_group: default grouplist: default: localhost:8091 tx-service-group: default_tx_group enable-auto-data-source-proxy: false # 开启自动代理 默认值是true 可以不屑 # data-source-proxy-mode: XA # 声明seata使用XA模式解决分布式
-
在seata-xa-multiresource-demo模块下,找到DBConfig配置数据源的类,针对每一个DataSource数据源声明DataSourceProxyXA数据源,并将原本SqlSessionFactory的代理的DataSource数据源改为DataSourceProxyXA数据源
import com.alibaba.druid.pool.DruidDataSource; import io.seata.rm.datasource.xa.DataSourceProxyXA; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScans; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import javax.sql.DataSource; @SpringBootConfiguration @MapperScans({ @MapperScan(basePackages = "org.example.mapper.db1", sqlSessionFactoryRef = "db1SessionFactory"), @MapperScan(basePackages = "org.example.mapper.db2", sqlSessionFactoryRef = "db2SessionFactory"), }) public class DBConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.db1") public DataSource db1Datasource() { return new DruidDataSource(); } @Bean @ConfigurationProperties(prefix = "spring.datasource.db2") public DataSource db2Datasource() { return new DruidDataSource(); } @Bean public DataSourceProxyXA db1DataSourceProxyXA(@Qualifier("db1Datasource") DataSource dataSource) { DataSourceProxyXA dataSourceProxyXA = new DataSourceProxyXA(dataSource); return dataSourceProxyXA; } @Bean public DataSourceProxyXA db2DataSourceProxyXA(@Qualifier("db2Datasource") DataSource dataSource) { DataSourceProxyXA dataSourceProxyXA = new DataSourceProxyXA(dataSource); return dataSourceProxyXA; } @Bean public SqlSessionFactory db1SessionFactory(@Qualifier("db1DataSourceProxyXA") DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean.getObject(); } @Bean public SqlSessionFactory db2SessionFactory(@Qualifier("db2DataSourceProxyXA") DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean.getObject(); } }
-
在seata-xa-multiresource-demo模块下,找到MoneyServiceImpl类的transfer方法,开启seata事务注解 @GlobalTransactional
import io.seata.spring.annotation.GlobalTransactional; import org.example.mapper.db1.AccountMapper; import org.example.mapper.db2.UserCashMapper; import org.example.service.MoneyService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MoneyServiceImpl implements MoneyService { @Autowired(required = false) private AccountMapper accountMapper; @Autowired(required = false) private UserCashMapper userCashMapper; @Override @GlobalTransactional public void transfer(int money) { accountMapper.reduceMoney(money); userCashMapper.increMoney(money); } }
5.2.5 Springboot项目 单体项目多数据源 测试
-
启动Springboot工程,在HTTP请求工具中访问 localhost:8100/transfer?money=100
HTTP请求工具
SpringBoot工程日志
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB1容器的MySQL数据库 docker exec -it MySQLDB1 mysql -u root -p -- 将张三数据重置为200 update db1.t_account set money=200 where id=1; # 进入MySQLDB2容器的MySQL数据库 docker exec -it MySQLDB2 mysql -u root -p -- 将李四数据重置为200 update db2.t_user_cash set money=200 where id=2;
执行结果截图:
-
在seata-xa-multiresource-demo模块下,找到MoneyServiceImpl类的transfer方法,加入异常
import io.seata.spring.annotation.GlobalTransactional; import org.example.mapper.db1.AccountMapper; import org.example.mapper.db2.UserCashMapper; import org.example.service.MoneyService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MoneyServiceImpl implements MoneyService { @Autowired(required = false) private AccountMapper accountMapper; @Autowired(required = false) private UserCashMapper userCashMapper; @Override @GlobalTransactional public void transfer(int money) { accountMapper.reduceMoney(money); // 异常错误 int i = 10 / 0; userCashMapper.increMoney(money); } }
-
重新启动Springboot工程,在HTTP请求工具中访问 localhost:8100/transfer?money=100
HTTP请求工具
SpringBoot工程日志
-
查看数据库数据变化
执行之前
执行之后
5.3 Seata XA模式的原理流程
- 应用(AP)启动时,Seata 会为每个应用实例创建一个全局唯一的 XID。这个 XID 将用于标识和跟踪整个分布式事务 - XID获取方式:RootContext.getXID()。
- 当应用发起一个分布式事务(TM)时,Seata 会在当前事务开始时为该事务生成一个唯一的事务分支 ID(Branch ID),并将 XID 和 Branch ID 关联起来。
- 应用在本地执行数据库操作(RM)时,Seata 会拦截这些操作,并创建一个对应的 Undo Log(回滚日志)。Undo Log 记录了如何回滚当前操作的信息,以便在分布式事务回滚时使用。
- Seata 会将本地操作的执行结果(RM,成功或失败)以及 Branch ID 发送给 Seata 的事务协调器(Transaction Coordinator,TC)。
- TC 收集所有分支事务的执行结果。如果所有分支事务都成功,TC 将向所有参与者(RM)发送 Commit 请求。参与者收到 Commit 请求后,会提交本地事务并删除对应的 Undo Log。
- 如果任何一个分支事务失败,TC 将向所有参与者(RM)发送 Rollback 请求。参与者收到 Rollback 请求后,会根据 Undo Log 回滚本地事务,并删除对应的 Undo Log。
- 应用在完成所有数据库操作后,需要向 Seata 注册一个结束事务的事件。Seata 会根据 XID 找到对应的全局事务,并将其状态设置为已完成。
5.4 Springboot项目 实现 分库分表的分布式事务
5.4.1 准备阶段
Docker规划:
- 创建两个MySQL数据库容器,名称分别是MySQLDB1和MySQLDB2
- 容器名为MySQLDB1端口对外映射为3307
- 容器名为MySQLDB2端口对外映射为3308
# 查看MySQL镜像
docker search mysql
# 拉取MySQL镜像
docker pull mysql
# 查看拉取的镜像
docker images
# 运行名为MySQLDB1容器名的MySQL镜像
docker run -d --name MySQLDB1 -p3307:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 运行名为MySQLDB2容器名的MySQL镜像
docker run -d --name MySQLDB2 -p3308:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 查看运行的容器
docker ps
MySQL规划:
- 在MySQLDB1容器运行的MySQL中创建db1数据库
- 在MySQLDB2容器运行的MySQL中创建db2数据库
- 在db1数据库和db2数据库中创建t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
# 进入MySQLDB1容器的MySQL数据库
docker exec -it MySQLDB1 mysql -u root -p
-- 创建db1数据库
CREATE DATABASE db1;
USE db1;
-- 在db1数据库中创建t_account数据库表
CREATE TABLE `t_account` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`money` int DEFAULT NULL COMMENT '金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息';
# 进入MySQLDB2容器的MySQL数据库
docker exec -it MySQLDB2 mysql -u root -p
-- 创建db2数据库
CREATE DATABASE db2;
USE db2;
-- 在db2数据库中创建t_account数据库表
CREATE TABLE `t_account` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`money` int DEFAULT NULL COMMENT '金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息';
项目规划:
- 打开IDEA,创建一个Empty Project空工程,Name输入Seata-Code
- 在刚创建的Seata-Code工程上,右键创建模块,Name输入seata-xa
- 在seata-xa模块中,右键创建模块,Name输入seata-xa-sharding-demo
执行效果截图:
5.4.2 搭建基础环境
-
在seata-xa模块中,找到pom.xml文件,引入SpringBoot的父工程依赖,并指定编译版本
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.8</version> <relativePath/> </parent> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
-
在seata-xa-sharding-demo模块中,找到pom.xml文件,引入SpringBoot依赖、MySQL依赖、Mybatis-plus数据操作组件、druid数据库池、lombok自动插入依赖、shardingshpere分库分表依赖,并指定编译版本
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.1.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.6</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-xa-sharding-demo模块中,创建application.yml文件,配置端口、多数据源信息、数据源分库策略、MyBatis-plus基础配置
server: port: 8101 spring: application: name: seata-xa-sharding autoconfigure: exclude: - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration shardingsphere: mode: type: Memory datasource: names: db1,db2 db1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3307/db1?serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ZYMzym111 db2: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3308/db2?serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ZYMzym111 rules: sharding: tables: t_account: actual-data-nodes: db$->{1..2}.t_account database-strategy: standard: sharding-column: id sharding-algorithm-name: database-split sharding-algorithms: database-split: type: INLINE props: algorithm-expression: db$->{id % 2 !=0 ? 1:2} props: sql-show: true # 开启Sql日志输出 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
-
在seata-xa-sharding-demo模块下,创建entity包,新建Account实体类,与数据表t_account对应
import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @Data @TableName("t_account") public class Account { private Integer id; private String name; private Integer money; }
-
在seata-xa-sharding-demo模块下,找到main/java目录创建mapper包,新建AccountMapper类,继承BaseMapper,并编写一个插入的Account的接口方法;找到mian/resource目录创建mapper文件夹,新建AccountMapper.xml文件。
main/java目录下mapper包,AccountMapper类
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Insert; import org.example.entity.Account; import org.springframework.stereotype.Repository; @Repository public interface AccountMapper extends BaseMapper<Account> { @Insert("insert into t_account (id,name,money) values (#{id},#{name},#{money})") public void insertAccount(Account account); }
main/resource目录下mapper文件夹,AccountMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.example.mapper.AccountMapper"> </mapper>
-
在seata-xa-sharding-demo模块下,创建service包,新建AccountService接口,定义转和新增的方法
import org.example.entity.Account; import org.springframework.stereotype.Service; @Service public interface AccountService { public void addAccount(Account account); public void transfer(int fromId, int toId, int money); }
-
在service包内创建impl包,新建AccountServiceImpl类,实现接口AccountService,并实现其接口方法
import org.example.entity.Account; import org.example.mapper.AccountMapper; import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountMapper accountMapper; @Override public void addAccount(Account account) { // accountMapper.insertAccount(account); accountMapper.insert(account); } @Override public void transfer(int fromId, int toId, int money) { Account fromAccount = accountMapper.selectById(fromId); Account toAccount = accountMapper.selectById(toId); fromAccount.setMoney(fromAccount.getMoney() - money); toAccount.setMoney(toAccount.getMoney() + money); accountMapper.updateById(fromAccount); accountMapper.updateById(toAccount); } }
-
在seata-xa-sharding-demo模块下,创建controller包,新建AccountController类,定义web请求接口的方法
import org.example.entity.Account; import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class AccountController { @Autowired private AccountService accountService; @PostMapping("addAccount") public String addAccount(Account account) { accountService.addAccount(account); return "success"; } @GetMapping("transfer") public String transfer(int fromId, int toId, int money) { accountService.transfer(fromId, toId, money); return "success"; } }
-
在根包(org.example)下,新建一个XAShardingApplication启动类,编写Springboot启动代码
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class XAMutilApplication { public static void main(String[] args) { SpringApplication.run(XAMutilApplication.class); } }
-
启动Springboot工程
-
在HTTP请求工具中访问addAccount的Post请求,参数分别是(1,'张三',200),(2,'李四',200)
HTTP请求工具
程序控制台
-
在HTTP请求工具中访问tansfer的Get请求
HTTP请求工具
程序控制台
-
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB1容器的MySQL数据库 docker exec -it MySQLDB1 mysql -u root -p -- 将张三数据重置为200 update db1.t_account set money=200 where id=1; # 进入MySQLDB2容器的MySQL数据库 docker exec -it MySQLDB2 mysql -u root -p -- 将李四数据重置为200 update db2.t_account set money=200 where id=2;
执行结果截图:
5.4.3 Springboot项目 分库分表 改造
-
在seata-xa-sharding-demo模块下,添加 XA事务依赖
<!-- 使用XA事务时,需要引入此模块 --> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-transaction-xa-core</artifactId> <version>5.1.0</version> <exclusions> <exclusion> <groupId>com.atomikos</groupId> <artifactId>transactions-jta</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.atomikos</groupId> <artifactId>transactions-jta</artifactId> <version>5.0.8</version> </dependency>
-
在seata-xa-sharding-demo模块下,创建config包,新建ShardingTransactionXAConfig配置类
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; @Configuration @EnableTransactionManagement public class ShardingTransactionXAConfig { @Bean public PlatformTransactionManager txManager(final DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }
-
在seata-xa-sharding-demo模块下,找到AccountServiceImpl类的transfer方法,开启XA事务注解
import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType; import org.apache.shardingsphere.transaction.core.TransactionType; import org.example.entity.Account; import org.example.mapper.AccountMapper; import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountMapper accountMapper; @Override public void addAccount(Account account) { // accountMapper.insertAccount(account); accountMapper.insert(account); } @Override @Transactional @ShardingSphereTransactionType(TransactionType.XA) public void transfer(int fromId, int toId, int money) { Account fromAccount = accountMapper.selectById(fromId); Account toAccount = accountMapper.selectById(toId); fromAccount.setMoney(fromAccount.getMoney() - money); toAccount.setMoney(toAccount.getMoney() + money); accountMapper.updateById(fromAccount); int i = 10 / 0; accountMapper.updateById(toAccount); } }
5.4.4 Springboot项目 分库分表 测试
-
启动Springboot工程,在HTTP请求工具中访问 localhost:8101/transfer 的Get请求
HTTP请求工具
SpringBoot工程日志
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB1容器的MySQL数据库 docker exec -it MySQLDB1 mysql -u root -p -- 将张三数据重置为200 update db1.t_account set money=200 where id=1; # 进入MySQLDB2容器的MySQL数据库 docker exec -it MySQLDB2 mysql -u root -p -- 将李四数据重置为200 update db2.t_account set money=200 where id=2;
执行结果截图:
-
在seata-xa-sharding-demo模块下,找到AccountServiceImpl类的transfer方法,加入异常
import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType; import org.apache.shardingsphere.transaction.core.TransactionType; import org.example.entity.Account; import org.example.mapper.AccountMapper; import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountMapper accountMapper; @Override public void addAccount(Account account) { // accountMapper.insertAccount(account); accountMapper.insert(account); } @Override @Transactional @ShardingSphereTransactionType(TransactionType.XA) public void transfer(int fromId, int toId, int money) { Account fromAccount = accountMapper.selectById(fromId); Account toAccount = accountMapper.selectById(toId); fromAccount.setMoney(fromAccount.getMoney() - money); toAccount.setMoney(toAccount.getMoney() + money); accountMapper.updateById(fromAccount); // 异常错误 int i = 10 / 0; accountMapper.updateById(toAccount); } }
-
重新启动Springboot工程,在HTTP请求工具中访问 localhost:8101/transfer 的Get请求
HTTP请求工具
SpringBoot工程日志
-
查看数据库数据变化
执行之前
执行之后
5.5 微服务 实现 跨数据库跨服务的分布式事务
5.5.1 准备阶段
Docker规划:
- 创建两个MySQL数据库容器,名称分别是MySQLDB3、MySQLDB4和MySQLDB5
- 容器名为MySQLDB3端口对外映射为3309
- 容器名为MySQLDB4端口对外映射为3310
- 容器名为MySQLDB5端口对外映射为3311
# 查看MySQL镜像
docker search mysql
# 拉取MySQL镜像
docker pull mysql
# 查看拉取的镜像
docker images
# 运行名为MySQLDB3容器名的MySQL镜像
docker run -d --name MySQLDB3 -p3309:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 运行名为MySQLDB4容器名的MySQL镜像
docker run -d --name MySQLDB4 -p3310:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 运行名为MySQLDB5容器名的MySQL镜像
docker run -d --name MySQLDB5 -p3311:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 查看运行的容器
docker ps
- 创建一个seata分布式容器,名称是seata-server
- 容器名为seata-server端口对外映射为7091和8091
# 查看seata镜像
docker search seata
# 拉取seata镜像
docker pull seataio/seata-server
# 查看拉取的镜像
docker images
# 运行名为seata-server容器名的seataio/seata-server镜像
docker run -d --name seata-server -p7091:7091 -p8091:8091 seataio/seata-server:latest
# 查看运行的容器
docker ps
- 创建一个nacos容器,名称是nacos-server
- 容器名为nacos-server端口对外映射为8848、9848和9849
# 查看nacos镜像
docker search nacos
# 拉取nacos镜像
docker pull nacos/nacos-server
# 查看拉取的镜像
docker images
# 运行名为nacos-server容器名的nacos/nacos-server镜像
docker run -d --name nacos-server -p8848:8848 -p9848:9848 -p9849:9849 -e MODE=standalone -e JVM_XMS=512m -e JVM_XMX=512m -e JVM_XMN=256m nacos/nacos-server:latest
# 查看运行的容器
docker ps
MySQL规划:
- 在MySQLDB3容器运行的MySQL中创建db3数据库
- 在MySQLDB4容器运行的MySQL中创建db4数据库
- 在MySQLDB5容器运行的MySQL中创建db5数据库
- 在db3数据库中创建t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- 在db4数据库中创建t_good数据库表
- t_good数据库表中字段:id、name、price,stock四个字段,类型分别是:bigint、varchar(30)、double、bigint
- 在db5数据库中创建t_order数据库表
- t_order数据库表中字段:id、account_id、good_id,good_count,status,all_pay六个字段,类型分别是:bigint、bigint、bigint、bigint、varchar(30)、double
- db3数据库的t_account数据库表中插入数据:(1,'张三',200)
- db4数据库的t_good数据库表中插入数据:(1,'玩具小车',80,200)
# 进入MySQLDB3容器的MySQL数据库
docker exec -it MySQLDB3 mysql -u root -p
-- 创建db3数据库
CREATE DATABASE db3;
USE db3;
-- 在db3数据库中创建t_account数据库表
CREATE TABLE `t_account` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`money` int DEFAULT NULL COMMENT '金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息';
-- 在db3数据库的t_account数据库表中插入数据
INSERT INTO db3.`t_account` (`id`,`name`,`money`) VALUES (1,'张三',200);
# 进入MySQLDB4容器的MySQL数据库
docker exec -it MySQLDB4 mysql -u root -p
-- 创建db4数据库
CREATE DATABASE db4;
USE db4;
-- 在db4数据库中创建t_good数据库表
CREATE TABLE `t_good` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '商品名',
`price` double DEFAULT NULL COMMENT '价格',
`stock` bigint DEFAULT NULL COMMENT '库存'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品信息';
-- 在db4数据库的t_good数据库表中插入数据
INSERT INTO db4.`t_good` (`id`,`name`,`price`,`stock`) VALUES (1,'玩具小车',80,200);
# 进入MySQLDB5容器的MySQL数据库
docker exec -it MySQLDB5 mysql -u root -p
-- 创建db5数据库
CREATE DATABASE db5;
USE db5;
-- 在db5数据库中创建t_order数据库表
CREATE TABLE `t_order` (
`id` bigint NOT NULL COMMENT '主键',
`account_id` bigint NOT NULL COMMENT '账户主键',
`good_id` bigint NOT NULL COMMENT '商品主键',
`good_count` bigint NOT NULL COMMENT '商品数量',
`status` varchar(30) DEFAULT NULL COMMENT '订单状态',
`all_pay` double DEFAULT NULL COMMENT '支付总金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单信息';
项目规划:
- 打开IDEA,创建一个Empty Project空工程,Name输入Seata-Code
- 在刚创建的Seata-Code工程上,右键创建模块,Name输入seata-xa
- 在seata-xa模块中,右键创建模块,Name输入seata-xa-springcloud-demo,注意不依赖于seata-xa模块
- 在seata-xa-springcloud-demo模块下,创建seata-xa-springcloud-common模块,作为公共实体类模块
- 在seata-xa-springcloud-demo模块下,创建seata-xa-springcloud-order模块,作为订单模块
- 在seata-xa-springcloud-demo模块下,创建seata-xa-springcloud-good模块,作为商品模块
- 在seata-xa-springcloud-demo模块下,创建seata-xa-springcloud-account模块,作为用户模块
- 在seata-xa-springcloud-demo模块下,创建seata-xa-springcloud-business模块,作为业务模块
执行效果截图:
5.5.2 搭建基础环境
搭建seata-xa-springcloud-demo模块,作为父级工程
-
在seata-xa-springcloud-demo模块中,找到pom.xml文件,引入SpringBoot的父工程依赖、SpringCloud依赖、SpringCloudAlibaba依赖
<groupId>org.example</groupId> <artifactId>seata-xa-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <modules> <module>seata-xa-springcloud-common</module> <module>seata-xa-springcloud-order</module> <module>seata-xa-springcloud-good</module> <module>seata-xa-springcloud-account</module> <module>seata-xa-springcloud-business</module> </modules> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <maven.compiler.compilerVersion>11</maven.compiler.compilerVersion> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring-cloud.version>Hoxton.SR12</spring-cloud.version> <spring-cloud-alibaba.version>2.2.9.RELEASE</spring-cloud-alibaba.version> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.12.RELEASE</version> <relativePath/> </parent> <dependencyManagement> <dependencies> <!--SpringCloud依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--SpringCloudAlibaba依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
搭建seata-xa-springcloud-common模块,作为公共实体类模块
-
在seata-xa-springcloud-common模块中,找到pom.xml文件,引入lombok自动插入依赖、Mybatis-plus数据操作组件
<parent> <groupId>org.example</groupId> <artifactId>seata-xa-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-xa-springcloud-common</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>
-
在seata-xa-springcloud-common模块下,创建entity包,新建Account实体类,与数据表t_account对应;新建Good实体类,与数据表t_good对应;新建Order实体类,与数据表t_order对应;
新建Account实体类,与数据表t_account对应
import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) @TableName("t_account") public class Account { private Integer id; private String name; private Integer money; }
新建Good实体类,与数据表t_good对应
import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) @TableName("t_good") public class Good { private Integer id; private String name; private double price; private int stock; }
新建Order实体类,与数据表t_order对应
import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) @TableName("t_order") public class Order { private Integer id; private Integer accountId; private Integer goodId; private Integer goodCount; private String status; private double allPay; }
搭建seata-xa-springcloud-order模块,作为订单模块
-
在seata-xa-springcloud-order模块中,找到pom.xml文件,引入SpringBoot的Web依赖、MySQL连接依赖、seata-xa-springcloud-common公共模块依赖、Nacos注册发现依赖
<parent> <groupId>org.example</groupId> <artifactId>seata-xa-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-xa-springcloud-order</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--nacos 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>seata-xa-springcloud-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-xa-springcloud-order模块中,创建application.yml文件,配置端口、数据源信息、MyBatis-plus基础配置、Nacos注册发现地址
server: port: 8201 spring: application: name: seata-xa-springcloud-order datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3311/db5?serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ZYMzym111 cloud: nacos: server-addr: localhost:8848 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true
-
在seata-xa-springcloud-order模块下,找到main/java目录创建mapper包,新建OrderMapper类,继承BaseMapper;找到mian/resource目录创建mapper文件夹,新建OrderMapper.xml文件
main/java目录下mapper包,OrderMapper类
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.example.entity.Order; import org.springframework.stereotype.Repository; @Repository public interface OrderMapper extends BaseMapper<Order> { }
main/resource目录下mapper文件夹,OrderMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.example.mapper.OrderMapper"> </mapper>
-
在seata-xa-springcloud-order模块下,创建service包,新建OrderService接口,定义新增的方法
import com.baomidou.mybatisplus.extension.service.IService; import org.example.entity.Order; import org.springframework.stereotype.Service; @Service public interface OrderService extends IService<Order> { public void addOrder(Order order); }
-
在service包内创建impl包,新建OrderServiceImpl类,实现接口OrderService,并实现其接口方法
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.example.entity.Order; import org.example.mapper.OrderMapper; import org.example.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Random; @Service public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService { @Autowired private OrderMapper orderMapper; @Override public void addOrder(Order order) { if (order.getId() == null) { Random rand = new Random(); int temp = rand.nextInt(100000); order.setId(temp); } orderMapper.insert(order); } }
-
在seata-xa-springcloud-order模块下,创建controller包,新建OrderController类,定义web请求接口的方法
import org.example.entity.Order; import org.example.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("order") public class OrderController { @Autowired private OrderService orderService; @PostMapping public String addOrder(@RequestBody Order order) { orderService.addOrder(order); return "success"; } }
-
在根包(org.example)下,新建一个XAOrderApplication启动类,编写Springboot启动代码
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan(basePackages = {"org.example.mapper"}) public class XAOrderApplication { public static void main(String[] args) { SpringApplication.run(XAOrderApplication.class, args); } }
-
启动Springboot工程
在HTTP请求工具中访问order的Post请求,参数分别是:{ "id": 1, "accountId": 1, "goodId": 1, "goodCount": 2 }
HTTP请求工具
程序控制台
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB5容器的MySQL数据库 docker exec -it MySQLDB5 mysql -u root -p -- 清空t_order表数据 truncate db5.t_order;
执行结果截图:
搭建seata-xa-springcloud-good模块,作为商品模块
-
在seata-xa-springcloud-good模块中,找到pom.xml文件,引入SpringBoot的Web依赖、MySQL连接依赖、seata-xa-springcloud-common公共模块依赖、Nacos注册发现依赖
<parent> <groupId>org.example</groupId> <artifactId>seata-xa-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-xa-springcloud-good</artifactId> <dependencies> <!--nacos 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>seata-xa-springcloud-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-xa-springcloud-good模块中,创建application.yml文件,配置端口、数据源信息、MyBatis-plus基础配置、Nacos注册发现地址
server: port: 8202 spring: application: name: seata-xa-springcloud-good datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3310/db4?serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ZYMzym111 cloud: nacos: server-addr: localhost:8848 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true
-
在seata-xa-springcloud-good模块下,找到main/java目录创建mapper包,新建GoodMapper类,继承BaseMapper;找到mian/resource目录创建mapper文件夹,新建GoodMapper.xml文件。
main/java目录下mapper包,GoodMapper类
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; import org.example.entity.Good; import org.springframework.stereotype.Repository; @Repository public interface GoodMapper extends BaseMapper<Good> { @Update("update t_good set stock=stock-#{stock} where id=#{id}") public void updateGoodStock(@Param("stock") Integer stock, @Param("id") Integer id); }
main/resource目录下mapper文件夹,GoodMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.example.mapper.GoodMapper"> </mapper>
-
在seata-xa-springcloud-good模块下,创建service包,新建GoodService接口,定义新增的方法
import com.baomidou.mybatisplus.extension.service.IService; import org.example.entity.Good; import org.springframework.stereotype.Service; @Service public interface GoodService extends IService<Good> { public void reduceGoodStock(int num, int goodId); }
-
在service包内创建impl包,新建GoodServiceImpl类,实现接口GoodService,并实现其接口方法
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.example.entity.Good; import org.example.mapper.GoodMapper; import org.example.service.GoodService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class GoodServiceImpl extends ServiceImpl<GoodMapper, Good> implements GoodService { @Autowired private GoodMapper goodMapper; @Override public void reduceGoodStock(int num, int goodId) { Good good = goodMapper.selectById(goodId); if (good.getStock() < num) { throw new RuntimeException("Good库存不足"); } goodMapper.updateGoodStock(num, goodId); } }
-
在seata-xa-springcloud-good模块下,创建controller包,新建GoodController类,定义web请求接口的方法
import org.example.entity.Good; import org.example.service.GoodService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("good") public class GoodController { @Autowired private GoodService goodService; @GetMapping("{id}") public Good findGoodById(@PathVariable("id") Integer id) { Good good = goodService.getById(id); return good; } @PutMapping public String reduceGoodStock(Integer num, Integer goodId) { goodService.reduceGoodStock(num, goodId); return "success"; } }
-
在根包(org.example)下,新建一个XAGoodApplication启动类,编写Springboot启动代码
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan(basePackages = {"org.example.mapper"}) public class XAGoodApplication { public static void main(String[] args) { SpringApplication.run(XAGoodApplication.class, args); } }
-
启动Springboot工程
-
在HTTP请求工具中访问good的Get请求,参数是:1(该访问数据前后不会发生变化)
HTTP请求工具
程序控制台
-
-
在HTTP请求工具中访问good的Put请求,参数是:num=2&goodId=1(该访问数据前后会发生变化)
HTTP请求工具
程序控制台
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB4容器的MySQL数据库 docker exec -it MySQLDB4 mysql -u root -p -- 还原t_good表数据 update db4.t_good set stock=200 where id=1;
执行结果截图:
搭建seata-xa-springcloud-account模块,作为用户模块
-
在seata-xa-springcloud-account模块中,找到pom.xml文件,引入SpringBoot的Web依赖、MySQL连接依赖、seata-xa-springcloud-common公共模块依赖、Nacos注册发现依赖
<parent> <groupId>org.example</groupId> <artifactId>seata-xa-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-xa-springcloud-account</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--nacos 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>seata-xa-springcloud-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-xa-springcloud-account模块中,创建application.yml文件,配置端口、数据源信息、MyBatis-plus基础配置、Nacos注册发现地址
server: port: 8203 spring: application: name: seata-xa-springcloud-account datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3309/db3?serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ZYMzym111 cloud: nacos: server-addr: localhost:8848 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true
-
在seata-xa-springcloud-account模块下,找到main/java目录创建mapper包,新建AccountMapper类,继承BaseMapper;找到mian/resource目录创建mapper文件夹,新建AccountMapper.xml文件。
main/java目录下mapper包,AccountMapper类
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; import org.example.entity.Account; import org.springframework.stereotype.Repository; @Repository public interface AccountMapper extends BaseMapper<Account> { @Update("update t_account set money=money-#{money} where id=#{id}") public void reduceAccountMoney(@Param("money") double money, @Param("id") Integer id); }
main/resource目录下mapper文件夹,AccountMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.example.mapper.AccountMapper"> </mapper>
-
在seata-xa-springcloud-account模块下,创建service包,新建AccountService接口,定义新增的方法
import com.baomidou.mybatisplus.extension.service.IService; import org.example.entity.Account; import org.springframework.stereotype.Service; @Service public interface AccountService extends IService<Account> { public void reduceAccountMoney(double money, Integer id); }
-
在service包内创建impl包,新建AccountServiceImpl类,实现接口AccountService,并实现其接口方法
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.example.entity.Account; import org.example.mapper.AccountMapper; import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService { @Autowired private AccountMapper accountMapper; @Override public void reduceAccountMoney(double money, Integer id) { Account account = accountMapper.selectById(id); if (account.getMoney() < money) { throw new RuntimeException("account 账户余额不足"); } accountMapper.reduceAccountMoney(money, id); } }
-
在seata-xa-springcloud-account模块下,创建controller包,新建AccountController类,定义web请求接口的方法
import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("account") public class AccountController { @Autowired private AccountService accountService; @PutMapping public String reduceAccountMoney(double money, Integer id) { accountService.reduceAccountMoney(money, id); return "success"; } }
-
在根包(org.example)下,新建一个XAAccountApplication启动类,编写Springboot启动代码
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan(basePackages = {"org.example.mapper"}) public class XAAccountApplication { public static void main(String[] args) { SpringApplication.run(XAAccountApplication.class, args); } }
-
启动Springboot工程
在HTTP请求工具中访问account的Put请求,参数是:money=100&id=1
HTTP请求工具
程序控制台
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB3容器的MySQL数据库 docker exec -it MySQLDB3 mysql -u root -p -- 还原t_account表数据 update db3.t_account set money=200 where id=1;
执行结果截图:
搭建seata-xa-springcloud-business模块,作为业务模块
-
在seata-xa-springcloud-business模块中,找到pom.xml文件,引入SpringBoot的Web依赖、seata-xa-springcloud-common公共模块依赖、Nacos注册发现依赖
<parent> <groupId>org.example</groupId> <artifactId>seata-xa-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-xa-springcloud-business</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--nacos 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>seata-xa-springcloud-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-xa-springcloud-business模块中,创建application.yml文件,配置端口、Nacos注册发现地址
server: port: 8204 spring: application: name: seata-xa-springcloud-business cloud: nacos: server-addr: localhost:8848
-
在seata-xa-springcloud-bussiness模块下,创建utils包,新建OrderStatus类和URL类,用于标识订单状态和URL请求路径。
OrderStatus类
public enum OrderStatus { CREATE,UPDATING,FINISH }
URL类
public class URL { public static final String CREATE_ORDER = "http://seata-xa-springcloud-order/order"; public static final String GOOD_INFO = "http://seata-xa-springcloud-good/good/%d"; public static final String GOOD_REDUCE = "http://seata-xa-springcloud-good/good?num=%d&goodId=%d"; public static final String ACCOUNT_REDUCE = "http://seata-xa-springcloud-account/account?money=%f&id=%d"; }
-
在seata-xa-springcloud-business模块下,创建service包,新建BusinessService接口,定义新增的方法
import org.springframework.stereotype.Service; @Service public interface BusinessService { public void placeOrder(Integer accountId,Integer goodId,Integer num); }
-
在service包内创建impl包,新建BusinessServiceImpl类,实现接口BusinessService,并实现其接口方法
import org.example.entity.Good; import org.example.entity.Order; import org.example.service.BusinessService; import org.example.utils.OrderStatus; import org.example.utils.URL; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class BusinessServiceImpl implements BusinessService { @Autowired private RestTemplate restTemplate; @Override public void placeOrder(Integer accountId, Integer goodId, Integer num) { // 查询商品信息 Good good = restTemplate.getForObject(String.format(URL.GOOD_INFO, goodId), Good.class); double allPay = good.getPrice() * num; Order order = new Order().setStatus(OrderStatus.CREATE.name()).setAccountId(accountId).setGoodId(goodId).setGoodCount(num).setAllPay(allPay); // 下订单 HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); HttpEntity<Order> httpEntity = new HttpEntity<>(order, httpHeaders); restTemplate.postForObject(URL.CREATE_ORDER, httpEntity, String.class); // 减库存 restTemplate.put(String.format(URL.GOOD_REDUCE, num, goodId), null); // 扣钱 restTemplate.put(String.format(URL.ACCOUNT_REDUCE, allPay, accountId), null); } }
-
在seata-xa-springcloud-business模块下,创建controller包,新建BusinessController类,定义web请求接口的方法
import org.example.service.BusinessService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class BusinessController { @Autowired private BusinessService businessService; @GetMapping("placeOrder") public String placeOrder(Integer accountId, Integer goodId, Integer num) { businessService.placeOrder(accountId, goodId, num); return "success"; } }
-
在根包(org.example)下,新建一个XABusinessApplication启动类,编写Springboot启动代码
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication(exclude= {DataSourceAutoConfiguration.class}) public class XABusinessApplication { public static void main(String[] args) { SpringApplication.run(XABusinessApplication.class, args); } @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }
-
启动Springboot工程
在HTTP请求工具中访问placeOrder的Get请求,参数是:accountId=1&goodId=1&num=1
HTTP请求工具
Business程序控制台
Order程序控制台
Good程序控制台
Account程序控制台
-
查看数据库数据变化
执行之前
order数据库
good数据库
account数据库
执行之后
order数据库
good数据库
account数据库
-
恢复数据库原始数据
# 进入MySQLDB3容器的MySQL数据库 docker exec -it MySQLDB3 mysql -u root -p -- 还原t_account表数据 update db3.t_account set money=200 where id=1; # 进入MySQLDB4容器的MySQL数据库 docker exec -it MySQLDB4 mysql -u root -p -- 还原t_good表数据 update db4.t_good set stock=200 where id=1; # 进入MySQLDB5容器的MySQL数据库 docker exec -it MySQLDB5 mysql -u root -p -- 清空t_order表数据 truncate db5.t_order;
执行结果截图:
5.5.3 微服务 跨数据库跨服务 改造 spring-boot-starter-web包解决(与spring-cloud-starter-alibaba-seata,二者选其一)
-
在seata-xa-springcloud-common模块中,找到pom.xml文件,加入seata-spring-boot-starter包依赖
<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.6.1</version> </dependency>
-
在seata-xa-springcloud-order、seata-xa-springcloud-good、seata-xa-springcloud-account、seata-xa-springcloud-business模块中,找到application.yml文件,文件中加入Seata配置信息
seata: service: vgroup-mapping: default_tx_group: default grouplist: default: localhost:8091 tx-service-group: default_tx_group enable-auto-data-source-proxy: true # 开启自动代理 默认值是true 可以不屑 data-source-proxy-mode: XA # 声明seata使用XA模式解决分布式
-
在seata-xa-springcloud-business模块中,创建intercrptors包,新建XIDInterceptor类拦截器,拦截所有的RestTemplate请求,将含有XID分布式事务注册信息的内容加入头字段中
import io.seata.core.context.RootContext; import org.apache.commons.lang.StringUtils; import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import java.io.IOException; public class XIDInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException { if (!StringUtils.isEmpty(RootContext.getXID())) { httpRequest.getHeaders().add(RootContext.KEY_XID, RootContext.getXID()); } return clientHttpRequestExecution.execute(httpRequest, bytes); } }
-
在seata-xa-springcloud-order、seata-xa-springcloud-good、seata-xa-springcloud-account模块中,创建intercrptors包,新建ReceiveXIDInterceptor类拦截器,处理接收到的请求
import io.seata.core.context.RootContext; import org.apache.commons.lang.StringUtils; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Component public class ReceiveXIDInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String header = request.getHeader(RootContext.KEY_XID); if (!StringUtils.isEmpty(header)) { RootContext.bind(header); } return true; } }
-
在seata-xa-springcloud-order、seata-xa-springcloud-good、seata-xa-springcloud-account模块中,创建config包,新建DBConfig类,更改数据源连接为Druid
import com.alibaba.druid.pool.DruidDataSource; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class DBConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return new DruidDataSource(); } }
-
在seata-xa-springcloud-business模块中,找到BusinessServiceImpl类的placeOrder方法,加入@GlobalTransactional注解
import io.seata.core.context.RootContext; import io.seata.spring.annotation.GlobalTransactional; import org.example.entity.Good; import org.example.entity.Order; import org.example.feign.AccountFeignClient; import org.example.feign.GoodFeignClient; import org.example.feign.OrderFeignClient; import org.example.service.BusinessService; import org.example.utils.OrderStatus; import org.example.utils.URL; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class BusinessServiceImpl implements BusinessService { @Autowired private RestTemplate restTemplate; @GlobalTransactional @Override public void placeOrder(Integer accountId, Integer goodId, Integer num) { System.out.println(RootContext.inGlobalTransaction()); System.out.println(RootContext.getXID()); System.out.println(RootContext.getBranchType()); // 查询商品信息 Good good = restTemplate.getForObject(String.format(URL.GOOD_INFO, goodId), Good.class); double allPay = good.getPrice() * num; Order order = new Order().setStatus(OrderStatus.CREATE.name()).setAccountId(accountId).setGoodId(goodId).setGoodCount(num).setAllPay(allPay); // 下订单 HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); HttpEntity<Order> httpEntity = new HttpEntity<>(order, httpHeaders); restTemplate.postForObject(URL.CREATE_ORDER, httpEntity, String.class); // 减库存 restTemplate.put(String.format(URL.GOOD_REDUCE, num, goodId), null); // 扣钱 restTemplate.put(String.format(URL.ACCOUNT_REDUCE, allPay, accountId), null); } }
-
在seata-xa-springcloud-demo模块下,所有模块的pom.xml文件中加入java版本
<properties> <java.version>11</java.version> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
-
运行模块加入配置,Modify options → Add VM options
--add-opens java.base/java.lang=ALL-UNNAMED
5.5.4 微服务 跨数据库跨服务 改造 spring-cloud-starter-alibaba-seata包解决(与seata-spring-boot-starter,二者选其一)
-
在seata-xa-springcloud-common模块中,找到pom.xml文件,加入spring-cloud-starter-alibaba-seata包依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
-
在seata-xa-springcloud-order、seata-xa-springcloud-good、seata-xa-springcloud-account、seata-xa-springcloud-business需要使用Seata的模块的application.yml,文件中加入Seata配置信息
seata: service: vgroup-mapping: default_tx_group: default grouplist: default: localhost:8091 tx-service-group: default_tx_group enable-auto-data-source-proxy: true # 开启自动代理 默认值是true 可以不屑 data-source-proxy-mode: XA # 声明seata使用XA模式解决分布式
-
在seata-xa-springcloud-order、seata-xa-springcloud-good、seata-xa-springcloud-account模块中,创建config包,新建DBConfig类,更改数据源连接为Druid
import com.alibaba.druid.pool.DruidDataSource; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class DBConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return new DruidDataSource(); } }
-
在seata-xa-springcloud-business模块中,找到BusinessServiceImpl类的placeOrder方法,加入@GlobalTransactional注解
import io.seata.core.context.RootContext; import io.seata.spring.annotation.GlobalTransactional; import org.example.entity.Good; import org.example.entity.Order; import org.example.feign.AccountFeignClient; import org.example.feign.GoodFeignClient; import org.example.feign.OrderFeignClient; import org.example.service.BusinessService; import org.example.utils.OrderStatus; import org.example.utils.URL; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class BusinessServiceImpl implements BusinessService { @Autowired private RestTemplate restTemplate; @GlobalTransactional @Override public void placeOrder(Integer accountId, Integer goodId, Integer num) { System.out.println(RootContext.inGlobalTransaction()); System.out.println(RootContext.getXID()); System.out.println(RootContext.getBranchType()); // 查询商品信息 Good good = restTemplate.getForObject(String.format(URL.GOOD_INFO, goodId), Good.class); double allPay = good.getPrice() * num; Order order = new Order().setStatus(OrderStatus.CREATE.name()).setAccountId(accountId).setGoodId(goodId).setGoodCount(num).setAllPay(allPay); // 下订单 HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); HttpEntity<Order> httpEntity = new HttpEntity<>(order, httpHeaders); restTemplate.postForObject(URL.CREATE_ORDER, httpEntity, String.class); // 减库存 restTemplate.put(String.format(URL.GOOD_REDUCE, num, goodId), null); // 扣钱 restTemplate.put(String.format(URL.ACCOUNT_REDUCE, allPay, accountId), null); } }
-
在seata-xa-springcloud-demo模块下,所有模块的pom.xml文件中加入java版本
<properties> <java.version>11</java.version> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
-
运行模块加入配置,Modify options → Add VM options
--add-opens java.base/java.lang=ALL-UNNAMED
5.5.5 微服务 跨数据库跨服务 改造 RestTemplate请求改造为OpenFeign请求(可选)
-
在seata-xa-springcloud-business模块中,找到pom.xml文件,引入spring-cloud-starter-openfeign依赖
<!--openfeign依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
在seata-xa-springcloud-business模块的application.yml中,配置OpenFeign参数
spring: main: allow-bean-definition-overriding: true feign: client: config: default: connect-timeout: 5000 read-timeout: 5000
-
在seata-xa-springcloud-business模块中,创建feign包,新建AccountFeignClient接口类、GoodFeignClient接口类、OrderFeignClient接口类
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @FeignClient("seata-xa-springcloud-account") @RequestMapping("account") public interface AccountFeignClient { @PutMapping public String reduceAccountMoney(@RequestParam("money") double money, @RequestParam("id") Integer id); }
import org.example.entity.Good; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.*; @FeignClient("seata-xa-springcloud-good") @RequestMapping("good") public interface GoodFeignClient { @GetMapping("{id}") public Good findGoodById(@PathVariable("id") Integer id); @PutMapping public String reduceGoodStock(@RequestParam("num") Integer num,@RequestParam("goodId") Integer goodId); }
import org.example.entity.Order; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @FeignClient("seata-xa-springcloud-order") @RequestMapping("order") public interface OrderFeignClient { @PostMapping public String addOrder(@RequestBody Order order); }
-
在seata-xa-springcloud-business模块中,调整服务间调用的方式,BusinessServiceImpl接口类
import io.seata.core.context.RootContext; import io.seata.spring.annotation.GlobalTransactional; import org.example.entity.Good; import org.example.entity.Order; import org.example.feign.AccountFeignClient; import org.example.feign.GoodFeignClient; import org.example.feign.OrderFeignClient; import org.example.service.BusinessService; import org.example.utils.OrderStatus; import org.example.utils.URL; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class BusinessServiceImpl implements BusinessService { // @Autowired // private RestTemplate restTemplate; @Autowired private AccountFeignClient accountFeignClient; @Autowired private GoodFeignClient goodFeignClient; @Autowired private OrderFeignClient orderFeignClient; @GlobalTransactional @Override public void placeOrder(Integer accountId, Integer goodId, Integer num) { System.out.println(RootContext.inGlobalTransaction()); System.out.println(RootContext.getXID()); System.out.println(RootContext.getBranchType()); Good good = goodFeignClient.findGoodById(goodId); double allPay = good.getPrice() * num; Order order = new Order().setStatus(OrderStatus.CREATE.name()).setAccountId(accountId).setGoodId(goodId).setGoodCount(num).setAllPay(allPay); orderFeignClient.addOrder(order); goodFeignClient.reduceGoodStock(num, goodId); accountFeignClient.reduceAccountMoney(allPay, accountId); // 查询商品信息 // Good good = restTemplate.getForObject(String.format(URL.GOOD_INFO, goodId), Good.class); // double allPay = good.getPrice() * num; // Order order = new Order().setStatus(OrderStatus.CREATE.name()).setAccountId(accountId).setGoodId(goodId).setGoodCount(num).setAllPay(allPay); // 下订单 // HttpHeaders httpHeaders = new HttpHeaders(); // httpHeaders.setContentType(MediaType.APPLICATION_JSON); // HttpEntity<Order> httpEntity = new HttpEntity<>(order, httpHeaders); // restTemplate.postForObject(URL.CREATE_ORDER, httpEntity, String.class); // 减库存 // restTemplate.put(String.format(URL.GOOD_REDUCE, num, goodId), null); // 扣钱 // restTemplate.put(String.format(URL.ACCOUNT_REDUCE, allPay, accountId), null); } }
-
开启OpenFeign调用方式,并注释掉RestTemplate的Bean声明
import io.seata.spring.boot.autoconfigure.SeataTCCFenceAutoConfiguration; import org.example.intercrptors.XIDInterceptor; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Bean; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.web.client.RestTemplate; import java.util.List; @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, SeataTCCFenceAutoConfiguration.class}) @EnableFeignClients public class XABusinessApplication { public static void main(String[] args) { SpringApplication.run(XABusinessApplication.class, args); } // @Bean // @LoadBalanced // public RestTemplate restTemplate() { // RestTemplate restTemplate = new RestTemplate(); //// List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors(); //// interceptors.add(new XIDInterceptor()); //// restTemplate.setInterceptors(interceptors); // return restTemplate; // } }
5.5.6 微服务 跨数据库跨服务 改造 全局异常处理改造(可选)
-
在seata-xa-springcloud-business模块中,创建handler包,新建SelfGlobalHandler异常处理类
import io.seata.core.context.RootContext; import io.seata.core.exception.TransactionException; import io.seata.tm.api.GlobalTransactionContext; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class SelfGlobalHandler { @ExceptionHandler(RuntimeException.class) public String handler(RuntimeException e) throws TransactionException { if (RootContext.inGlobalTransaction()) { // 手动回滚分布式事务 GlobalTransactionContext.reload(RootContext.getXID()).rollback(); } return e.getMessage(); } }
5.5.7 微服务 跨数据库跨服务 改造 基于OpenFeign的熔断降级(可选)
-
在seata-xa-springcloud-business模块中,找到pom.xml文件,引入spring-cloud-starter-alibaba-sentinel依赖
<!--sentinel 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
-
在seata-xa-springcloud-business模块中,找到application.yml文件,配置熔断降级的参数
feign: client: config: default: connect-timeout: 5000 read-timeout: 5000 sentinel: enabled: true
-
在seata-xa-springcloud-business模块下的feign包中,创建fallback包。对account进行熔断处理。新建AccountFeignClientSentinelFallback类,实现AccountFeignClient类,将AccountFeignClient类中的@RequestMapping("account")改动到方法上@PutMapping("account")
import org.example.feign.fallback.AccountFeignClientSentinelFallback; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @FeignClient(value = "seata-xa-springcloud-account", fallback = AccountFeignClientSentinelFallback.class) //@RequestMapping("account") public interface AccountFeignClient { @PutMapping("account") public String reduceAccountMoney(@RequestParam("money") double money, @RequestParam("id") Integer id); }
import io.seata.core.context.RootContext; import io.seata.core.exception.TransactionException; import io.seata.tm.api.GlobalTransactionContext; import org.example.feign.AccountFeignClient; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RequestMapping; @Component public class AccountFeignClientSentinelFallback implements AccountFeignClient { @Override public String reduceAccountMoney(double money, Integer id) { if(RootContext.inGlobalTransaction()){ try { GlobalTransactionContext.reload(RootContext.getXID()).rollback(); } catch (TransactionException e) { throw new RuntimeException(e); } } return "Account - 熔断降级"; } }
-
在seata-xa-springcloud-business模块下的feign包中,创建fallback包。对good进行熔断处理。与Account类似
-
在seata-xa-springcloud-business模块下的feign包中,创建fallback包。对order进行熔断处理。与Account类似
5.4.8 微服务 跨数据库跨服务 测试
-
正确请求分布式事务,正确处理
在HTTP请求工具中发送两次placeOrder的Get成功请求,参数为:accountId=1&goodId=1&num=1
HTTP请求工具
business程序控制台
order程序控制台
good程序控制台
account程序控制台
-
查看数据库数据变化
执行之前
order数据库
good数据库
account数据库
执行之后
order数据库
good数据库
account数据库
-
正确请求分布式事务,异常处理
在HTTP请求工具中发送两次placeOrder的Get成功请求,参数为:accountId=1&goodId=1&num=1
HTTP请求工具
business程序控制台
order程序控制台
good程序控制台
account程序控制台
-
查看数据库数据变化
执行之前
order数据库
good数据库
account数据库
执行之后
order数据库
good数据库
account数据库
5.6 Seata 三种依赖包的区别
-
seata-all
最基础的包,没有自动配置,需要使用.conf编写配置信息,适合于JavaSM项目
-
seata-spring-boot-starter
在seata-all的基础上,添加了自动配置,可以将.conf文件中的配置转化为application.yml中
-
spring-cloud-starter-alibaba-seata
在seata-spring-boot-starter的基础上,添加了全局事务ID的传递
5.7 Seata 分布式事务 在微服务场景下失效原因
- 注册分支事务时,没有全局事务ID
- 解决方案一:手动传递事务ID
- 解决方案二:使用spring-cloud-starter-alibaba-seata包
- Seata在某些版本(1.5.2)如果自动代理需要使用Druid数据源可用,但是默认数据源不可用
- 解决方案一:找到可以自动代理默认数据源的版本
- 解决方案二:手动添加代理数据源Druid
- 全局异常处理会导致分布式事务失效(全局异常处理之后,项目中不在会出现显式异常,则TC认为无异常)
- 解决方案一:后台服务不需要处理异常
- 解决方案二:手动回滚异常
- 微服务架构中降级熔断会导致分布式事务消失
- 解决方案:手动回滚
- @GlobalTransactional注解 写在controller中 某些场景下可能不生效(既有本地事务也有分布式事务)\
- 解决方案:将@GlobalTransactional注解放入service层,业务逻辑也放入service层
- 在2问题的基础上,如果在yaml中声明数据源type为Druid数据源,可能也会失效
- 解决方案:手动添加代理数据源Druid
6. 弱一致性:AT
弱一致性(Weak Consistency)是数据库事务处理中的一种概念,指的是在分布式系统中,为了追求更高的性能和可用性,允许系统在处理事务时出现一定程度的数据不一致。
在弱一致性模型下,当多个节点同时进行数据操作时,系统的目标是在最终达到数据一致性,但在某些时刻可能会出现数据的临时不一致。
6.1 什么是AT模式
AT模式是一种基于两阶段提交(2PC)协议的分布式事务处理模式,它通过维护全局事务ID和分支事务ID来确保分布式事务的一致性。与XA模式演化而来。
AT模式的优点是可以在不侵入业务的情况下实现分布式事务处理,具有较好的兼容性和扩展性。然而,它也存在一定的性能开销,因为两阶段提交协议需要协调各个参与者,可能导致事务处理延迟。
6.2 Seata AT模式的原理流程
- 事务发起方(TM)通过Seata客户端向Seata Server注册全局事务,并获取全局事务ID(XID)。
- TM向涉及的资源(RM)发送请求,并携带XID。RM接收到请求后,将XID与本地事务关联,并通知Seata Server。
- RM执行本地事务,如果执行成功,则提交本地事务,并将执行结果上报给Seata Server。如果执行失败,则回滚本地事务,并将回滚结果上报给Seata Server。并写入undo_log日志操作表
- TM收到所有RM的执行结果后,根据全局事务的完成情况,决定全局事务是提交还是回滚。
- TM通知Seata Server提交或回滚全局事务。
- Seata Server根据TM的指令,通知所有涉及的RM提交或回滚本地事务。如果TM是提交指令,则删除undo_log日志。如果TM是回滚操作,undo_log日志根据日志内容恢复原始数据。
AT模式的优点是简单易用,无需手动编写补偿事务。但需要注意的是,它依赖于Seata Server,因此需要保证Seata Server的高可用性。
6.3 Seata AT模式的全局锁
- 全局锁
全局锁由表名和操作记录的主键按照⼀定的规律组成。seata的AT模式中的全局锁保存在TC端(seata-server端) 。TC端保存全局锁可以在如下三个位置 redis mysql file(默认) - 注册全局锁
资源管理器 RM 注册分支事务,执行业务逻辑(操作数据库)。提交事务之前,会把修改的表和主键信息封装成全局锁,发送到 TC 服务器进行注册。如果 TC 服务器发现已经有这个主键的全局锁,证明有其他事务正在执行这条数据,则会抛出全局锁冲突异常,客户端会循环等待并且重试 - 释放全局锁
在第二阶段,异步执行被触发,当事务协调器(TC)向资源管理器(客户端)发送分支提交请求后,客户端将分支提交信息插入内存队列中。这意味着,如果第一阶段成功并且第二阶段没有异常,那么全局锁将在第二阶段开始时立即释放,而不是等到第二阶段执行结束才释放。
7. Seata分布式事务实现AT模式
7.1 Springboot项目 实现 单体项目多数据源的分布式事务
7.1.1 准备阶段
Docker规划:
- 创建两个MySQL数据库容器,名称分别是MySQLDB1和MySQLDB2
- 容器名为MySQLDB1端口对外映射为3307
- 容器名为MySQLDB2端口对外映射为3308
# 查看MySQL镜像
docker search mysql
# 拉取MySQL镜像
docker pull mysql
# 查看拉取的镜像
docker images
# 运行名为MySQLDB1容器名的MySQL镜像
docker run -d --name MySQLDB1 -p3307:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 运行名为MySQLDB2容器名的MySQL镜像
docker run -d --name MySQLDB2 -p3308:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 查看运行的容器
docker ps
- 创建一个seata分布式容器,名称是seata-server
- 容器名为seata-server端口对外映射为7091和8091
# 查看seata镜像
docker search seata
# 拉取seata镜像
docker pull seataio/seata-server
# 查看拉取的镜像
docker images
# 运行名为seata-server容器名的seataio/seata-server镜像
docker run -d --name seata-server -p7091:7091 -p8091:8091 seataio/seata-server:latest
# 查看运行的容器
docker ps
MySQL规划:
- 在MySQLDB1容器运行的MySQL中创建db1数据库
- 在db1数据库中创建t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- 在db1数据库创建的t_account数据库表中插入数据(1,'张三',200)
- 在MySQLDB2容器运行的MySQL中创建db2数据库
- 在db2数据库中创建t_user_cash数据库表
- t_user_cash数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- 在db2数据库创建的t_user_cash数据库表中插入数据(2,'李四',200)
# 进入MySQLDB1容器的MySQL数据库
docker exec -it MySQLDB1 mysql -u root -p
-- 创建db1数据库
CREATE DATABASE db1;
USE db1;
-- 在db1数据库中创建t_account数据库表
CREATE TABLE `t_account` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`money` int DEFAULT NULL COMMENT '金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息';
-- 在db1数据库创建的t_account数据库表中插入数据(1,'张三',200)
INSERT INTO db1.`t_account` (`id`,`name`,`money`) VALUES (1,'张三',200);
# 进入MySQLDB2容器的MySQL数据库
docker exec -it MySQLDB2 mysql -u root -p
-- 创建db2数据库
CREATE DATABASE db2;
USE db2;
-- 在db2数据库中创建t_user_cash数据库表
CREATE TABLE `t_user_cash` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`money` int DEFAULT NULL COMMENT '金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息';
-- 在db2数据库创建的t_user_cash数据库表中插入数据(2,'李四',200)
INSERT INTO db2.`t_user_cash` (`id`,`name`,`money`) VALUES (2,'李四',200);
项目规划:
- 打开IDEA,创建一个Empty Project空工程,Name输入Seata-Code
- 在刚创建的Seata-Code工程上,右键创建模块,Name输入seata-at
- 在seata-at模块中,右键创建模块,Name输入seata-at-multiresource-demo
执行效果截图:
7.1.2 搭建基础环境
-
在seata-at模块中,找到pom.xml文件,引入SpringBoot的父工程依赖,并指定编译版本
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.8</version> <relativePath/> </parent> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
-
在seata-at-multiresource-demo模块中,找到pom.xml文件,引入SpringBoot依赖、MySQL依赖、Mybatis-plus数据操作组件、druid数据库池
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.6</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-at-multiresource-demo模块中,创建application.yml文件,配置端口和多数据源信息
server: port: 8300 spring: datasource: db1: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3307/db1?serverTimezone=UTC&auto username: root password: ZYMzym111 db2: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3308/db2?serverTimezone=UTC username: root password: ZYMzym111 application: name: seata-at-mutilsource
-
在seata-at-multiresource-demo模块下,创建config包,新建DBConfig类初始化多数据源
import com.alibaba.druid.pool.DruidDataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScans; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import javax.sql.DataSource; @SpringBootConfiguration @MapperScans({ @MapperScan(basePackages = "org.example.mapper.db1", sqlSessionFactoryRef = "db1SessionFactory"), @MapperScan(basePackages = "org.example.mapper.db2", sqlSessionFactoryRef = "db2SessionFactory"), }) public class DBConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.db1") public DataSource db1Datasource() { return new DruidDataSource(); } @Bean @ConfigurationProperties(prefix = "spring.datasource.db2") public DataSource db2Datasource() { return new DruidDataSource(); } @Bean public SqlSessionFactory db1SessionFactory(@Qualifier("db1Datasource") DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean.getObject(); } @Bean public SqlSessionFactory db2SessionFactory(@Qualifier("db2Datasource") DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean.getObject(); } }
-
在seata-at-multiresource-demo模块下,创建的config包中MapperScan中扫描到的两个包(mapper/db1,mapper/db2)。在mapper/db1中,新建AccountMapper类,实现转入逻辑;在mapper/db2中,新建UserCashMapper类,实现转出逻辑
import org.apache.ibatis.annotations.Update; public interface AccountMapper { @Update("update t_account set money=money-#{money} where id=1") public void reduceMoney(int money); }
import org.apache.ibatis.annotations.Update; public interface UserCashMapper { @Update("update t_user_cash set money=money+${money} where id=2") public void increMoney(int money); }
-
在seata-at-multiresource-demo模块下,创建service包,新建MoneyService接口,定义转的方法
public interface MoneyService { public void transfer(int money); }
-
在service包内创建impl包,新建MoneyServiceImpl类,继承接口MoneyService,并实现其接口方法
import org.example.mapper.db1.AccountMapper; import org.example.mapper.db2.UserCashMapper; import org.example.service.MoneyService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MoneyServiceImpl implements MoneyService { @Autowired(required = false) private AccountMapper accountMapper; @Autowired(required = false) private UserCashMapper userCashMapper; @Override public void transfer(int money) { accountMapper.reduceMoney(money); userCashMapper.increMoney(money); } }
-
在seata-at-multiresource-demo模块下,创建controller包,新建MoneyController类,定义web请求接口的方法
import org.example.service.MoneyService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class MoneyController { @Autowired private MoneyService moneyService; @GetMapping("/transfer") public String transfer(int money) { moneyService.transfer(money); return "success"; } }
-
在根包(org.example)下,新建一个ATMutilApplication启动类,编写Springboot启动代码
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class XAMutilApplication { public static void main(String[] args) { SpringApplication.run(XAMutilApplication.class); } }
-
启动Springboot工程,使用HTTP请求工具发送Get请求 localhost:8300/transfer?money=100
HTTP请求工具
程序控制台
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB1容器的MySQL数据库 docker exec -it MySQLDB1 mysql -u root -p -- 将张三数据重置为200 update db1.t_account set money=200 where id=1; # 进入MySQLDB2容器的MySQL数据库 docker exec -it MySQLDB2 mysql -u root -p -- 将李四数据重置为200 update db2.t_user_cash set money=200 where id=2;
执行结果截图:
7.1.3 Springboot项目 单体项目多数据源 改造 自动配置(与手动配置,二者选其一)
-
在包含分布式事务的数据库中,加入undo_log表结构
-- 注意此处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;
执行效果截图
-
t_account表和t_user_cash表定义主键(全局锁由表名和操作记录的主键 按照⼀定的规律组成)
-- t_account表定义主键 ALTER TABLE db1.t_account ADD CONSTRAINT t_account_PK PRIMARY KEY (id); --t_user_cash表定义主键 ALTER TABLE db2.t_user_cash ADD CONSTRAINT t_user_cash_PK PRIMARY KEY (id);
-
在seata-at-multiresource-demo模块中,添加seata-spring-boot-start依赖
<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.6.1</version> </dependency>
-
在seata-at-multiresource-demo模块中,找到application.yml配置文件,添加seata配置信息
server: port: 8100 spring: datasource: db1: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3307/db1?serverTimezone=UTC&auto username: root password: ZYMzym111 db2: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3308/db2?serverTimezone=UTC username: root password: ZYMzym111 application: name: seata-xa-mutilsource ## seata较为完整的配置信息 #seata: # registry: # type: file # 配置注册中心 没有注册中心,默认file 可以省略 # config: # type: file # 配置配置中心 没有注册中心,默认file 可以省略 # service: # vgroup-mapping: # default_tx_group: default # 分组名称: 集群名称 # grouplist: # 集群名称: seata地址 # default: localhost:8091 # disable-global-transaction: false # 开启全局事务,默认开启 可以省略 # application-id: seata-xa-mutilsource # 指定项目名称,默认取值为spring.application.name # tx-service-group: default_tx_group # 指定 分组名称,配合application-id 一起初始化TM和RM seata: service: vgroup-mapping: default_tx_group: default grouplist: default: localhost:8091 tx-service-group: default_tx_group enable-auto-data-source-proxy: true # 开启自动代理 默认值是true 可以不屑 data-source-proxy-mode: AT # 声明seata使用AT模式解决分布式
-
在seata-at-multiresource-demo模块中,找到MoneyServiceImpl类的transfer方法,开启seata事务注解 @GlobalTransactional
import io.seata.spring.annotation.GlobalTransactional; import org.example.mapper.db1.AccountMapper; import org.example.mapper.db2.UserCashMapper; import org.example.service.MoneyService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MoneyServiceImpl implements MoneyService { @Autowired(required = false) private AccountMapper accountMapper; @Autowired(required = false) private UserCashMapper userCashMapper; @Override @GlobalTransactional public void transfer(int money) { accountMapper.reduceMoney(money); userCashMapper.increMoney(money); } }
7.1.4 Springboot 单体项目多数据源 改造 手动配置(与自动配置,二者选其一)
-
在包含分布式事务的数据库中,加入undo_log表结构
-- 注意此处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;
执行效果截图
-
t_account表和t_user_cash表定义主键(全局锁由表名和操作记录的主键 按照⼀定的规律组成)
-- t_account表定义主键 ALTER TABLE db1.t_account ADD CONSTRAINT t_account_PK PRIMARY KEY (id); --t_user_cash表定义主键 ALTER TABLE db2.t_user_cash ADD CONSTRAINT t_user_cash_PK PRIMARY KEY (id);
-
在seata-at-multiresource-demo模块中,添加 seata-spring-boot-start依赖
<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.6.1</version> </dependency>
-
在seata-at-multiresource-demo模块中,找到application.yml配置文件,添加seata配置信息
server: port: 8100 spring: datasource: db1: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3307/db1?serverTimezone=UTC&auto username: root password: ZYMzym111 db2: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3308/db2?serverTimezone=UTC username: root password: ZYMzym111 application: name: seata-xa-mutilsource seata: service: vgroup-mapping: default_tx_group: default grouplist: default: localhost:8091 tx-service-group: default_tx_group enable-auto-data-source-proxy: false # 开启自动代理 默认值是true 可以不屑 # data-source-proxy-mode: XA # 声明seata使用XA模式解决分布式
-
在seata-at-multiresource-demo模块中,找到DBConfig配置数据源的类,针对每一个DataSource数据源声明DataSourceProxy数据源,并将原本SqlSessionFactory的代理的DataSource数据源改为DataSourceProxy数据源
import com.alibaba.druid.pool.DruidDataSource; import io.seata.rm.datasource.DataSourceProxy; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScans; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import javax.sql.DataSource; @SpringBootConfiguration @MapperScans({ @MapperScan(basePackages = "org.example.mapper.db1", sqlSessionFactoryRef = "db1SessionFactory"), @MapperScan(basePackages = "org.example.mapper.db2", sqlSessionFactoryRef = "db2SessionFactory"), }) public class DBConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.db1") public DataSource db1Datasource() { return new DruidDataSource(); } @Bean @ConfigurationProperties(prefix = "spring.datasource.db2") public DataSource db2Datasource() { return new DruidDataSource(); } @Bean public DataSource db1XADatasource(@Qualifier("db1Datasource") DataSource dataSource) { DataSourceProxy dataSourceProxy = new DataSourceProxy(dataSource); return dataSourceProxy; } @Bean public DataSource db2XADatasource(@Qualifier("db2Datasource") DataSource dataSource) { DataSourceProxy dataSourceProxy = new DataSourceProxy(dataSource); return dataSourceProxy; } @Bean public SqlSessionFactory db1SessionFactory(@Qualifier("db1XADatasource") DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean.getObject(); } @Bean public SqlSessionFactory db2SessionFactory(@Qualifier("db2XADatasource") DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean.getObject(); } }
-
在seata-at-multiresource-demo模块中,找到MoneyServiceImpl类的transfer方法,开启seata事务注解 @GlobalTransactional
import io.seata.spring.annotation.GlobalTransactional; import org.example.mapper.db1.AccountMapper; import org.example.mapper.db2.UserCashMapper; import org.example.service.MoneyService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MoneyServiceImpl implements MoneyService { @Autowired(required = false) private AccountMapper accountMapper; @Autowired(required = false) private UserCashMapper userCashMapper; @Override @GlobalTransactional public void transfer(int money) { accountMapper.reduceMoney(money); userCashMapper.increMoney(money); } }
7.1.5 Springboot 单体项目多数据源 测试
-
启动Springboot工程,正常业务流程,在HTTP请求工具中访问 localhost:8300/transfer?money=100
HTTP请求工具
SpringBoot工程日志
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB1容器的MySQL数据库 docker exec -it MySQLDB1 mysql -u root -p -- 将张三数据重置为200 update db1.t_account set money=200 where id=1; # 进入MySQLDB2容器的MySQL数据库 docker exec -it MySQLDB2 mysql -u root -p -- 将李四数据重置为200 update db2.t_user_cash set money=200 where id=2;
执行结果截图:
-
在seata-at-multiresource-demo模块中,找到MoneyServiceImpl类的transfer方法,加入异常
import io.seata.spring.annotation.GlobalTransactional; import org.example.mapper.db1.AccountMapper; import org.example.mapper.db2.UserCashMapper; import org.example.service.MoneyService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MoneyServiceImpl implements MoneyService { @Autowired(required = false) private AccountMapper accountMapper; @Autowired(required = false) private UserCashMapper userCashMapper; @Override @GlobalTransactional public void transfer(int money) { accountMapper.reduceMoney(money); // 异常错误 int i = 10 / 0; userCashMapper.increMoney(money); } }
-
重新启动Springboot工程,在HTTP请求工具中访问 localhost:8300/transfer?money=100
HTTP请求工具
SpringBoot工程日志
-
查看数据库数据变化
执行之前
执行之后
7.2 Springboot项目 实现 分库分表的分布式事务
7.2.1 准备阶段
Docker规划:
- 创建两个MySQL数据库容器,名称分别是MySQLDB1和MySQLDB2
- 容器名为MySQLDB1端口对外映射为3307
- 容器名为MySQLDB2端口对外映射为3308
# 查看MySQL镜像
docker search mysql
# 拉取MySQL镜像
docker pull mysql
# 查看拉取的镜像
docker images
# 运行名为MySQLDB1容器名的MySQL镜像
docker run -d --name MySQLDB1 -p3307:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 运行名为MySQLDB2容器名的MySQL镜像
docker run -d --name MySQLDB2 -p3308:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 查看运行的容器
docker ps
MySQL规划:
- 在MySQLDB1容器运行的MySQL中创建db1数据库
- 在MySQLDB2容器运行的MySQL中创建db2数据库
- 在db1数据库和db2数据库中创建t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
# 进入MySQLDB1容器的MySQL数据库
docker exec -it MySQLDB1 mysql -u root -p
-- 创建db1数据库
CREATE DATABASE db1;
USE db1;
-- 在db1数据库中创建t_account数据库表
CREATE TABLE `t_account` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`money` int DEFAULT NULL COMMENT '金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息';
# 进入MySQLDB2容器的MySQL数据库
docker exec -it MySQLDB2 mysql -u root -p
-- 创建db2数据库
CREATE DATABASE db2;
USE db2;
-- 在db2数据库中创建t_account数据库表
CREATE TABLE `t_account` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`money` int DEFAULT NULL COMMENT '金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息';
项目规划:
- 打开IDEA,创建一个Empty Project空工程,Name输入Seata-Code
- 在刚创建的Seata-Code工程上,右键创建模块,Name输入seata-at
- 在seata-at模块中,右键创建模块,Name输入seata-at-sharding-demo
执行效果截图:
7.2.2 搭建基础环境
-
在seata-at模块中,找到pom.xml文件,引入SpringBoot的父工程依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.8</version> <relativePath/> </parent> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
-
在seata-at-sharding-demo模块中,找到pom.xml文件,引入SpringBoot依赖、MySQL依赖、Mybatis-plus数据操作组件、druid数据库池、lombok自动插入依赖、shardingshpere分库分表依赖
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.1.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.6</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-at-sharding-demo模块中,创建application.yml文件,配置端口、多数据源信息、数据源分库策略、MyBatis-plus基础配置
server: port: 8301 spring: application: name: seata-at-sharding autoconfigure: exclude: - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration shardingsphere: mode: type: Memory datasource: names: db1,db2 db1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3307/db1?serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ZYMzym111 db2: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3308/db2?serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ZYMzym111 rules: sharding: tables: t_account: actual-data-nodes: db$->{1..2}.t_account database-strategy: standard: sharding-column: id sharding-algorithm-name: database-split sharding-algorithms: database-split: type: INLINE props: algorithm-expression: db$->{id % 2 !=0 ? 1:2} props: sql-show: true # 开启Sql日志输出 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
-
在seata-at-sharding-demo模块中,创建entity包,新建Account实体类,与数据表t_account对应
import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @Data @TableName("t_account") public class Account { private Integer id; private String name; private Integer money; }
-
在seata-at-sharding-demo模块中,找到main/java目录创建mapper包,新建AccountMapper类,继承BaseMapper,并编写一个插入的Account的接口方法;找到mian/resource目录创建mapper文件夹,新建AccountMapper.xml文件。
main/java目录下mapper包,AccountMapper类
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Insert; import org.example.entity.Account; import org.springframework.stereotype.Repository; @Repository public interface AccountMapper extends BaseMapper<Account> { @Insert("insert into t_account (id,name,money) values (#{id},#{name},#{money})") public void insertAccount(Account account); }
main/resource目录下mapper文件夹,AccountMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.example.mapper.AccountMapper"> </mapper>
-
在seata-at-sharding-demo模块中,创建service包,新建AccountService接口,定义转和新增的方法
import org.example.entity.Account; import org.springframework.stereotype.Service; @Service public interface AccountService { public void addAccount(Account account); public void transfer(int fromId, int toId, int money); }
-
在service包内创建impl包,新建AccountServiceImpl类,实现接口AccountService,并实现其接口方法
import org.example.entity.Account; import org.example.mapper.AccountMapper; import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountMapper accountMapper; @Override public void addAccount(Account account) { // accountMapper.insertAccount(account); accountMapper.insert(account); } @Override public void transfer(int fromId, int toId, int money) { Account fromAccount = accountMapper.selectById(fromId); Account toAccount = accountMapper.selectById(toId); fromAccount.setMoney(fromAccount.getMoney() - money); toAccount.setMoney(toAccount.getMoney() + money); accountMapper.updateById(fromAccount); accountMapper.updateById(toAccount); } }
-
在seata-at-sharding-demo模块中,创建controller包,新建AccountController类,定义web请求接口的方法
import org.example.entity.Account; import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class AccountController { @Autowired private AccountService accountService; @PostMapping("addAccount") public String addAccount(Account account) { accountService.addAccount(account); return "success"; } @GetMapping("transfer") public String transfer(int fromId, int toId, int money) { accountService.transfer(fromId, toId, money); return "success"; } }
-
在根包(org.example)下,新建一个ATShardingApplication启动类,编写Springboot启动代码
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan(basePackages = {"org.example.mapper"}) public class ATShardingApplication { public static void main(String[] args) { SpringApplication.run(ATShardingApplication.class); } }
-
启动Springboot工程
-
在HTTP请求工具中访问addAccount的Post请求,参数分别是(1,'张三',200),(2,'李四',200)
HTTP请求工具
程序控制台
-
-
在HTTP请求工具中访问tansfer的Get请求
HTTP请求工具
程序控制台
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB1容器的MySQL数据库 docker exec -it MySQLDB1 mysql -u root -p -- 将张三数据重置为200 update db1.t_account set money=200 where id=1; # 进入MySQLDB2容器的MySQL数据库 docker exec -it MySQLDB2 mysql -u root -p -- 将李四数据重置为200 update db2.t_account set money=200 where id=2;
执行结果截图:
7.2.3 Springboot项目 分库分表 改造
-
在包含分布式事务的数据库中,加入undo_log表结构
-- 注意此处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;
执行效果截图
-
数据库db1和db2中的t_account表定义主键(全局锁由表名和操作记录的主键 按照⼀定的规律组成)
-- 数据库db1的t_account表定义主键 ALTER TABLE db1.t_account ADD CONSTRAINT t_account_PK PRIMARY KEY (id); --数据库db2的t_account表定义主键 ALTER TABLE db2.t_account ADD CONSTRAINT t_account_PK PRIMARY KEY (id);
-
在seata-at-sharding-demo模块中,找到pom.xml文件,添加 Seata依赖、shardingsphere-transaction-base-seata-at管理器依赖
<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.6.1</version> </dependency> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-transaction-base-seata-at</artifactId> <version>5.1.0</version> </dependency>
-
在seata-at-sharding-demo模块中,application.yml中添加seata依赖
seata: service: vgroup-mapping: default_tx_group: default grouplist: default: 180.76.103.211:8091 tx-service-group: default_tx_group enable-auto-data-source-proxy: false # data-source-proxy-mode: AT
-
在seata-at-sharding-demo模块中,resources资源目录下,编写seata.conf配置文件,供SeataATShardingSphereTransactionManager类使用
sharding.transaction.seata.at.enable=true client.application.id=seata-at-sharding client.transaction.service.group=default_tx_group sharding.transaction.seata.tx.timeout=60
-
在seata-at-sharding-demo模块中,找到AccountServiceImpl类的transfer方法,开启AT事务注解
import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType; import org.apache.shardingsphere.transaction.core.TransactionType; import org.example.entity.Account; import org.example.mapper.AccountMapper; import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountMapper accountMapper; @Override public void addAccount(Account account) { // accountMapper.insertAccount(account); accountMapper.insert(account); } @Override @Transactional @ShardingSphereTransactionType(TransactionType.BASE) public void transfer(int fromId, int toId, int money) { Account fromAccount = accountMapper.selectById(fromId); Account toAccount = accountMapper.selectById(toId); fromAccount.setMoney(fromAccount.getMoney() - money); toAccount.setMoney(toAccount.getMoney() + money); accountMapper.updateById(fromAccount); accountMapper.updateById(toAccount); } }
7.2.4 Springboot项目 分库分表 测试
-
启动Springboot工程,在HTTP请求工具中访问 localhost:8301/transfer 的Get请求
HTTP请求工具
SpringBoot工程日志
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB1容器的MySQL数据库 docker exec -it MySQLDB1 mysql -u root -p -- 将张三数据重置为200 update db1.t_account set money=200 where id=1; # 进入MySQLDB2容器的MySQL数据库 docker exec -it MySQLDB2 mysql -u root -p -- 将李四数据重置为200 update db2.t_account set money=200 where id=2;
执行结果截图:
-
在seata-at-sharding-demo模块中,找到AccountServiceImpl类的transfer方法,加入异常
import org.apache.shardingsphere.transaction.annotation.ShardingSphereTransactionType; import org.apache.shardingsphere.transaction.core.TransactionType; import org.example.entity.Account; import org.example.mapper.AccountMapper; import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountMapper accountMapper; @Override public void addAccount(Account account) { // accountMapper.insertAccount(account); accountMapper.insert(account); } @Override @Transactional @ShardingSphereTransactionType(TransactionType.BASE) public void transfer(int fromId, int toId, int money) { Account fromAccount = accountMapper.selectById(fromId); Account toAccount = accountMapper.selectById(toId); fromAccount.setMoney(fromAccount.getMoney() - money); toAccount.setMoney(toAccount.getMoney() + money); accountMapper.updateById(fromAccount); // 异常信息 int i = 10 / 0; accountMapper.updateById(toAccount); } }
-
重新启动Springboot工程,在HTTP请求工具中访问 localhost:8301/transfer 的Get请求
HTTP请求工具
SpringBoot工程日志
-
查看数据库数据变化
执行之前
执行之后
7.3 微服务 实现 跨数据库跨服务的分布式事务
7.3.1 准备阶段
Docker规划:
- 创建两个MySQL数据库容器,名称分别是MySQLDB3、MySQLDB4和MySQLDB5
- 容器名为MySQLDB3端口对外映射为3309
- 容器名为MySQLDB4端口对外映射为3310
- 容器名为MySQLDB5端口对外映射为3311
# 查看MySQL镜像
docker search mysql
# 拉取MySQL镜像
docker pull mysql
# 查看拉取的镜像
docker images
# 运行名为MySQLDB3容器名的MySQL镜像
docker run -d --name MySQLDB3 -p3309:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 运行名为MySQLDB4容器名的MySQL镜像
docker run -d --name MySQLDB4 -p3310:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 运行名为MySQLDB5容器名的MySQL镜像
docker run -d --name MySQLDB5 -p3311:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 查看运行的容器
docker ps
- 创建一个seata分布式容器,名称是seata-server
- 容器名为seata-server端口对外映射为7091和8091
# 查看seata镜像
docker search seata
# 拉取seata镜像
docker pull seataio/seata-server
# 查看拉取的镜像
docker images
# 运行名为seata-server容器名的seataio/seata-server镜像
docker run -d --name seata-server -p7091:7091 -p8091:8091 seataio/seata-server:latest
# 查看运行的容器
docker ps
- 创建一个nacos容器,名称是nacos-server
- 容器名为nacos-server端口对外映射为8848、9848和9849
# 查看nacos镜像
docker search nacos
# 拉取nacos镜像
docker pull nacos/nacos-server
# 查看拉取的镜像
docker images
# 运行名为nacos-server容器名的nacos/nacos-server镜像
docker run -d --name nacos-server -p8848:8848 -p9848:9848 -p9849:9849 -e MODE=standalone -e JVM_XMS=512m -e JVM_XMX=512m -e JVM_XMN=256m nacos/nacos-server:latest
# 查看运行的容器
docker ps
MySQL规划:
- 在MySQLDB3容器运行的MySQL中创建db3数据库
- 在MySQLDB4容器运行的MySQL中创建db4数据库
- 在MySQLDB5容器运行的MySQL中创建db5数据库
- 在db3数据库中创建t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- 在db4数据库中创建t_good数据库表
- t_good数据库表中字段:id、name、price,stock四个字段,类型分别是:bigint、varchar(30)、double、bigint
- 在db5数据库中创建t_order数据库表
- t_order数据库表中字段:id、account_id、good_id,good_count,status,all_pay六个字段,类型分别是:bigint、bigint、bigint、bigint、varchar(30)、double
- db3数据库的t_account数据库表中插入数据:(1,'张三',200)
- db4数据库的t_good数据库表中插入数据:(1,'玩具小车',80,200)
# 进入MySQLDB3容器的MySQL数据库
docker exec -it MySQLDB3 mysql -u root -p
-- 创建db3数据库
CREATE DATABASE db3;
USE db3;
-- 在db3数据库中创建t_account数据库表
CREATE TABLE `t_account` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`money` int DEFAULT NULL COMMENT '金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息';
-- 在db3数据库的t_account数据库表中插入数据
INSERT INTO db3.`t_account` (`id`,`name`,`money`) VALUES (1,'张三',200);
# 进入MySQLDB4容器的MySQL数据库
docker exec -it MySQLDB4 mysql -u root -p
-- 创建db4数据库
CREATE DATABASE db4;
USE db4;
-- 在db4数据库中创建t_good数据库表
CREATE TABLE `t_good` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '商品名',
`price` double DEFAULT NULL COMMENT '价格',
`stock` bigint DEFAULT NULL COMMENT '库存'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品信息';
-- 在db4数据库的t_good数据库表中插入数据
INSERT INTO db4.`t_good` (`id`,`name`,`price`,`stock`) VALUES (1,'玩具小车',80,200);
# 进入MySQLDB5容器的MySQL数据库
docker exec -it MySQLDB5 mysql -u root -p
-- 创建db5数据库
CREATE DATABASE db5;
USE db5;
-- 在db5数据库中创建t_order数据库表
CREATE TABLE `t_order` (
`id` bigint NOT NULL COMMENT '主键',
`account_id` bigint NOT NULL COMMENT '账户主键',
`good_id` bigint NOT NULL COMMENT '商品主键',
`good_count` bigint NOT NULL COMMENT '商品数量',
`status` varchar(30) DEFAULT NULL COMMENT '订单状态',
`all_pay` double DEFAULT NULL COMMENT '支付总金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单信息';
项目规划:
- 打开IDEA,创建一个Empty Project空工程,Name输入Seata-Code
- 在刚创建的Seata-Code工程上,右键创建模块,Name输入seata-at
- 在seata-at模块中,右键创建模块,Name输入seata-at-springcloud-demo,注意不依赖于seata-at模块
- 在seata-at-springcloud-demo模块下,创建seata-at-springcloud-common模块,作为公共实体类模块
- 在seata-at-springcloud-demo模块下,创建seata-at-springcloud-order模块,作为订单模块
- 在seata-at-springcloud-demo模块下,创建seata-at-springcloud-good模块,作为商品模块
- 在seata-at-springcloud-demo模块下,创建seata-at-springcloud-account模块,作为用户模块
- 在seata-at-springcloud-demo模块下,创建seata-at-springcloud-business模块,作为业务模块
执行效果截图:
7.3.2 搭建基础环境
搭建seata-at-springcloud-demo模块,作为父级工程
-
在seata-at-springcloud-demo模块中,找到pom.xml文件,引入SpringBoot的父工程依赖、SpringCloud依赖、SpringCloudAlibaba依赖
<groupId>org.example</groupId> <artifactId>seata-at-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <modules> <module>seata-at-springcloud-common</module> <module>seata-at-springcloud-order</module> <module>seata-at-springcloud-good</module> <module>seata-at-springcloud-account</module> <module>seata-at-springcloud-business</module> </modules> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <maven.compiler.compilerVersion>11</maven.compiler.compilerVersion> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring-cloud.version>Hoxton.SR12</spring-cloud.version> <spring-cloud-alibaba.version>2.2.9.RELEASE</spring-cloud-alibaba.version> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.12.RELEASE</version> <relativePath/> </parent> <dependencyManagement> <dependencies> <!--SpringCloud依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--SpringCloudAlibaba依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
搭建seata-at-springcloud-common模块,作为公共实体类模块
-
在seata-at-springcloud-common模块中,找到pom.xml文件,引入lombok自动插入依赖、Mybatis-plus数据操作组件
<parent> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-at-springcloud-common</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>
-
在seata-at-springcloud-common模块中,创建entity包,新建Account实体类,与数据表t_account对应;新建Good实体类,与数据表t_good对应;新建Order实体类,与数据表t_order对应;
新建Account实体类,与数据表t_account对应
import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) @TableName("t_account") public class Account { private Integer id; private String name; private Integer money; }
新建Good实体类,与数据表t_good对应
import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) @TableName("t_good") public class Good { private Integer id; private String name; private double price; private int stock; }
新建Order实体类,与数据表t_order对应
import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) @TableName("t_order") public class Order { private Integer id; private Integer accountId; private Integer goodId; private Integer goodCount; private String status; private double allPay; }
搭建seata-at-springcloud-order模块,作为订单模块
-
在seata-at-springcloud-order模块中,找到pom.xml文件,引入SpringBoot的Web依赖、MySQL连接依赖、seata-at-springcloud-common公共模块依赖、Nacos注册发现依赖
<parent> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-at-springcloud-order</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--nacos 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-at-springcloud-order模块中,创建application.yml文件,配置端口、数据源信息、MyBatis-plus基础配置、Nacos注册发现地址
server: port: 8401 spring: application: name: seata-at-springcloud-order datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3311/db5?serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ZYMzym111 cloud: nacos: server-addr: localhost:8848 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true
-
在seata-at-springcloud-order模块中,找到main/java目录创建mapper包,新建OrderMapper类,继承BaseMapper;找到mian/resource目录创建mapper文件夹,新建OrderMapper.xml文件。
main/java目录下mapper包,OrderMapper类
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.example.entity.Order; import org.springframework.stereotype.Repository; @Repository public interface OrderMapper extends BaseMapper<Order> { }
main/resource目录下mapper文件夹,OrderMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.example.mapper.OrderMapper"> </mapper>
-
在seata-at-springcloud-order模块下,创建service包,新建OrderService接口,定义新增的方法
import com.baomidou.mybatisplus.extension.service.IService; import org.example.entity.Order; import org.springframework.stereotype.Service; @Service public interface OrderService extends IService<Order> { public void addOrder(Order order); }
-
在service包内创建impl包,新建OrderServiceImpl类,实现接口OrderService,并实现其接口方法
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.example.entity.Order; import org.example.mapper.OrderMapper; import org.example.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Random; @Service public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService { @Autowired private OrderMapper orderMapper; @Override public void addOrder(Order order) { if (order.getId() == null) { Random rand = new Random(); int temp = rand.nextInt(100000); order.setId(temp); } orderMapper.insert(order); } }
-
在seata-xa-springcloud-order模块下,创建controller包,新建OrderController类,定义web请求接口的方法
import org.example.entity.Order; import org.example.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("order") public class OrderController { @Autowired private OrderService orderService; @PostMapping public String addOrder(@RequestBody Order order) { orderService.addOrder(order); return "success"; } }
-
在根包(org.example)下,新建一个ATOrderApplication启动类,编写Springboot启动代码
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan(basePackages = {"org.example.mapper"}) public class ATOrderApplication { public static void main(String[] args) { SpringApplication.run(ATOrderApplication.class, args); } }
-
启动Springboot工程
在HTTP请求工具中访问order的Post请求,参数分别是:{ "id": 1, "accountId": 1, "goodId": 1, "goodCount": 2 }
HTTP请求工具
程序控制台
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB5容器的MySQL数据库 docker exec -it MySQLDB5 mysql -u root -p -- 清空t_order表数据 truncate db5.t_order;
执行结果截图:
搭建seata-at-springcloud-good模块,作为商品模块
-
在seata-at-springcloud-good模块中,找到pom.xml文件,引入SpringBoot的Web依赖、MySQL连接依赖、seata-at-springcloud-common公共模块依赖、Nacos注册发现依赖
<parent> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-at-springcloud-good</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--nacos 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-at-springcloud-good模块中,创建application.yml文件,配置端口、数据源信息、MyBatis-plus基础配置、Nacos注册发现地址
server: port: 8402 spring: application: name: seata-at-springcloud-good datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3310/db4?serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ZYMzym111 cloud: nacos: server-addr: localhost:8848 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true
-
在seata-at-springcloud-good模块中,找到main/java目录创建mapper包,新建GoodMapper类,继承BaseMapper;找到mian/resource目录创建mapper文件夹,新建GoodMapper.xml文件。
main/java目录下mapper包,GoodMapper类
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; import org.example.entity.Good; import org.springframework.stereotype.Repository; @Repository public interface GoodMapper extends BaseMapper<Good> { @Update("update t_good set stock=stock-#{stock} where id=#{id}") public void updateGoodStock(@Param("stock") Integer stock, @Param("id") Integer id); }
main/resource目录下mapper文件夹,GoodMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.example.mapper.GoodMapper"> </mapper>
-
在seata-at-springcloud-good模块下,创建service包,新建GoodService接口,定义新增的方法
import com.baomidou.mybatisplus.extension.service.IService; import org.example.entity.Good; import org.springframework.stereotype.Service; @Service public interface GoodService extends IService<Good> { public void reduceGoodStock(int num, int goodId); }
-
在service包内创建impl包,新建GoodServiceImpl类,实现接口GoodService,并实现其接口方法
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.example.entity.Good; import org.example.mapper.GoodMapper; import org.example.service.GoodService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class GoodServiceImpl extends ServiceImpl<GoodMapper, Good> implements GoodService { @Autowired private GoodMapper goodMapper; @Override public void reduceGoodStock(int num, int goodId) { Good good = goodMapper.selectById(goodId); if (good.getStock() < num) { throw new RuntimeException("Good库存不足"); } goodMapper.updateGoodStock(num, goodId); } }
-
在seata-at-springcloud-good模块下,创建controller包,新建GoodController类,定义web请求接口的方法
import org.example.entity.Good; import org.example.service.GoodService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("good") public class GoodController { @Autowired private GoodService goodService; @GetMapping("{id}") public Good findGoodById(@PathVariable("id") Integer id) { Good good = goodService.getById(id); return good; } @PutMapping public String reduceGoodStock(Integer num, Integer goodId) { goodService.reduceGoodStock(num, goodId); return "success"; } }
-
在根包(org.example)下,新建一个ATGoodApplication启动类,编写Springboot启动代码
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan(basePackages = {"org.example.mapper"}) public class ATGoodApplication { public static void main(String[] args) { SpringApplication.run(ATGoodApplication.class, args); } }
-
启动Springboot工程
-
在HTTP请求工具中访问good的Get请求,参数是:1(该访问数据前后不会发生变化)
HTTP请求工具
程序控制台
-
在HTTP请求工具中访问good的Put请求,参数是:num=2&goodId=1(该访问数据前后会发生变化)
HTTP请求工具
程序控制台
-
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB4容器的MySQL数据库 docker exec -it MySQLDB4 mysql -u root -p -- 还原t_good表数据 update db4.t_good set stock=200 where id=1;
执行结果截图:
搭建seata-at-springcloud-account模块,作为用户模块
-
在seata-at-springcloud-account模块中,找到pom.xml文件,引入SpringBoot的Web依赖、MySQL连接依赖、seata-at-springcloud-common公共模块依赖、Nacos注册发现依赖
<parent> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-at-springcloud-account</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--nacos 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-at-springcloud-account模块中,创建application.yml文件,配置端口、数据源信息、MyBatis-plus基础配置、Nacos注册发现地址
server: port: 8403 spring: application: name: seata-at-springcloud-account datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3309/db3?serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ZYMzym111 cloud: nacos: server-addr: localhost:8848 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true
-
在seata-at-springcloud-account模块下,找到main/java目录创建mapper包,新建AccountMapper类,继承BaseMapper;找到mian/resource目录创建mapper文件夹,新建AccountMapper.xml文件。
main/java目录下mapper包,AccountMapper类
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; import org.example.entity.Account; import org.springframework.stereotype.Repository; @Repository public interface AccountMapper extends BaseMapper<Account> { @Update("update t_account set money=money-#{money} where id=#{id}") public void reduceAccountMoney(@Param("money") double money, @Param("id") Integer id); }
main/resource目录下mapper文件夹,AccountMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.example.mapper.AccountMapper"> </mapper>
-
在seata-at-springcloud-account模块下,创建service包,新建AccountService接口,定义新增的方法
import com.baomidou.mybatisplus.extension.service.IService; import org.example.entity.Account; import org.springframework.stereotype.Service; @Service public interface AccountService extends IService<Account> { public void reduceAccountMoney(double money, Integer id); }
-
在service包内创建impl包,新建AccountServiceImpl类,实现接口AccountService,并实现其接口方法
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.example.entity.Account; import org.example.mapper.AccountMapper; import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService { @Autowired private AccountMapper accountMapper; @Override public void reduceAccountMoney(double money, Integer id) { Account account = accountMapper.selectById(id); if (account.getMoney() < money) { throw new RuntimeException("account 账户余额不足"); } accountMapper.reduceAccountMoney(money, id); } }
-
在seata-at-springcloud-account模块下,创建controller包,新建AccountController类,定义web请求接口的方法
import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("account") public class AccountController { @Autowired private AccountService accountService; @PutMapping public String reduceAccountMoney(double money, Integer id) { accountService.reduceAccountMoney(money, id); return "success"; } }
-
在根包(org.example)下,新建一个ATAccountApplication启动类,编写Springboot启动代码
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan(basePackages = {"org.example.mapper"}) public class ATAccountApplication { public static void main(String[] args) { SpringApplication.run(ATAccountApplication.class, args); } }
-
启动Springboot工程
在HTTP请求工具中访问account的Put请求,参数是:money=100&id=1
HTTP请求工具
程序控制台
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB3容器的MySQL数据库 docker exec -it MySQLDB3 mysql -u root -p -- 还原t_account表数据 update db3.t_account set money=200 where id=1;
执行结果截图:
搭建seata-at-springcloud-business模块,作为业务模块
-
在seata-at-springcloud-business模块中,找到pom.xml文件,引入SpringBoot的Web依赖、seata-at-springcloud-common公共模块依赖、Nacos注册发现依赖
<parent> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-at-springcloud-business</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--nacos 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-at-springcloud-business模块中,创建application.yml文件,配置端口、Nacos注册发现地址
server: port: 8404 spring: application: name: seata-at-springcloud-business cloud: nacos: server-addr: localhost:8848
-
在seata-at-springcloud-bussiness模块中,创建utils包,新建OrderStatus类和URL类,用于标识订单状态和URL请求路径。
OrderStatus类
public enum OrderStatus { CREATE,UPDATING,FINISH }
URL类
public class URL { public static final String CREATE_ORDER = "http://seata-at-springcloud-order/order"; public static final String GOOD_INFO = "http://seata-at-springcloud-good/good/%d"; public static final String GOOD_REDUCE = "http://seata-at-springcloud-good/good?num=%d&goodId=%d"; public static final String ACCOUNT_REDUCE = "http://seata-at-springcloud-account/account?money=%f&id=%d"; }
-
在seata-at-springcloud-business模块中,创建service包,新建BusinessService接口,定义新增的方法
import org.springframework.stereotype.Service; @Service public interface BusinessService { public void placeOrder(Integer accountId,Integer goodId,Integer num); }
-
在service包内创建impl包,新建BusinessServiceImpl类,实现接口BusinessService,并实现其接口方法
import org.example.entity.Good; import org.example.entity.Order; import org.example.service.BusinessService; import org.example.utils.OrderStatus; import org.example.utils.URL; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class BusinessServiceImpl implements BusinessService { @Autowired private RestTemplate restTemplate; @Override public void placeOrder(Integer accountId, Integer goodId, Integer num) { // 查询商品信息 Good good = restTemplate.getForObject(String.format(URL.GOOD_INFO, goodId), Good.class); double allPay = good.getPrice() * num; Order order = new Order().setStatus(OrderStatus.CREATE.name()).setAccountId(accountId).setGoodId(goodId).setGoodCount(num).setAllPay(allPay); // 下订单 HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); HttpEntity<Order> httpEntity = new HttpEntity<>(order, httpHeaders); restTemplate.postForObject(URL.CREATE_ORDER, httpEntity, String.class); // 减库存 restTemplate.put(String.format(URL.GOOD_REDUCE, num, goodId), null); // 扣钱 restTemplate.put(String.format(URL.ACCOUNT_REDUCE, allPay, accountId), null); } }
-
在seata-at-springcloud-business模块中,创建controller包,新建BusinessController类,定义web请求接口的方法
import org.example.service.BusinessService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class BusinessController { @Autowired private BusinessService businessService; @GetMapping("placeOrder") public String placeOrder(Integer accountId, Integer goodId, Integer num) { businessService.placeOrder(accountId, goodId, num); return "success"; } }
-
在根包(org.example)下,新建一个ATBusinessApplication启动类,编写Springboot启动代码
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication(exclude= {DataSourceAutoConfiguration.class}) public class ATBusinessApplication { public static void main(String[] args) { SpringApplication.run(ATBusinessApplication.class, args); } @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }
-
启动Springboot工程
在HTTP请求工具中访问placeOrder的Get请求,参数是:accountId=1&goodId=1&num=1
HTTP请求工具
Business程序控制台
Order程序控制台
Good程序控制台
Account程序控制台
-
查看数据库数据变化
执行之前
order数据库
good数据库
account数据库
执行之后
order数据库
good数据库
account数据库
-
恢复数据库原始数据
# 进入MySQLDB3容器的MySQL数据库 docker exec -it MySQLDB3 mysql -u root -p -- 还原t_account表数据 update db3.t_account set money=200 where id=1; # 进入MySQLDB4容器的MySQL数据库 docker exec -it MySQLDB4 mysql -u root -p -- 还原t_good表数据 update db4.t_good set stock=200 where id=1; # 进入MySQLDB5容器的MySQL数据库 docker exec -it MySQLDB5 mysql -u root -p -- 清空t_order表数据 truncate db5.t_order;
执行结果截图:
7.3.3 微服务 跨数据库跨服务 改造 spring-cloud-starter-alibaba-seata包解决
-
在包含分布式事务的数据库中,加入undo_log表结构
-- 注意此处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;
执行效果截图
-
分别为数据库db3、db4和db5中的t_account、t_good、t_order表定义主键(全局锁由表名和操作记录的主键 按照⼀定的规律组成)
# 进入MySQLDB3容器的MySQL数据库 docker exec -it MySQLDB3 mysql -u root -p -- 数据库db3的t_account表定义主键 ALTER TABLE db3.t_account ADD CONSTRAINT t_account_PK PRIMARY KEY (id); # 进入MySQLDB4容器的MySQL数据库 docker exec -it MySQLDB4 mysql -u root -p --数据库db4的t_good表定义主键 ALTER TABLE db4.t_good ADD CONSTRAINT t_good_PK PRIMARY KEY (id); # 进入MySQLDB5容器的MySQL数据库 docker exec -it MySQLDB5 mysql -u root -p --数据库db5的t_order表定义主键 ALTER TABLE db5.t_order ADD CONSTRAINT t_order_PK PRIMARY KEY (id);
-
找到seata-at-springcloud-common模块中,找到pom.xml文件,加入spring-cloud-starter-alibaba-seata包依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
-
在seata-at-springcloud-order、seata-at-springcloud-good、seata-at-springcloud-account、seata-at-springcloud-business模块中,找到application.yml文件,加入Seata配置信息
seata: service: vgroup-mapping: default_tx_group: default grouplist: default: localhost:8091 tx-service-group: default_tx_group enable-auto-data-source-proxy: true # 开启自动代理 默认值是true 可以不屑 data-source-proxy-mode: AT # 声明seata使用XA模式解决分布式
-
在seata-at-springcloud-order、seata-at-springcloud-good、seata-at-springcloud-account模块中,创建config包,新建DBConfig类,更改数据源连接为Druid
import com.alibaba.druid.pool.DruidDataSource; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; @Configuration //@ConditionalOnClass(PlatformTransactionManager.class) public class DBConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return new DruidDataSource(); } }
-
在seata-at-springcloud-business模块中,找到BusinessServiceImpl类的placeOrder方法,加入@GlobalTransactional注解
import io.seata.spring.annotation.GlobalTransactional; import org.example.entity.Good; import org.example.entity.Order; import org.example.service.BusinessService; import org.example.utils.OrderStatus; import org.example.utils.URL; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class BusinessServiceImpl implements BusinessService { @Autowired private RestTemplate restTemplate; @Override @GlobalTransactional public void placeOrder(Integer accountId, Integer goodId, Integer num) { // 查询商品信息 Good good = restTemplate.getForObject(String.format(URL.GOOD_INFO, goodId), Good.class); double allPay = good.getPrice() * num; Order order = new Order().setStatus(OrderStatus.CREATE.name()).setAccountId(accountId).setGoodId(goodId).setGoodCount(num).setAllPay(allPay); // 下订单 HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); HttpEntity<Order> httpEntity = new HttpEntity<>(order, httpHeaders); restTemplate.postForObject(URL.CREATE_ORDER, httpEntity, String.class); // 减库存 restTemplate.put(String.format(URL.GOOD_REDUCE, num, goodId), null); // 扣钱 restTemplate.put(String.format(URL.ACCOUNT_REDUCE, allPay, accountId), null); } }
-
在seata-at-springcloud-demo模块下,所有模块的pom.xml文件中加入java版本
<properties> <java.version>11</java.version> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
-
运行模块加入配置,Modify options → Add VM options
--add-opens java.base/java.lang=ALL-UNNAMED
7.3.4 微服务 跨数据库跨服务 测试
-
正确请求分布式事务,正确处理
在HTTP请求工具中发送两次placeOrder的Get成功请求,参数为:accountId=1&goodId=1&num=1
HTTP请求工具
business程序控制台
order程序控制台
good程序控制台
account程序控制台
-
查看数据库数据变化
执行之前
order数据库
good数据库
account数据库
执行之后
order数据库
good数据库
account数据库
-
正确请求分布式事务,异常处理
在HTTP请求工具中发送两次placeOrder的Get成功请求,参数为:accountId=1&goodId=1&num=1
HTTP请求工具
business程序控制台
order程序控制台
good程序控制台
account程序控制台
-
查看数据库数据变化
执行之前
order数据库
good数据库
account数据库
执行之后
order数据库
good数据库
account数据库
7.4 微服务 实现 跨数据库跨服务+分库分表的分布式事务
7.4.1 准备阶段
Docker规划:
- 创建两个MySQL数据库容器,名称分别是MySQLDB3、MySQLDB4和MySQLDB5
- 容器名为MySQLDB3端口对外映射为3309
- 容器名为MySQLDB4端口对外映射为3310
- 容器名为MySQLDB5端口对外映射为3311
- 容器名为MySQLDB6端口对外映射为3312
# 查看MySQL镜像
docker search mysql
# 拉取MySQL镜像
docker pull mysql
# 查看拉取的镜像
docker images
# 运行名为MySQLDB3容器名的MySQL镜像
docker run -d --name MySQLDB3 -p3309:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 运行名为MySQLDB4容器名的MySQL镜像
docker run -d --name MySQLDB4 -p3310:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 运行名为MySQLDB5容器名的MySQL镜像
docker run -d --name MySQLDB5 -p3311:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 运行名为MySQLDB6容器名的MySQL镜像
docker run -d --name MySQLDB6 -p3312:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 查看运行的容器
docker ps
- 创建一个seata分布式容器,名称是seata-server
- 容器名为seata-server端口对外映射为7091和8091
# 查看seata镜像
docker search seata
# 拉取seata镜像
docker pull seataio/seata-server
# 查看拉取的镜像
docker images
# 运行名为seata-server容器名的seataio/seata-server镜像
docker run -d --name seata-server -p7091:7091 -p8091:8091 -e JVM_XMS=512m -e JVM_XMX=512m -e JVM_XMN=256m seataio/seata-server:latest
# 查看运行的容器
docker ps
- 创建一个nacos容器,名称是nacos-server
- 容器名为nacos-server端口对外映射为8848、9848和9849
# 查看nacos镜像
docker search nacos
# 拉取nacos镜像
docker pull nacos/nacos-server
# 查看拉取的镜像
docker images
# 运行名为nacos-server容器名的nacos/nacos-server镜像
docker run -d --name nacos-server -p8848:8848 -p9848:9848 -p9849:9849 -e MODE=standalone -e JVM_XMS=512m -e JVM_XMX=512m -e JVM_XMN=256m nacos/nacos-server:latest
# 查看运行的容器
docker ps
MySQL规划:
- 在MySQLDB3容器运行的MySQL中创建db3数据库
- 在MySQLDB4容器运行的MySQL中创建db4数据库
- 在MySQLDB5容器运行的MySQL中创建db5数据库
- 在MySQLDB6容器运行的MySQL中创建db6数据库
- 在db3数据库中创建t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- 在db4数据库中创建t_good数据库表
- t_good数据库表中字段:id、name、price,stock四个字段,类型分别是:bigint、varchar(30)、double、bigint
- 在db5数据库中创建t_order_0、t_order_1、t_order_2数据库表
- t_order_0、t_order_1、t_order_2数据库表中字段:id、account_id、good_id,good_count,status,all_pay六个字段,类型分别是:bigint、bigint、bigint、bigint、varchar(30)、double
- 在db6数据库中创建t_order_0、t_order_1、t_order_2数据库表
- t_order_0、t_order_1、t_order_2数据库表中字段:id、account_id、good_id,good_count,status,all_pay六个字段,类型分别是:bigint、bigint、bigint、bigint、varchar(30)、double
- db3数据库的t_account数据库表中插入数据:(1,'张三',200)
- db4数据库的t_good数据库表中插入数据:(1,'玩具小车',80,200)
# 进入MySQLDB3容器的MySQL数据库
docker exec -it MySQLDB3 mysql -u root -p
-- 创建db3数据库
CREATE DATABASE db3;
USE db3;
-- 在db3数据库中创建t_account数据库表
CREATE TABLE `t_account` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`money` int DEFAULT NULL COMMENT '金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息';
-- 在db3数据库的t_account数据库表中插入数据
INSERT INTO db3.`t_account` (`id`,`name`,`money`) VALUES (1,'张三',200);
# 进入MySQLDB4容器的MySQL数据库
docker exec -it MySQLDB4 mysql -u root -p
-- 创建db4数据库
CREATE DATABASE db4;
USE db4;
-- 在db4数据库中创建t_good数据库表
CREATE TABLE `t_good` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '商品名',
`price` double DEFAULT NULL COMMENT '价格',
`stock` bigint DEFAULT NULL COMMENT '库存'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品信息';
-- 在db4数据库的t_good数据库表中插入数据
INSERT INTO db4.`t_good` (`id`,`name`,`price`,`stock`) VALUES (1,'玩具小车',80,200);
# 进入MySQLDB5容器的MySQL数据库
docker exec -it MySQLDB5 mysql -u root -p
-- 创建db5数据库
CREATE DATABASE db5;
USE db5;
-- 在db5数据库中创建t_order_0数据库表
CREATE TABLE `t_order_0` (
`id` bigint NOT NULL COMMENT '主键',
`account_id` bigint NOT NULL COMMENT '账户主键',
`good_id` bigint NOT NULL COMMENT '商品主键',
`good_count` bigint NOT NULL COMMENT '商品数量',
`status` varchar(30) DEFAULT NULL COMMENT '订单状态',
`all_pay` double DEFAULT NULL COMMENT '支付总金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单信息';
-- 在db5数据库中创建t_order_1数据库表
CREATE TABLE `t_order_1` (
`id` bigint NOT NULL COMMENT '主键',
`account_id` bigint NOT NULL COMMENT '账户主键',
`good_id` bigint NOT NULL COMMENT '商品主键',
`good_count` bigint NOT NULL COMMENT '商品数量',
`status` varchar(30) DEFAULT NULL COMMENT '订单状态',
`all_pay` double DEFAULT NULL COMMENT '支付总金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单信息';
-- 在db5数据库中创建t_order_2数据库表
CREATE TABLE `t_order_2` (
`id` bigint NOT NULL COMMENT '主键',
`account_id` bigint NOT NULL COMMENT '账户主键',
`good_id` bigint NOT NULL COMMENT '商品主键',
`good_count` bigint NOT NULL COMMENT '商品数量',
`status` varchar(30) DEFAULT NULL COMMENT '订单状态',
`all_pay` double DEFAULT NULL COMMENT '支付总金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单信息';
# 进入MySQLDB6容器的MySQL数据库
docker exec -it MySQLDB6 mysql -u root -p
-- 创建db6数据库
CREATE DATABASE db6;
USE db6;
-- 在db6数据库中创建t_order_0数据库表
CREATE TABLE `t_order_0` (
`id` bigint NOT NULL COMMENT '主键',
`account_id` bigint NOT NULL COMMENT '账户主键',
`good_id` bigint NOT NULL COMMENT '商品主键',
`good_count` bigint NOT NULL COMMENT '商品数量',
`status` varchar(30) DEFAULT NULL COMMENT '订单状态',
`all_pay` double DEFAULT NULL COMMENT '支付总金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单信息';
-- 在db6数据库中创建t_order_1数据库表
CREATE TABLE `t_order_1` (
`id` bigint NOT NULL COMMENT '主键',
`account_id` bigint NOT NULL COMMENT '账户主键',
`good_id` bigint NOT NULL COMMENT '商品主键',
`good_count` bigint NOT NULL COMMENT '商品数量',
`status` varchar(30) DEFAULT NULL COMMENT '订单状态',
`all_pay` double DEFAULT NULL COMMENT '支付总金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单信息';
-- 在db6数据库中创建t_order_2数据库表
CREATE TABLE `t_order_2` (
`id` bigint NOT NULL COMMENT '主键',
`account_id` bigint NOT NULL COMMENT '账户主键',
`good_id` bigint NOT NULL COMMENT '商品主键',
`good_count` bigint NOT NULL COMMENT '商品数量',
`status` varchar(30) DEFAULT NULL COMMENT '订单状态',
`all_pay` double DEFAULT NULL COMMENT '支付总金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单信息';
项目规划:
- 打开IDEA,创建一个Empty Project空工程,Name输入Seata-Code
- 在刚创建的Seata-Code工程上,右键创建模块,Name输入seata-at
- 在seata-at模块中,右键创建模块,Name输入seata-at-springcloud-sharding-demo,注意不依赖于seata-at模块
- 在seata-at-springcloud-sharding-demo模块下,创建seata-at-springcloud-sharding-common模块,作为公共实体类模块
- 在seata-at-springcloud-sharding-demo模块下,创建seata-at-springcloud-sharding-order模块,作为订单模块
- 在seata-at-springcloud-sharding-demo模块下,创建seata-at-springcloud-sharding-good模块,作为商品模块
- 在seata-at-springcloud-sharding-demo模块下,创建seata-at-springcloud-sharding-account模块,作为用户模块
- 在seata-at-springcloud-sharding-demo模块下,创建seata-at-springcloud-sharding-business模块,作为业务模块
执行效果截图:
7.4.2 搭建基础环境
搭建seata-at-springcloud-sharding-demo模块,作为父级工程
-
在seata-at-springcloud-sharding-demo模块中,找到pom.xml文件,引入SpringBoot的父工程依赖、SpringCloud依赖、SpringCloudAlibaba依赖
<groupId>org.example</groupId> <artifactId>seata-at-springcloud-sharding-demo</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <modules> <module>seata-at-springcloud-sharding-common</module> <module>seata-at-springcloud-sharding-order</module> <module>seata-at-springcloud-sharding-good</module> <module>seata-at-springcloud-sharding-account</module> <module>seata-at-springcloud-sharding-business</module> </modules> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <maven.compiler.compilerVersion>11</maven.compiler.compilerVersion> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring-cloud.version>Hoxton.SR12</spring-cloud.version> <spring-cloud-alibaba.version>2.2.9.RELEASE</spring-cloud-alibaba.version> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.12.RELEASE</version> <relativePath/> </parent> <dependencyManagement> <dependencies> <!--SpringCloud依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--SpringCloudAlibaba依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
搭建seata-at-springcloud-sharding-common模块,作为公共实体类模块
-
在seata-at-springcloud-sharding-common模块中,找到pom.xml文件,引入lombok自动插入依赖、Mybatis-plus数据操作组件
<parent> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-sharding-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-at-springcloud-sharding-common</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>
-
在seata-at-springcloud-sharding-common模块中,创建entity包,新建Account实体类,与数据表t_account对应;新建Good实体类,与数据表t_good对应;新建Order实体类,与数据表t_order对应;
新建Account实体类,与数据表t_account对应
import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) @TableName("t_account") public class Account { private Integer id; private String name; private Integer money; }
新建Good实体类,与数据表t_good对应
import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) @TableName("t_good") public class Good { private Integer id; private String name; private double price; private int stock; }
新建Order实体类,与数据表t_order对应
import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) @TableName("t_order") public class Order { private Integer id; private Integer accountId; private Integer goodId; private Integer goodCount; private String status; private double allPay; }
搭建seata-at-springcloud-sharding-order模块,作为订单模块
-
在seata-at-springcloud-sharding-order模块中,找到pom.xml文件,引入SpringBoot的Web依赖、MySQL连接依赖、seata-at-springcloud-sharding-common公共模块依赖、Nacos注册发现依赖
<parent> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-sharding-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-at-springcloud-sharding-order</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--nacos 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-sharding-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.1.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-at-springcloud-sharding-order模块中,创建application.yml文件,配置端口、数据源信息、MyBatis-plus基础配置、Nacos注册发现地址
server: port: 8501 spring: application: name: seata-at-springcloud-sharding-order autoconfigure: exclude: - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration shardingsphere: mode: type: Memory datasource: names: db1,db2 db1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3311/db5?serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ZYMzym111 db2: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3312/db6?serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ZYMzym111 rules: sharding: tables: t_order: actual-data-nodes: db$->{1..2}.t_order_$->{0..2} database-strategy: standard: sharding-column: id sharding-algorithm-name: database-split table-strategy: standard: sharding-column: id sharding-algorithm-name: account-split sharding-algorithms: database-split: type: INLINE props: algorithm-expression: db$->{id % 2 !=0 ? 1:2} account-split: type: INLINE props: algorithm-expression: t_order_$->{id % 3} props: sql-show: true # 开启Sql日志输出 cloud: nacos: server-addr: localhost:8848 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
-
在seata-at-springcloud-sharding-order模块中,找到main/java目录创建mapper包,新建OrderMapper类,继承BaseMapper;找到mian/resource目录创建mapper文件夹,新建OrderMapper.xml文件。
main/java目录下mapper包,OrderMapper类
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.example.entity.Order; import org.springframework.stereotype.Repository; @Repository public interface OrderMapper extends BaseMapper<Order> { }
main/resource目录下mapper文件夹,OrderMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.example.mapper.OrderMapper"> </mapper>
-
在seata-at-springcloud-sharding-order模块中,创建service包,新建OrderService接口,定义新增的方法
import com.baomidou.mybatisplus.extension.service.IService; import org.example.entity.Order; import org.springframework.stereotype.Service; @Service public interface OrderService extends IService<Order> { public void addOrder(Order order); }
-
在service包内创建impl包,新建OrderServiceImpl类,实现接口OrderService,并实现其接口方法
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.example.entity.Order; import org.example.mapper.OrderMapper; import org.example.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Random; @Service public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService { @Autowired private OrderMapper orderMapper; @Override public void addOrder(Order order) { if (order.getId() == null) { Random rand = new Random(); int temp = rand.nextInt(100000); order.setId(temp); } orderMapper.insert(order); } }
-
在seata-xa-springcloud-order模块中,创建controller包,新建OrderController类,定义web请求接口的方法
import org.example.entity.Order; import org.example.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("order") public class OrderController { @Autowired private OrderService orderService; @PostMapping public String addOrder(@RequestBody Order order) { orderService.addOrder(order); return "success"; } }
-
在根包(org.example)下,新建一个ATCloudShardingOrderApplication启动类,编写Springboot启动代码
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan(basePackages = {"org.example.mapper"}) public class ATCloudShardingOrderApplication { public static void main(String[] args) { SpringApplication.run(ATCloudShardingOrderApplication.class, args); } }
-
启动Springboot工程
在HTTP请求工具中访问order的Post请求,参数分别是:{ "id": 1, "accountId": 1, "goodId": 1, "goodCount": 2 }
HTTP请求工具
程序控制台
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB5容器的MySQL数据库 docker exec -it MySQLDB5 mysql -u root -p -- 清空t_order_0表数据 truncate db5.t_order_0; -- 清空t_order_1表数据 truncate db5.t_order_1; -- 清空t_order_0表数据 truncate db5.t_order_2; # 进入MySQLDB6容器的MySQL数据库 docker exec -it MySQLDB6 mysql -u root -p -- 清空t_order_0表数据 truncate db6.t_order_0; -- 清空t_order_1表数据 truncate db6.t_order_1; -- 清空t_order_0表数据 truncate db6.t_order_2;
执行结果截图:
搭建seata-at-springcloud-sharding-good模块,作为商品模块
-
在seata-at-springcloud-sharding-good模块中,找到pom.xml文件,引入SpringBoot的Web依赖、MySQL连接依赖、seata-at-springcloud-sharding-common公共模块依赖、Nacos注册发现依赖
<parent> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-sharding-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-at-springcloud-sharding-good</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--nacos 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-sharding-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-at-springcloud-sharding-good模块中,创建application.yml文件,配置端口、数据源信息、MyBatis-plus基础配置、Nacos注册发现地址
server: port: 8502 spring: application: name: seata-at-springcloud-sharding-good datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3310/db4?serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ZYMzym111 cloud: nacos: server-addr: localhost:8848 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true
-
在seata-at-springcloud-sharding-good模块中,找到main/java目录创建mapper包,新建GoodMapper类,继承BaseMapper;找到mian/resource目录创建mapper文件夹,新建GoodMapper.xml文件。
main/java目录下mapper包,GoodMapper类
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; import org.example.entity.Good; import org.springframework.stereotype.Repository; @Repository public interface GoodMapper extends BaseMapper<Good> { @Update("update t_good set stock=stock-#{stock} where id=#{id}") public void updateGoodStock(@Param("stock") Integer stock, @Param("id") Integer id); }
main/resource目录下mapper文件夹,GoodMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.example.mapper.GoodMapper"> </mapper>
-
在seata-at-springcloud-sharding-good模块中,创建service包,新建GoodService接口,定义新增的方法
import com.baomidou.mybatisplus.extension.service.IService; import org.example.entity.Good; import org.springframework.stereotype.Service; @Service public interface GoodService extends IService<Good> { public void reduceGoodStock(int num, int goodId); }
-
在service包内创建impl包,新建GoodServiceImpl类,实现接口GoodService,并实现其接口方法
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.example.entity.Good; import org.example.mapper.GoodMapper; import org.example.service.GoodService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class GoodServiceImpl extends ServiceImpl<GoodMapper, Good> implements GoodService { @Autowired private GoodMapper goodMapper; @Override public void reduceGoodStock(int num, int goodId) { Good good = goodMapper.selectById(goodId); if (good.getStock() < num) { throw new RuntimeException("Good库存不足"); } goodMapper.updateGoodStock(num, goodId); } }
-
在seata-at-springcloud-sharding-good模块中,创建controller包,新建GoodController类,定义web请求接口的方法
import org.example.entity.Good; import org.example.service.GoodService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("good") public class GoodController { @Autowired private GoodService goodService; @GetMapping("{id}") public Good findGoodById(@PathVariable("id") Integer id) { Good good = goodService.getById(id); return good; } @PutMapping public String reduceGoodStock(Integer num, Integer goodId) { goodService.reduceGoodStock(num, goodId); return "success"; } }
-
在根包(org.example)下,新建一个ATCloudShardingGoodApplication启动类,编写Springboot启动代码
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan(basePackages = {"org.example.mapper"}) public class ATCloudShardingGoodApplication { public static void main(String[] args) { SpringApplication.run(ATCloudShardingGoodApplication.class, args); } }
-
启动Springboot工程
-
在HTTP请求工具中访问good的Get请求,参数是:1(该访问数据前后不会发生变化)
HTTP请求工具
程序控制台
-
在HTTP请求工具中访问good的Put请求,参数是:num=2&goodId=1(该访问数据前后会发生变化)
HTTP请求工具
程序控制台
-
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB4容器的MySQL数据库 docker exec -it MySQLDB4 mysql -u root -p -- 还原t_good表数据 update db4.t_good set stock=200 where id=1;
执行结果截图:
搭建seata-at-springcloud-sharding-account模块,作为用户模块
-
在seata-at-springcloud-sharding-account模块中,找到pom.xml文件,引入SpringBoot的Web依赖、MySQL连接依赖、seata-at-springcloud-sharding-common公共模块依赖、Nacos注册发现依赖
<parent> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-sharding-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-at-springcloud-sharding-account</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--nacos 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-sharding-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-at-springcloud-sharding-account模块中,创建application.yml文件,配置端口、数据源信息、MyBatis-plus基础配置、Nacos注册发现地址
server: port: 8503 spring: application: name: seata-at-springcloud-sharding-account datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3309/db3?serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ZYMzym111 cloud: nacos: server-addr: localhost:8848 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true
-
在seata-at-springcloud-sharding-account模块中,找到main/java目录创建mapper包,新建AccountMapper类,继承BaseMapper;找到mian/resource目录创建mapper文件夹,新建AccountMapper.xml文件。
main/java目录下mapper包,AccountMapper类
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; import org.example.entity.Account; import org.springframework.stereotype.Repository; @Repository public interface AccountMapper extends BaseMapper<Account> { @Update("update t_account set money=money-#{money} where id=#{id}") public void reduceAccountMoney(@Param("money") double money, @Param("id") Integer id); }
main/resource目录下mapper文件夹,AccountMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.example.mapper.AccountMapper"> </mapper>
-
在seata-at-springcloud-sharding-account模块中,创建service包,新建AccountService接口,定义新增的方法
import com.baomidou.mybatisplus.extension.service.IService; import org.example.entity.Account; import org.springframework.stereotype.Service; @Service public interface AccountService extends IService<Account> { public void reduceAccountMoney(double money, Integer id); }
-
在service包内创建impl包,新建AccountServiceImpl类,实现接口AccountService,并实现其接口方法
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.example.entity.Account; import org.example.mapper.AccountMapper; import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService { @Autowired private AccountMapper accountMapper; @Override public void reduceAccountMoney(double money, Integer id) { Account account = accountMapper.selectById(id); if (account.getMoney() < money) { throw new RuntimeException("account 账户余额不足"); } accountMapper.reduceAccountMoney(money, id); } }
-
在seata-at-springcloud-sharding-account模块中,创建controller包,新建AccountController类,定义web请求接口的方法
import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("account") public class AccountController { @Autowired private AccountService accountService; @PutMapping public String reduceAccountMoney(double money, Integer id) { accountService.reduceAccountMoney(money, id); return "success"; } }
-
在根包(org.example)下,新建一个ATCoudShardingAccountApplication启动类,编写Springboot启动代码
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan(basePackages = {"org.example.mapper"}) public class ATCoudShardingAccountApplication { public static void main(String[] args) { SpringApplication.run(ATCoudShardingAccountApplication.class, args); } }
-
启动Springboot工程
在HTTP请求工具中访问account的Put请求,参数是:money=100&id=1
HTTP请求工具
程序控制台
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB3容器的MySQL数据库 docker exec -it MySQLDB3 mysql -u root -p -- 还原t_account表数据 update db3.t_account set money=200 where id=1;
执行结果截图:
搭建seata-at-springcloud-sharding-business模块,作为业务模块
-
在seata-at-springcloud-sharding-business模块中,找到pom.xml文件,引入SpringBoot的Web依赖、seata-at-springcloud-common公共模块依赖、Nacos注册发现依赖
<parent> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-sharding-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-at-springcloud-sharding-business</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--nacos 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>seata-at-springcloud-sharding-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-at-springcloud-sharding-business模块中,创建application.yml文件,配置端口、Nacos注册发现地址
server: port: 8504 spring: application: name: seata-at-springcloud-sharding-business cloud: nacos: server-addr: localhost:8848
-
在seata-at-springcloud-sharding-bussiness模块中,创建utils包,新建OrderStatus类和URL类,用于标识订单状态和URL请求路径。
OrderStatus类
public enum OrderStatus { CREATE,UPDATING,FINISH }
URL类
public class URL { public static final String CREATE_ORDER = "http://seata-at-springcloud-sharding-order/order"; public static final String GOOD_INFO = "http://seata-at-springcloud-sharding-good/good/%d"; public static final String GOOD_REDUCE = "http://seata-at-springcloud-sharding-good/good?num=%d&goodId=%d"; public static final String ACCOUNT_REDUCE = "http://seata-at-springcloud-sharding-account/account?money=%f&id=%d"; }
-
在seata-at-springcloud-sharding-business模块中,创建service包,新建BusinessService接口,定义新增的方法
import org.springframework.stereotype.Service; @Service public interface BusinessService { public void placeOrder(Integer accountId,Integer goodId,Integer num); }
-
在service包内创建impl包,新建BusinessServiceImpl类,实现接口BusinessService,并实现其接口方法
import org.example.entity.Good; import org.example.entity.Order; import org.example.service.BusinessService; import org.example.utils.OrderStatus; import org.example.utils.URL; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class BusinessServiceImpl implements BusinessService { @Autowired private RestTemplate restTemplate; @Override public void placeOrder(Integer accountId, Integer goodId, Integer num) { // 查询商品信息 Good good = restTemplate.getForObject(String.format(URL.GOOD_INFO, goodId), Good.class); double allPay = good.getPrice() * num; Order order = new Order().setStatus(OrderStatus.CREATE.name()).setAccountId(accountId).setGoodId(goodId).setGoodCount(num).setAllPay(allPay); // 下订单 HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); HttpEntity<Order> httpEntity = new HttpEntity<>(order, httpHeaders); restTemplate.postForObject(URL.CREATE_ORDER, httpEntity, String.class); // 减库存 restTemplate.put(String.format(URL.GOOD_REDUCE, num, goodId), null); // 扣钱 restTemplate.put(String.format(URL.ACCOUNT_REDUCE, allPay, accountId), null); } }
-
在seata-at-springcloud-sharding-business模块中,创建controller包,新建BusinessController类,定义web请求接口的方法
import org.example.service.BusinessService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class BusinessController { @Autowired private BusinessService businessService; @GetMapping("placeOrder") public String placeOrder(Integer accountId, Integer goodId, Integer num) { businessService.placeOrder(accountId, goodId, num); return "success"; } }
-
在根包(org.example)下,新建一个ATCloudShardingBusinessApplication启动类,编写Springboot启动代码
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication(exclude= {DataSourceAutoConfiguration.class}) public class ATCloudShardingBusinessApplication { public static void main(String[] args) { SpringApplication.run(ATCloudShardingBusinessApplication.class, args); } @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }
-
启动Springboot工程
在HTTP请求工具中访问placeOrder的Get请求,参数是:accountId=1&goodId=1&num=1
HTTP请求工具
Business程序控制台
Order程序控制台
Good程序控制台
Account程序控制台
-
查看数据库数据变化
执行之前
order数据库
good数据库
account数据库
执行之后
order数据库
good数据库
account数据库
-
恢复数据库原始数据
# 进入MySQLDB3容器的MySQL数据库 docker exec -it MySQLDB3 mysql -u root -p -- 还原t_account表数据 update db3.t_account set money=200 where id=1; # 进入MySQLDB4容器的MySQL数据库 docker exec -it MySQLDB4 mysql -u root -p -- 还原t_good表数据 update db4.t_good set stock=200 where id=1; # 进入MySQLDB5容器的MySQL数据库 docker exec -it MySQLDB5 mysql -u root -p -- 清空t_order_0表数据 truncate db5.t_order_0; -- 清空t_order_1表数据 truncate db5.t_order_1; -- 清空t_order_0表数据 truncate db5.t_order_2; # 进入MySQLDB6容器的MySQL数据库 docker exec -it MySQLDB6 mysql -u root -p -- 清空t_order_0表数据 truncate db6.t_order_0; -- 清空t_order_1表数据 truncate db6.t_order_1; -- 清空t_order_0表数据 truncate db6.t_order_2;
执行结果截图:
7.4.3 微服务 跨数据库跨服务+分库分表 改造 spring-cloud-starter-alibaba-seata包解决
-
在包含分布式事务的数据库中,加入undo_log表结构
-- 注意此处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;
执行效果截图
-
分别为数据库db3、db4和db5中的t_account、t_good、t_order表定义主键(全局锁由表名和操作记录的主键 按照⼀定的规律组成)
# 进入MySQLDB3容器的MySQL数据库 docker exec -it MySQLDB3 mysql -u root -p -- 数据库db3的t_account表定义主键 ALTER TABLE db3.t_account ADD CONSTRAINT t_account_PK PRIMARY KEY (id); # 进入MySQLDB4容器的MySQL数据库 docker exec -it MySQLDB4 mysql -u root -p --数据库db4的t_good表定义主键 ALTER TABLE db4.t_good ADD CONSTRAINT t_good_PK PRIMARY KEY (id); # 进入MySQLDB5容器的MySQL数据库 docker exec -it MySQLDB5 mysql -u root -p --数据库db5的t_order_0表定义主键 ALTER TABLE db5.t_order_0 ADD CONSTRAINT t_order_0_PK PRIMARY KEY (id); --数据库db5的t_order_1表定义主键 ALTER TABLE db5.t_order_1 ADD CONSTRAINT t_order_1_PK PRIMARY KEY (id); --数据库db5的t_order_2表定义主键 ALTER TABLE db5.t_order_2 ADD CONSTRAINT t_order_2_PK PRIMARY KEY (id); # 进入MySQLDB6容器的MySQL数据库 docker exec -it MySQLDB6 mysql -u root -p --数据库db6的t_order_0表定义主键 ALTER TABLE db6.t_order_0 ADD CONSTRAINT t_order_0_PK PRIMARY KEY (id); --数据库db6的t_order_1表定义主键 ALTER TABLE db6.t_order_1 ADD CONSTRAINT t_order_1_PK PRIMARY KEY (id); --数据库db6的t_order_2表定义主键 ALTER TABLE db6.t_order_2 ADD CONSTRAINT t_order_2_PK PRIMARY KEY (id);
-
在seata-at-springcloud-sharding-common模块中,找到pom.xml文件,加入spring-cloud-starter-alibaba-seata包依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
-
在seata-at-springcloud-sharding-order、seata-at-springcloud-sharding-good、seata-at-springcloud-sharding-account、seata-at-springcloud-sharding-business模块中,找到application.yml文件,加入Seata配置信息
在seata-at-springcloud-sharding-order模块中,找到application.yml文件,加入Seata配置信息
seata: service: vgroup-mapping: default_tx_group: default grouplist: default: localhost:8091 tx-service-group: default_tx_group enable-auto-data-source-proxy: false # 开启自动代理 默认值是true 可以不屑 # data-source-proxy-mode: AT # 声明seata使用AT模式解决分布式
在seata-at-springcloud-sharding-good、seata-at-springcloud-sharding-account、seata-at-springcloud-sharding-business模块中,找到application.yml文件,加入Seata配置信息
seata: service: vgroup-mapping: default_tx_group: default grouplist: default: localhost:8091 tx-service-group: default_tx_group enable-auto-data-source-proxy: true # 开启自动代理 默认值是true 可以不屑 data-source-proxy-mode: AT # 声明seata使用AT模式解决分布式
-
在seata-at-springcloud-sharding-good、seata-at-springcloud-sharding-account模块中,创建config包,新建DBConfig类,更改数据源连接为Druid
import com.alibaba.druid.pool.DruidDataSource; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class DBConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return new DruidDataSource(); } }
-
在seata-at-springcloud-sharding-order模块中,找到pom.xml文件,加入shardingsphere-transaction-base-seata-at包依赖
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-transaction-base-seata-at</artifactId> <version>5.1.0</version> </dependency>
-
在seata-at-springcloud-sharding-order模块中,resources资源目录下,编写seata.conf配置文件,供SeataATShardingSphereTransactionManager类使用
sharding.transaction.seata.at.enable=true client.application.id=seata-at-springcloud-sharding-order client.transaction.service.group=default_tx_group sharding.transaction.seata.tx.timeout=60
-
在seata-at-springcloud-sharding-order模块中,分库分表的分布式事务的方法内,执行业务代码之前加入事务声明
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.apache.shardingsphere.transaction.core.TransactionType; import org.apache.shardingsphere.transaction.core.TransactionTypeHolder; import org.example.entity.Order; import org.example.mapper.OrderMapper; import org.example.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.swing.*; import java.util.Random; @Service public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService { @Autowired private OrderMapper orderMapper; @Override public void addOrder(Order order) { TransactionTypeHolder.set(TransactionType.BASE); if (order.getId() == null) { Random rand = new Random(); int temp = rand.nextInt(100000); order.setId(temp); } orderMapper.insert(order); } }
-
在seata-at-springcloud-sharding-business模块中,找到BusinessServiceImpl类的placeOrde方法,加入@GlobalTransactional注解
import io.seata.spring.annotation.GlobalTransactional; import org.example.entity.Good; import org.example.entity.Order; import org.example.service.BusinessService; import org.example.utils.OrderStatus; import org.example.utils.URL; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class BusinessServiceImpl implements BusinessService { @Autowired private RestTemplate restTemplate; @Override @GlobalTransactional public void placeOrder(Integer accountId, Integer goodId, Integer num) { // 查询商品信息 Good good = restTemplate.getForObject(String.format(URL.GOOD_INFO, goodId), Good.class); double allPay = good.getPrice() * num; Order order = new Order().setStatus(OrderStatus.CREATE.name()).setAccountId(accountId).setGoodId(goodId).setGoodCount(num).setAllPay(allPay); // 下订单 HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); HttpEntity<Order> httpEntity = new HttpEntity<>(order, httpHeaders); restTemplate.postForObject(URL.CREATE_ORDER, httpEntity, String.class); // 减库存 restTemplate.put(String.format(URL.GOOD_REDUCE, num, goodId), null); // 扣钱 restTemplate.put(String.format(URL.ACCOUNT_REDUCE, allPay, accountId), null); } }
-
在seata-at-springcloud-sharding-business、seata-at-springcloud-sharding-order、seata-at-springcloud-sharding-good、seata-at-springcloud-sharding-account、seata-at-springcloud-sharding-commoon模块中,所有模块的pom.xml文件中加入java版本
<properties> <java.version>11</java.version> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
-
运行模块加入配置,Modify options → Add VM options
--add-opens java.base/java.lang=ALL-UNNAMED
7.4.4 微服务 跨数据库跨服务+分库分表 测试
-
正确请求分布式事务,正确处理
在HTTP请求工具中发送两次placeOrder的Get成功请求,参数为:accountId=1&goodId=1&num=1
HTTP请求工具
business程序控制台
order程序控制台
good程序控制台
account程序控制台
-
查看数据库数据变化
执行之前
order数据库
good数据库
account数据库
执行之后
order数据库
good数据库
account数据库
-
正确请求分布式事务,异常处理
在HTTP请求工具中发送两次placeOrder的Get成功请求,参数为:accountId=1&goodId=1&num=1
HTTP请求工具
business程序控制台
order程序控制台
good程序控制台
account程序控制台
-
查看数据库数据变化
执行之前
order数据库
good数据库
account数据库
执行之后
order数据库
good数据库
account数据库
8. Seata XA模式和AT模式在不同场景下解决方案比较总结
8.1 多数据源情况
XA 模式:
- 添加依赖
- 编写配置
- 开启XA的自动代理
- 使用注解@GlobalTransactional
- XA 手动代理时,需要关闭自动代理配置,并使用DatasourceProxyXA数据源
AT 模式:
- 添加undo_log表结构
- 添加依赖
- 编写配置
- 直接使用注解@GlobalTransactional
- AT 手动代理时,需要关闭自动代理,并且使用DatasourceProxy数据源
8.2 多数据源情况
XA 模式:
注意:Seata默认情况下不支持分库分表XA模式(分库分表使用shardingsphere)
- 添加依赖
- @Transactional+@ShardingSphereTransactionType(TransactionType.XA)
AT 模式:
- 添加undo_log表结构
- 添加依赖(Seata包+shardingsphere-transaction-base-seata-at包)
- 编写Seata的配置
- 编写seata.conf的配置,根据SeataATShardingSphereTransactionManager的无参构造进行配置
- @Transactional+@ShardingSphereTransactionType(TransactionType.BASE)
8.3 微服务情况
XA 模式:
- 添加依赖(分布式事务都需要添加)
- 编写配置(分布式事务都需要添加)
- 开启并指定使用XA的自动代理
- 添加注解@GlobalTransactional
AT 模式:
- 添加undo_log表结构
- 添加依赖(分布式事务都需要添加)
- 编写配置(分布式事务都需要添加)
- 添加注解@GlobalTransactional
9. 弱一致性:TCC
9.1 什么是TCC模式
TCC(Try-Confirm-Cancel)模式是Seata分布式事务框架中的一个重要模式。Seata是一个开源的分布式事务解决方案,旨在简化分布式环境下的事务管理。TCC模式主要针对微服务架构下的分布式事务问题,提供一种高性能、易用的解决方案。
在TCC模式中,事务的处理过程分为三个阶段:
- Try 阶段:尝试执行事务操作。在这个阶段,所有的服务都会进行资源的检查和预留,但不会提交事务。如果所有的服务都能成功执行这个操作,那么事务可以继续进行。否则,事务会被回滚。
- Confirm 阶段:如果所有的服务都在Try阶段成功执行了操作,那么Confirm阶段将提交所有的事务。这个阶段的主要目的是确保所有的服务都能成功地完成实际的操作,并确保数据一致性。
- Cancel 阶段:如果Try阶段有任何服务执行失败,那么Cancel阶段将回滚所有的事务。这个阶段的主要目的是确保在发生错误时,所有服务都能恢复到事务执行之前的状态。
9.2 Seata TCC模式的原理流程
TCC 原理流程主要包括三个步骤:尝试(Try)、确认(Confirm)和取消(Cancel)。
- 尝试(Try)阶段:
a. 事务发起方发起一个请求,要求执行一个包含多个参与者的分布式事务。
b. 事务协调器(Seata Server)为该事务生成一个全局唯一的事务ID。
c. 事务参与者(微服务)收到请求后,执行相应的本地事务操作,但不提交。
d. 事务参与者将本地事务操作的结果(成功或失败)上报给事务协调器。 - 确认(Confirm)阶段:
a. 如果所有参与者的本地事务操作都成功,事务协调器通知所有参与者提交本地事务。
b. 参与者收到提交通知后,将本地事务提交。
c. 事务协调器将全局事务标记为完成。 - 取消(Cancel)阶段:
a. 如果有任何一个参与者的本地事务操作失败,事务协调器通知所有参与者取消本地事务。
b. 参与者收到取消通知后,执行与本地事务操作相反的操作,以撤销之前的操作。
c. 事务协调器将全局事务标记为取消。
9.3 Seata TCC模式的优缺点
TCC模式的优点:
- 高性能:因为Try阶段不会真正提交事务,所以性能损耗相对较小。
- 可靠性:通过三个阶段的处理,确保事务的一致性和完整性。
- 易于理解:TCC模式提供了一种直观的事务处理方式,便于开发者理解和实现。
TCC模式的缺点:
- 实现复杂度:TCC模式需要为每个服务实现Try、Confirm和Cancel三个操作,这可能会增加开发工作量和维护成本。
- 数据一致性:在Try、Confirm和Cancel阶段之间,数据可能处于不一致的状态。开发者需要确保在实现过程中充分考虑这一问题。
9.3 Seata TCC模式与AT模式相同点和不同点
相同点:
- 都是 Seata 提供的分布式事务解决方案。
- 都可以确保分布式系统中的数据一致性。
- 都支持多种类型的资源,如数据库、消息队列等。
不同点:
- 实现原理:
- TCC 模式基于补偿机制,分为三个阶段:Try(尝试)、Confirm(确认)和 Cancel(取消)。在 Try 阶段,尝试执行所有涉及的事务,但不提交。在 Confirm 阶段,如果所有 Try 操作都成功,则提交所有事务。在 Cancel 阶段,如果存在失败的 Try 操作,则回滚相应的事务。
- AT 模式则依赖于底层数据资源的事务支持。在 AT 模式下,Seata 通过一阶段提交和二阶段提交来保证分布式事务的一致性。一阶段提交时,所有涉及的资源都将被锁定。在二阶段提交时,如果所有资源都已锁定,则提交事务;如果存在失败的锁定,则回滚事务。
- 业务侵入性:
- TCC 模式需要对业务逻辑进行改造,引入 Try、Confirm 和 Cancel 三个接口。因此,业务侵入性较高。
- AT 模式对业务逻辑的侵入性较低,只需在代码中添加少量注解即可。
- 性能:
- TCC 模式的性能可能略低于 AT 模式,因为 TCC 需要处理更多的接口调用和补偿操作。
- AT 模式的性能相对较高,因为它依赖于底层数据资源的事务支持。
- 适用场景:
- TCC 模式适用于对数据一致性要求较高且能够接受一定业务改造的场景。
- AT 模式适用于对性能要求较高且希望降低业务侵入性的场景。
9.4 TCC需要解决的问题
- 幂等性(Idempotence):
幂等性是指在分布式系统中,多次执行同一操作,得到的结果与执行一次相同。在TCC中,幂等性主要体现在Confirm和Cancel阶段。幂等性的实现可以通过在数据库中记录操作的状态,以便在重复执行操作时能够判断操作是否已经完成,从而避免重复执行导致的数据不一致。 - 空回滚(No-op rollback):
空回滚是指在TCC事务中,如果一个分支事务在Try阶段执行成功,但在Confirm阶段执行之前,全局事务已经结束(提交或回滚),那么Confirm阶段将不再执行。这种情况称为空回滚。空回滚的实现可以通过在数据库记录分支事务的状态,并在Confirm阶段判断事务是否已经提交或回滚。 - 悬挂(Hanging):
悬挂是指在TCC事务中,如果一个分支事务在Try阶段执行成功,但在Confirm或Cancel阶段执行之前,全局事务协调者出现故障,导致无法确定全局事务的最终状态(提交或回滚),从而使得分支事务一直处于等待状态。这种情况称为悬挂。悬挂的问题可以通过引入超时机制、重试机制或者监控系统来解决,以便在超时后自动执行重试或者报警。
TCC通过将事务过程拆分为三个阶段,并引入幂等性、空回滚和悬挂等概念,有效地解决了分布式系统中的数据一致性问题。
10. Seata分布式事务实现TCC模式
10.1 Springboot项目 实现 单体项目多数据源的分布式事务
10.1.1 准备阶段
Docker规划:
- 创建两个MySQL数据库容器,名称分别是MySQLDB1和MySQLDB2
- 容器名为MySQLDB1端口对外映射为3307
- 容器名为MySQLDB2端口对外映射为3308
# 查看MySQL镜像
docker search mysql
# 拉取MySQL镜像
docker pull mysql
# 查看拉取的镜像
docker images
# 运行名为MySQLDB1容器名的MySQL镜像
docker run -d --name MySQLDB1 -p3307:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 运行名为MySQLDB2容器名的MySQL镜像
docker run -d --name MySQLDB2 -p3308:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 查看运行的容器
docker ps
- 创建一个seata分布式容器,名称是seata-server
- 容器名为seata-server端口对外映射为7091和8091
# 查看seata镜像
docker search seata
# 拉取seata镜像
docker pull seataio/seata-server
# 查看拉取的镜像
docker images
# 运行名为seata-server容器名的seataio/seata-server镜像
docker run -d --name seata-server -p7091:7091 -p8091:8091 seataio/seata-server:latest
# 查看运行的容器
docker ps
MySQL规划:
- 在MySQLDB1容器运行的MySQL中创建db1数据库
- 在db1数据库中创建t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- 在db1数据库创建的t_account数据库表中插入数据(1,'张三',200)
- 在MySQLDB2容器运行的MySQL中创建db2数据库
- 在db2数据库中创建t_user_cash数据库表
- t_user_cash数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- 在db2数据库创建的t_user_cash数据库表中插入数据(2,'李四',200)
# 进入MySQLDB1容器的MySQL数据库
docker exec -it MySQLDB1 mysql -u root -p
-- 创建db1数据库
CREATE DATABASE db1;
USE db1;
-- 在db1数据库中创建t_account数据库表
CREATE TABLE `t_account` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`money` int DEFAULT NULL COMMENT '金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息';
-- 在db1数据库创建的t_account数据库表中插入数据(1,'张三',200)
INSERT INTO db1.`t_account` (`id`,`name`,`money`) VALUES (1,'张三',200);
# 进入MySQLDB2容器的MySQL数据库
docker exec -it MySQLDB2 mysql -u root -p
-- 创建db2数据库
CREATE DATABASE db2;
USE db2;
-- 在db2数据库中创建t_user_cash数据库表
CREATE TABLE `t_user_cash` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`money` int DEFAULT NULL COMMENT '金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息';
-- 在db2数据库创建的t_user_cash数据库表中插入数据(2,'李四',200)
INSERT INTO db2.`t_user_cash` (`id`,`name`,`money`) VALUES (2,'李四',200);
项目规划:
- 打开IDEA,创建一个Empty Project空工程,Name输入Seata-Code
- 在刚创建的Seata-Code工程上,右键创建模块,Name输入seata-tcc
- 在seata-tcc模块中,右键创建模块,Name输入seata-tcc-multiresource-demo
执行效果截图:
10.1.2 搭建基础环境
-
在seata-tcc模块中,找到pom.xml文件,引入SpringBoot的父工程依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.8</version> <relativePath/> </parent> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>
-
在seata-tcc-multiresource-demo模块中,找到pom.xml文件,引入SpringBoot依赖、MySQL依赖、Mybatis-plus数据操作组件、druid数据库池
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.6</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-tcc-multiresource-demo模块中,创建application.yml文件,配置端口和多数据源信息
server: port: 8600 spring: datasource: db1: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3307/db1?serverTimezone=UTC&auto username: root password: ZYMzym111 db2: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3308/db2?serverTimezone=UTC username: root password: ZYMzym111 application: name: seata-tcc-mutilsource
-
在seata-tcc-multiresource-demo模块中,创建config包,新建DBConfig类初始化多数据源
import com.alibaba.druid.pool.DruidDataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScans; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import javax.sql.DataSource; @SpringBootConfiguration @MapperScans({ @MapperScan(basePackages = "org.example.mapper.db1", sqlSessionFactoryRef = "db1SessionFactory"), @MapperScan(basePackages = "org.example.mapper.db2", sqlSessionFactoryRef = "db2SessionFactory"), }) public class DBConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.db1") public DataSource db1Datasource() { return new DruidDataSource(); } @Bean @ConfigurationProperties(prefix = "spring.datasource.db2") public DataSource db2Datasource() { return new DruidDataSource(); } @Bean public SqlSessionFactory db1SessionFactory(@Qualifier("db1Datasource") DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean.getObject(); } @Bean public SqlSessionFactory db2SessionFactory(@Qualifier("db2Datasource") DataSource dataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); return sqlSessionFactoryBean.getObject(); } }
-
在seata-tcc-multiresource-demo模块中,创建的config包中MapperScan中扫描到的两个包(mapper/db1,mapper/db2)。在mapper/db1中,新建AccountMapper类,实现转入逻辑;在mapper/db2中,新建UserCashMapper类,实现转出逻辑
mapper/db1中,AccountMapper类
import org.apache.ibatis.annotations.Update; public interface AccountMapper { @Update("update t_account set money=money-#{money} where id=1") public void reduceMoney(int money); }
mapper/db2中,UserCashMapper类
import org.apache.ibatis.annotations.Update; public interface UserCashMapper { @Update("update t_user_cash set money=money+${money} where id=2") public void increMoney(int money); }
-
在seata-tcc-multiresource-demo模块中,创建service包,新建AccountService接口和UserCashService接口,定义转的方法
public interface AccountService { public void reduceMoney(int money); }
public interface UserCashService { public void increMoney(int money); }
-
在service包内创建impl包,新建AccountServiceImpl类,继承接口AccountService,并实现其接口方法;新建UserCashServiceImpl类,继承接口UserCashService,并实现其接口方法
import org.example.mapper.db1.AccountMapper; import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountMapper accountMapper; @Override public void reduceMoney(int money) { accountMapper.reduceMoney(money); } }
import org.example.mapper.db2.UserCashMapper; import org.example.service.UserCashService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserCashServiceImpl implements UserCashService { @Autowired private UserCashMapper userCashMapper; @Override public void increMoney(int money) { userCashMapper.increMoney(money); } }
-
在seata-tcc-multiresource-demo模块中,创建business包,新建MoneyService接口,定义转的方法
public interface MoneyService { public void transfer(int money); }
-
在business包内创建impl包,新建MoneyServiceImpl类,继承接口MoneyService,并实现其接口方法
import org.example.business.MoneyService; import org.example.service.AccountService; import org.example.service.UserCashService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MoneyServiceImpl implements MoneyService { @Autowired(required = false) private AccountService accountService; @Autowired(required = false) private UserCashService userCashService; @Override public void transfer(int money) { accountService.reduceMoney(money); userCashService.increMoney(money); } }
-
在seata-at-multiresource-demo模块中,创建controller包,新建MoneyController类,定义web请求接口的方法
import org.example.business.MoneyService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class MoneyController { @Autowired private MoneyService moneyService; @GetMapping("/transfer") public String transfer(int money) { moneyService.transfer(money); return "success"; } }
-
在根包(org.example)下,新建一个TCCMutilApplication启动类,编写Springboot启动代码
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class TCCMutilApplication { public static void main(String[] args) { SpringApplication.run(TCCMutilApplication.class); } }
-
启动Springboot工程,使用HTTP请求工具发送Get请求 localhost:8600/transfer?money=100
HTTP请求工具
程序控制台
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB1容器的MySQL数据库 docker exec -it MySQLDB1 mysql -u root -p -- 将张三数据重置为200 update db1.t_account set money=200 where id=1; # 进入MySQLDB2容器的MySQL数据库 docker exec -it MySQLDB2 mysql -u root -p -- 将李四数据重置为200 update db2.t_user_cash set money=200 where id=2;
执行结果截图:
10.1.3 Springboot项目 单体项目多数据源 改造一(基础改造,未解决幂等性、空回滚、悬挂问题)
-
在包含分布式事务的数据库表中,加入标识字段
# 进入MySQLDB1容器的MySQL数据库 docker exec -it MySQLDB1 mysql -u root -p -- 将张三数据重置为200 alter table db1.t_account add column transfer int default 0 comment '转钱标识'; # 进入MySQLDB2容器的MySQL数据库 docker exec -it MySQLDB2 mysql -u root -p -- 将李四数据重置为200 alter table db2.t_user_cash add column transfer int default 0 comment '转钱标识';
执行效果截图
-
在seata-tcc-multiresource-demo模块中,找到pom.xml文件,添加seata-spring-boot-start依赖
<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.6.1</version> </dependency>
-
在seata-tcc-multiresource-demo模块中,找到application.yml配置文件,添加seata配置信息
server: port: 8600 spring: datasource: db1: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3307/db1?serverTimezone=UTC&auto username: root password: ZYMzym111 db2: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3308/db2?serverTimezone=UTC username: root password: ZYMzym111 application: name: seata-tcc-mutilsource seata: service: vgroup-mapping: default_tx_group: default grouplist: default: localhost:8091 tx-service-group: default_tx_group enable-auto-data-source-proxy: false # 开启自动代理 默认值是true 可以不屑
-
在seata-tcc-multiresource-demo模块中,AccountMapper类,实现转出逻辑的提交和回滚;UserCashMapper类,实现转入逻辑的提交和回滚
mapper/db1中,AccountMapper类
import org.apache.ibatis.annotations.Update; public interface AccountMapper { @Update("update t_account set money=money-#{money},transfer=transfer+#{money} where id=1") public void reduceMoney(int money); @Update("update t_account set transfer=transfer-#{money} where id=1") public void confirmMoney(int money); @Update("update t_account set money=money+#{money},transfer=transfer-#{money} where id=1") public void rollbackMoney(int money); }
mapper/db2中,UserCashMapper类
import org.apache.ibatis.annotations.Update; public interface UserCashMapper { @Update("update t_user_cash set money=money+${money},transfer=transfer-#{money} where id=2") public void increMoney(int money); @Update("update t_user_cash set transfer=transfer+#{money} where id=2") public void confirmMoney(int money); @Update("update t_user_cash set money=money-#{money},transfer=transfer+#{money} where id=2") public void rollbackMoney(int money); }
-
在seata-tcc-multiresource-demo模块中,AccountService接口和UserCashService接口,定义转方法提交和回滚,并添加事务注解
import io.seata.rm.tcc.api.BusinessActionContext; import io.seata.rm.tcc.api.BusinessActionContextParameter; import io.seata.rm.tcc.api.LocalTCC; import io.seata.rm.tcc.api.TwoPhaseBusinessAction; @LocalTCC public interface AccountService { @TwoPhaseBusinessAction(name = "reduceMoney", commitMethod = "confirmMoney", rollbackMethod = "rollbackMoney") public void reduceMoney(BusinessActionContext context, @BusinessActionContextParameter("money") int money); public void confirmMoney(BusinessActionContext context); public void rollbackMoney(BusinessActionContext context); }
import io.seata.rm.tcc.api.BusinessActionContext; import io.seata.rm.tcc.api.BusinessActionContextParameter; import io.seata.rm.tcc.api.LocalTCC; import io.seata.rm.tcc.api.TwoPhaseBusinessAction; @LocalTCC public interface UserCashService { @TwoPhaseBusinessAction(name = "increMoney", commitMethod = "confirmMoney", rollbackMethod = "rollbackMoney") public void increMoney(BusinessActionContext context, @BusinessActionContextParameter("money") int money); public void confirmMoney(BusinessActionContext context); public void rollbackMoney(BusinessActionContext context); }
-
在service包内创建impl包,AccountServiceImpl类,继承接口AccountService,并实现其接口方法;UserCashServiceImpl类,继承接口UserCashService,并实现其接口方法
import io.seata.rm.tcc.api.BusinessActionContext; import org.example.mapper.db1.AccountMapper; import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountMapper accountMapper; @Override public void reduceMoney(BusinessActionContext context, int money) { accountMapper.reduceMoney(money); } @Override public void confirmMoney(BusinessActionContext context) { int money = Integer.parseInt(context.getActionContext("money").toString()); accountMapper.confirmMoney(money); } @Override public void rollbackMoney(BusinessActionContext context) { int money = Integer.parseInt(context.getActionContext("money").toString()); accountMapper.rollbackMoney(money); } }
import io.seata.rm.tcc.api.BusinessActionContext; import org.example.mapper.db2.UserCashMapper; import org.example.service.UserCashService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserCashServiceImpl implements UserCashService { @Autowired private UserCashMapper userCashMapper; @Override public void increMoney(BusinessActionContext context, int money) { userCashMapper.increMoney(money); } @Override public void confirmMoney(BusinessActionContext context) { int money = Integer.parseInt(context.getActionContext("money").toString()); userCashMapper.confirmMoney(money); } @Override public void rollbackMoney(BusinessActionContext context) { int money = Integer.parseInt(context.getActionContext("money").toString()); userCashMapper.rollbackMoney(money); } }
-
在business包内创建impl包,MoneyServiceImpl类,继承接口MoneyService,开启全局事务注解
import io.seata.rm.tcc.api.BusinessActionContextUtil; import io.seata.spring.annotation.GlobalTransactional; import org.example.business.MoneyService; import org.example.service.AccountService; import org.example.service.UserCashService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MoneyServiceImpl implements MoneyService { @Autowired private AccountService accountService; @Autowired private UserCashService userCashService; @Override @GlobalTransactional public void transfer(int money) { accountService.reduceMoney(BusinessActionContextUtil.getContext(), money); userCashService.increMoney(BusinessActionContextUtil.getContext(), money); } }
10.1.4 Springboot项目 单体项目多数据源 改造二(基于基础改造,解决幂等性、空回滚问题)
-
在包含分布式事务的数据库中,加入tcc_fence_log表结构,seata版本至少1.5.0,作用防悬挂
CREATE TABLE IF NOT EXISTS `tcc_fence_log` ( `xid` VARCHAR(128) NOT NULL COMMENT 'global id', `branch_id` BIGINT NOT NULL COMMENT 'branch id', `action_name` VARCHAR(64) NOT NULL COMMENT 'action name', `status` TINYINT NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)', `gmt_create` DATETIME(3) NOT NULL COMMENT 'create time', `gmt_modified` DATETIME(3) NOT NULL COMMENT 'update time', PRIMARY KEY (`xid`, `branch_id`), KEY `idx_gmt_modified` (`gmt_modified`), KEY `idx_status` (`status`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
执行效果截图
-
在seata-tcc-multiresource-demo模块下,创建utils包,新建TransactionStatusHolder类,用户存储分布式事务当前的状态
import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class TransactionStatusHolder { public static Map<Class<?>, Map<String, String>> map = new ConcurrentHashMap<>(); /** * 设置状态 */ public static void setStatus(Class<?> clazz, String key, String value) { Map<String, String> map1 = map.get(clazz); if (map1 == null) { synchronized (map) { if (map1 == null) { map1 = new ConcurrentHashMap<>(); map1.put(key, value); } } } map.put(clazz, map1); } /** * 获取状态 */ public static String getStatus(Class<?> clazz, String key) { Map<String, String> map1 = map.get(clazz); if (map1 == null) { return null; } else { return map1.get(key); } } /** * 释放状态 */ public static void releaseStatus(Class<?> clazz, String key) { Map<String, String> map1 = map.get(clazz); if (map1 != null) { map1.remove(key); } } }
-
在seata-tcc-multiresource-demo模块中,AccountMapper类,实现查询当前用户余额
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; public interface AccountMapper { @Select("select money from t_account where id=1") public int findMoney(); @Update("update t_account set money=money-#{money},transfer=transfer+#{money} where id=1") public void reduceMoney(int money); @Update("update t_account set transfer=transfer-#{money} where id=1") public void confirmMoney(int money); @Update("update t_account set money=money+#{money},transfer=transfer-#{money} where id=1") public void rollbackMoney(int money); }
-
在seata-tcc-multiresource-demo模块中,AccountService接口和UserCashService接口,加入放悬挂的注解useTCCFence = true
import io.seata.rm.tcc.api.BusinessActionContext; import io.seata.rm.tcc.api.BusinessActionContextParameter; import io.seata.rm.tcc.api.LocalTCC; import io.seata.rm.tcc.api.TwoPhaseBusinessAction; @LocalTCC public interface AccountService { @TwoPhaseBusinessAction(name = "reduceMoney", commitMethod = "confirmMoney", rollbackMethod = "rollbackMoney") public void reduceMoney(BusinessActionContext context, @BusinessActionContextParameter("money") int money); public void confirmMoney(BusinessActionContext context); public void rollbackMoney(BusinessActionContext context); }
import io.seata.rm.tcc.api.BusinessActionContext; import io.seata.rm.tcc.api.BusinessActionContextParameter; import io.seata.rm.tcc.api.LocalTCC; import io.seata.rm.tcc.api.TwoPhaseBusinessAction; @LocalTCC public interface UserCashService { @TwoPhaseBusinessAction(name = "increMoney", commitMethod = "confirmMoney", rollbackMethod = "rollbackMoney") public void increMoney(BusinessActionContext context, @BusinessActionContextParameter("money") int money); public void confirmMoney(BusinessActionContext context); public void rollbackMoney(BusinessActionContext context); }
-
在service包内创建impl包,AccountServiceImpl类,继承接口AccountService,加入分布式事务的幂等性和空回滚处理方法;UserCashServiceImpl类,继承接口UserCashService,加入分布式事务的幂等性和空回滚处理方法
import io.seata.rm.tcc.api.BusinessActionContext; import org.example.mapper.db1.AccountMapper; import org.example.service.AccountService; import org.example.utils.TransactionStatusHolder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountMapper accountMapper; @Override @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void reduceMoney(BusinessActionContext context, int money) { // 幂等性 if (!StringUtils.hasText(TransactionStatusHolder.getStatus(getClass(), context.getXid()))) { String xid = context.getXid(); if (accountMapper.findMoney() < money) { throw new RuntimeException("余额不足"); } accountMapper.reduceMoney(money); // 设置执行状态 TransactionStatusHolder.setStatus(getClass(), xid, "reduceMoney"); } } @Override public void confirmMoney(BusinessActionContext context) { // 空回滚 if (StringUtils.hasText(TransactionStatusHolder.getStatus(getClass(), context.getXid()))) { int money = Integer.parseInt(context.getActionContext("money").toString()); accountMapper.confirmMoney(money); // 幂等性 TransactionStatusHolder.releaseStatus(getClass(), context.getXid()); } } @Override public void rollbackMoney(BusinessActionContext context) { // 空回滚 if (StringUtils.hasText(TransactionStatusHolder.getStatus(getClass(), context.getXid()))) { int money = Integer.parseInt(context.getActionContext("money").toString()); accountMapper.rollbackMoney(money); // 幂等性 TransactionStatusHolder.releaseStatus(getClass(), context.getXid()); } } }
import io.seata.rm.tcc.api.BusinessActionContext; import org.example.mapper.db2.UserCashMapper; import org.example.service.UserCashService; import org.example.utils.TransactionStatusHolder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @Service public class UserCashServiceImpl implements UserCashService { @Autowired private UserCashMapper userCashMapper; @Override @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void increMoney(BusinessActionContext context, int money) { // 幂等性 if (!StringUtils.hasText(TransactionStatusHolder.getStatus(getClass(), context.getXid()))) { userCashMapper.increMoney(money); // 设置执行状态 TransactionStatusHolder.setStatus(getClass(), context.getXid(), "IncreMoney"); } } @Override public void confirmMoney(BusinessActionContext context) { // 空回滚 if (StringUtils.hasText(TransactionStatusHolder.getStatus(getClass(), context.getXid()))) { int money = Integer.parseInt(context.getActionContext("money").toString()); userCashMapper.confirmMoney(money); // 幂等性 TransactionStatusHolder.releaseStatus(getClass(), context.getXid()); } } @Override public void rollbackMoney(BusinessActionContext context) { // 空回滚 if (StringUtils.hasText(TransactionStatusHolder.getStatus(getClass(), context.getXid()))) { int money = Integer.parseInt(context.getActionContext("money").toString()); userCashMapper.rollbackMoney(money); // 幂等性 TransactionStatusHolder.releaseStatus(getClass(), context.getXid()); } } }
10.1.5 Springboot项目 单体项目多数据源 测试
-
启动Springboot工程,正常业务流程,在HTTP请求工具中访问 localhost:8600/transfer?money=100
HTTP请求工具
SpringBoot工程日志
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB1容器的MySQL数据库 docker exec -it MySQLDB1 mysql -u root -p -- 将张三数据重置为200 update db1.t_account set money=200 where id=1; # 进入MySQLDB2容器的MySQL数据库 docker exec -it MySQLDB2 mysql -u root -p -- 将李四数据重置为200 update db2.t_user_cash set money=200 where id=2;
执行结果截图:
-
重新启动Springboot工程,在HTTP请求工具中访问 localhost:8600/transfer?money=300,造成转账失败的现象
HTTP请求工具
SpringBoot工程日志
-
查看数据库数据变化
执行之前
执行之后
10.2 微服务 实现 跨数据库跨服务的分布式事务
10.2.1 准备阶段
Docker规划:
- 创建两个MySQL数据库容器,名称分别是MySQLDB3、MySQLDB4和MySQLDB5
- 容器名为MySQLDB3端口对外映射为3309
- 容器名为MySQLDB4端口对外映射为3310
- 容器名为MySQLDB5端口对外映射为3311
# 查看MySQL镜像
docker search mysql
# 拉取MySQL镜像
docker pull mysql
# 查看拉取的镜像
docker images
# 运行名为MySQLDB3容器名的MySQL镜像
docker run -d --name MySQLDB3 -p3309:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 运行名为MySQLDB4容器名的MySQL镜像
docker run -d --name MySQLDB4 -p3310:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 运行名为MySQLDB5容器名的MySQL镜像
docker run -d --name MySQLDB5 -p3311:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest
# 查看运行的容器
docker ps
- 创建一个seata分布式容器,名称是seata-server
- 容器名为seata-server端口对外映射为7091和8091
# 查看seata镜像
docker search seata
# 拉取seata镜像
docker pull seataio/seata-server
# 查看拉取的镜像
docker images
# 运行名为seata-server容器名的seataio/seata-server镜像
docker run -d --name seata-server -p7091:7091 -p8091:8091 seataio/seata-server:latest
# 查看运行的容器
docker ps
- 创建一个nacos容器,名称是nacos-server
- 容器名为nacos-server端口对外映射为8848、9848和9849
# 查看nacos镜像
docker search nacos
# 拉取nacos镜像
docker pull nacos/nacos-server
# 查看拉取的镜像
docker images
# 运行名为nacos-server容器名的nacos/nacos-server镜像
docker run -d --name nacos-server -p8848:8848 -p9848:9848 -p9849:9849 -e MODE=standalone -e JVM_XMS=512m -e JVM_XMX=512m -e JVM_XMN=256m nacos/nacos-server:latest
# 查看运行的容器
docker ps
MySQL规划:
- 在MySQLDB3容器运行的MySQL中创建db3数据库
- 在MySQLDB4容器运行的MySQL中创建db4数据库
- 在MySQLDB5容器运行的MySQL中创建db5数据库
- 在db3数据库中创建t_account数据库表
- t_account数据库表中字段:id、name、money三个字段,类型分别是:bigint、varchar(30)、int
- 在db4数据库中创建t_good数据库表
- t_good数据库表中字段:id、name、price,stock四个字段,类型分别是:bigint、varchar(30)、double、bigint
- 在db5数据库中创建t_order数据库表
- t_order数据库表中字段:id、account_id、good_id,good_count,status,all_pay六个字段,类型分别是:bigint、bigint、bigint、bigint、varchar(30)、double
- db3数据库的t_account数据库表中插入数据:(1,'张三',200)
- db4数据库的t_good数据库表中插入数据:(1,'玩具小车',80,200)
# 进入MySQLDB3容器的MySQL数据库
docker exec -it MySQLDB3 mysql -u root -p
-- 创建db3数据库
CREATE DATABASE db3;
USE db3;
-- 在db3数据库中创建t_account数据库表
CREATE TABLE `t_account` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`money` int DEFAULT NULL COMMENT '金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户信息';
-- 在db3数据库的t_account数据库表中插入数据
INSERT INTO db3.`t_account` (`id`,`name`,`money`) VALUES (1,'张三',200);
# 进入MySQLDB4容器的MySQL数据库
docker exec -it MySQLDB4 mysql -u root -p
-- 创建db4数据库
CREATE DATABASE db4;
USE db4;
-- 在db4数据库中创建t_good数据库表
CREATE TABLE `t_good` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(30) DEFAULT NULL COMMENT '商品名',
`price` double DEFAULT NULL COMMENT '价格',
`stock` bigint DEFAULT NULL COMMENT '库存'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品信息';
-- 在db4数据库的t_good数据库表中插入数据
INSERT INTO db4.`t_good` (`id`,`name`,`price`,`stock`) VALUES (1,'玩具小车',80,200);
# 进入MySQLDB5容器的MySQL数据库
docker exec -it MySQLDB5 mysql -u root -p
-- 创建db5数据库
CREATE DATABASE db5;
USE db5;
-- 在db5数据库中创建t_order数据库表
CREATE TABLE `t_order` (
`id` bigint NOT NULL COMMENT '主键',
`account_id` bigint NOT NULL COMMENT '账户主键',
`good_id` bigint NOT NULL COMMENT '商品主键',
`good_count` bigint NOT NULL COMMENT '商品数量',
`status` varchar(30) DEFAULT NULL COMMENT '订单状态',
`all_pay` double DEFAULT NULL COMMENT '支付总金额'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单信息';
项目规划:
- 打开IDEA,创建一个Empty Project空工程,Name输入Seata-Code
- 在刚创建的Seata-Code工程上,右键创建模块,Name输入seata-tcc
- 在seata-tcc模块中,右键创建模块,Name输入seata-tcc-springcloud-demo,注意不依赖于seata-tcc模块
- 在seata-tcc-springcloud-demo模块下,创建seata-tcc-springcloud-common模块,作为公共实体类模块
- 在seata-tcc-springcloud-demo模块下,创建seata-tcc-springcloud-order模块,作为订单模块
- 在seata-tcc-springcloud-demo模块下,创建seata-tcc-springcloud-good模块,作为商品模块
- 在seata-tcc-springcloud-demo模块下,创建seata-tcc-springcloud-account模块,作为用户模块
- 在seata-tcc-springcloud-demo模块下,创建seata-tcc-springcloud-business模块,作为业务模块
执行效果截图:
10.2.2 搭建基础环境
搭建seata-tcc-springcloud-demo模块,作为父级工程
-
在seata-tcc-springcloud-demo模块中,找到pom.xml文件,引入SpringBoot的父工程依赖、SpringCloud依赖、SpringCloudAlibaba依赖
<groupId>org.example</groupId> <artifactId>seata-tcc-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <modules> <module>seata-tcc-springcloud-common</module> <module>seata-tcc-springcloud-good</module> <module>seata-tcc-springcloud-account</module> <module>seata-tcc-springcloud-business</module> </modules> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <maven.compiler.compilerVersion>11</maven.compiler.compilerVersion> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring-cloud.version>Hoxton.SR12</spring-cloud.version> <spring-cloud-alibaba.version>2.2.9.RELEASE</spring-cloud-alibaba.version> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.12.RELEASE</version> <relativePath/> </parent> <dependencyManagement> <dependencies> <!--SpringCloud依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--SpringCloudAlibaba依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
搭建seata-tcc-springcloud-common模块,作为公共实体类模块
-
在seata-tcc-springcloud-common模块中,找到pom.xml文件,引入lombok自动插入依赖、Mybatis-plus数据操作组件
<parent> <groupId>org.example</groupId> <artifactId>seata-tcc-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-tcc-springcloud-common</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>
-
在seata-tcc-springcloud-common模块中,创建entity包,新建Account实体类,与数据表t_account对应;新建Good实体类,与数据表t_good对应;新建Order实体类,与数据表t_order对应;
新建Account实体类,与数据表t_account对应
import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) @TableName("t_account") public class Account { private Integer id; private String name; private Integer money; }
新建Good实体类,与数据表t_good对应
import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) @TableName("t_good") public class Good { private Integer id; private String name; private double price; private int stock; }
新建Order实体类,与数据表t_order对应
import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) @TableName("t_order") public class Order { private Integer id; private Integer accountId; private Integer goodId; private Integer goodCount; private String status; private double allPay; }
搭建seata-tcc-springcloud-order模块,作为订单模块
-
在seata-tcc-springcloud-order模块中,找到pom.xml文件,引入SpringBoot的Web依赖、MySQL连接依赖、seata-tcc-springcloud-common公共模块依赖、Nacos注册发现依赖
<parent> <groupId>org.example</groupId> <artifactId>seata-tcc-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-tcc-springcloud-order</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--nacos 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>seata-tcc-springcloud-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-tcc-springcloud-order模块中,创建application.yml文件,配置端口、数据源信息、MyBatis-plus基础配置、Nacos注册发现地址
server: port: 8601 spring: application: name: seata-tcc-springcloud-order datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3311/db5?serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ZYMzym111 cloud: nacos: server-addr: localhost:8848 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true
-
在seata-tcc-springcloud-order模块中,找到main/java目录创建mapper包,新建OrderMapper类,继承BaseMapper;找到mian/resource目录创建mapper文件夹,新建OrderMapper.xml文件。
main/java目录下mapper包,OrderMapper类
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.example.entity.Order; import org.springframework.stereotype.Repository; @Repository public interface OrderMapper extends BaseMapper<Order> { }
main/resource目录下mapper文件夹,OrderMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.example.mapper.OrderMapper"> </mapper>
-
在seata-tcc-springcloud-order模块中,创建service包,新建OrderService接口,定义新增的方法
import com.baomidou.mybatisplus.extension.service.IService; import org.example.entity.Order; import org.springframework.stereotype.Service; @Service public interface OrderService extends IService<Order> { public void addOrder(Order order); }
-
在service包内创建impl包,新建OrderServiceImpl类,实现接口OrderService,并实现其接口方法
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.example.entity.Order; import org.example.mapper.OrderMapper; import org.example.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Random; @Service public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService { @Autowired private OrderMapper orderMapper; @Override public void addOrder(Order order) { if (order.getId() == null) { Random rand = new Random(); int temp = rand.nextInt(100000); order.setId(temp); } orderMapper.insert(order); } }
-
在seata-tcc-springcloud-order模块中,创建controller包,新建OrderController类,定义web请求接口的方法
import org.example.entity.Order; import org.example.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("order") public class OrderController { @Autowired private OrderService orderService; @PostMapping public String addOrder(@RequestBody Order order) { orderService.addOrder(order); return "success"; } }
-
在根包(org.example)下,新建一个TCCOrderApplication启动类,编写Springboot启动代码
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan(basePackages = {"org.example.mapper"}) public class TCCOrderApplication { public static void main(String[] args) { SpringApplication.run(TCCOrderApplication.class, args); } }
-
启动Springboot工程
在HTTP请求工具中访问order的Post请求,参数分别是:{ "id": 1, "accountId": 1, "goodId": 1, "goodCount": 2 }
HTTP请求工具
程序控制台
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB5容器的MySQL数据库 docker exec -it MySQLDB5 mysql -u root -p -- 清空t_order表数据 truncate db5.t_order;
执行结果截图:
搭建seata-tcc-springcloud-good模块,作为商品模块
-
在seata-tcc-springcloud-good模块中,找到pom.xml文件,引入SpringBoot的Web依赖、MySQL连接依赖、seata-tcc-springcloud-common公共模块依赖、Nacos注册发现依赖
<parent> <groupId>org.example</groupId> <artifactId>seata-tcc-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-tcc-springcloud-good</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--nacos 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>seata-tcc-springcloud-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-tcc-springcloud-good模块中,创建application.yml文件,配置端口、数据源信息、MyBatis-plus基础配置、Nacos注册发现地址
server: port: 8602 spring: application: name: seata-at-springcloud-good datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3310/db4?serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ZYMzym111 cloud: nacos: server-addr: localhost:8848 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true
-
在seata-tcc-springcloud-good模块中,找到main/java目录创建mapper包,新建GoodMapper类,继承BaseMapper;找到mian/resource目录创建mapper文件夹,新建GoodMapper.xml文件。
main/java目录下mapper包,GoodMapper类
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; import org.example.entity.Good; import org.springframework.stereotype.Repository; @Repository public interface GoodMapper extends BaseMapper<Good> { @Update("update t_good set stock=stock-#{stock} where id=#{id}") public void updateGoodStock(@Param("stock") Integer stock, @Param("id") Integer id); }
main/resource目录下mapper文件夹,GoodMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.example.mapper.GoodMapper"> </mapper>
-
在seata-at-springcloud-good模块中,创建service包,新建GoodService接口,定义新增的方法
import com.baomidou.mybatisplus.extension.service.IService; import org.example.entity.Good; import org.springframework.stereotype.Service; @Service public interface GoodService extends IService<Good> { public void reduceGoodStock(int num, int goodId); }
-
在service包内创建impl包,新建GoodServiceImpl类,实现接口GoodService,并实现其接口方法
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.example.entity.Good; import org.example.mapper.GoodMapper; import org.example.service.GoodService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class GoodServiceImpl extends ServiceImpl<GoodMapper, Good> implements GoodService { @Autowired private GoodMapper goodMapper; @Override public void reduceGoodStock(int num, int goodId) { Good good = goodMapper.selectById(goodId); if (good.getStock() < num) { throw new RuntimeException("Good库存不足"); } goodMapper.updateGoodStock(num, goodId); } }
-
在seata-at-springcloud-good模块中,创建controller包,新建GoodController类,定义web请求接口的方法
import org.example.entity.Good; import org.example.service.GoodService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("good") public class GoodController { @Autowired private GoodService goodService; @GetMapping("{id}") public Good findGoodById(@PathVariable("id") Integer id) { Good good = goodService.getById(id); return good; } @PutMapping public String reduceGoodStock(Integer num, Integer goodId) { goodService.reduceGoodStock(num, goodId); return "success"; } }
-
在根包(org.example)下,新建一个TCCGoodApplication启动类,编写Springboot启动代码
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan(basePackages = {"org.example.mapper"}) public class TCCGoodApplication { public static void main(String[] args) { SpringApplication.run(TCCGoodApplication.class, args); } }
-
启动Springboot工程
-
在HTTP请求工具中访问good的Get请求,参数是:1(该访问数据前后不会发生变化)
HTTP请求工具
程序控制台
-
在HTTP请求工具中访问good的Put请求,参数是:num=2&goodId=1(该访问数据前后会发生变化)
HTTP请求工具
程序控制台
-
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB4容器的MySQL数据库 docker exec -it MySQLDB4 mysql -u root -p -- 还原t_good表数据 update db4.t_good set stock=200 where id=1;
执行结果截图:
搭建seata-tcc-springcloud-account模块,作为用户模块
-
在seata-tcc-springcloud-account模块中,找到pom.xml文件,引入SpringBoot的Web依赖、MySQL连接依赖、seata-tcc-springcloud-common公共模块依赖、Nacos注册发现依赖
<parent> <groupId>org.example</groupId> <artifactId>seata-tcc-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-tcc-springcloud-account</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--nacos 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>seata-tcc-springcloud-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-tcc-springcloud-account模块中,找到application.yml文件,配置端口、数据源信息、MyBatis-plus基础配置、Nacos注册发现地址
server: port: 8603 spring: application: name: seata-tcc-springcloud-account datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3309/db3?serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ZYMzym111 cloud: nacos: server-addr: localhost:8848 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true
-
在seata-tcc-springcloud-account模块中,找到main/java目录创建mapper包,新建AccountMapper类,继承BaseMapper;找到mian/resource目录创建mapper文件夹,新建AccountMapper.xml文件。
main/java目录下mapper包,AccountMapper类
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; import org.example.entity.Account; import org.springframework.stereotype.Repository; @Repository public interface AccountMapper extends BaseMapper<Account> { @Update("update t_account set money=money-#{money} where id=#{id}") public void reduceAccountMoney(@Param("money") double money, @Param("id") Integer id); }
main/resource目录下mapper文件夹,AccountMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.example.mapper.AccountMapper"> </mapper>
-
在seata-tcc-springcloud-account模块下,创建service包,新建AccountService接口,定义新增的方法
import com.baomidou.mybatisplus.extension.service.IService; import org.example.entity.Account; import org.springframework.stereotype.Service; @Service public interface AccountService extends IService<Account> { public void reduceAccountMoney(double money, Integer id); }
-
在service包内创建impl包,新建AccountServiceImpl类,实现接口AccountService,并实现其接口方法
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.example.entity.Account; import org.example.mapper.AccountMapper; import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService { @Autowired private AccountMapper accountMapper; @Override public void reduceAccountMoney(double money, Integer id) { Account account = accountMapper.selectById(id); if (account.getMoney() < money) { throw new RuntimeException("account 账户余额不足"); } accountMapper.reduceAccountMoney(money, id); } }
-
在seata-at-springcloud-account模块中,创建controller包,新建AccountController类,定义web请求接口的方法
import org.example.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("account") public class AccountController { @Autowired private AccountService accountService; @PutMapping public String reduceAccountMoney(double money, Integer id) { accountService.reduceAccountMoney(money, id); return "success"; } }
-
在根包(org.example)下,新建一个TCCAccountApplication启动类,编写Springboot启动代码
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan(basePackages = {"org.example.mapper"}) public class TCCAccountApplication { public static void main(String[] args) { SpringApplication.run(TCCAccountApplication.class, args); } }
-
启动Springboot工程
在HTTP请求工具中访问account的Put请求,参数是:money=100&id=1
HTTP请求工具
程序控制台
-
查看数据库数据变化
执行之前
执行之后
-
恢复数据库原始数据
# 进入MySQLDB3容器的MySQL数据库 docker exec -it MySQLDB3 mysql -u root -p -- 还原t_account表数据 update db3.t_account set money=200 where id=1;
执行结果截图:
搭建seata-tcc-springcloud-business模块,作为业务模块
-
在seata-tcc-springcloud-business模块中,找到pom.xml文件,引入SpringBoot的Web依赖、seata-tcc-springcloud-common公共模块依赖、Nacos注册发现依赖
<parent> <groupId>org.example</groupId> <artifactId>seata-tcc-springcloud-demo</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>seata-tcc-springcloud-business</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--nacos 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.example</groupId> <artifactId>seata-tcc-springcloud-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
-
在seata-at-springcloud-business模块中,找到application.yml文件,配置端口、Nacos注册发现地址
server: port: 8604 spring: application: name: seata-tcc-springcloud-business cloud: nacos: server-addr: localhost:8848
-
在seata-at-springcloud-bussiness模块中,创建utils包,新建OrderStatus类和URL类,用于标识订单状态和URL请求路径。
OrderStatus类
public enum OrderStatus { CREATE,UPDATING,FINISH }
URL类
public class URL { public static final String CREATE_ORDER = "http://seata-tcc-springcloud-order/order"; public static final String GOOD_INFO = "http://seata-tcc-springcloud-good/good/%d"; public static final String GOOD_REDUCE = "http://seata-tcc-springcloud-good/good?num=%d&goodId=%d"; public static final String ACCOUNT_REDUCE = "http://seata-tcc-springcloud-account/account?money=%f&id=%d"; }
-
在seata-tcc-springcloud-business模块中,创建service包,新建BusinessService接口,定义新增的方法
import org.springframework.stereotype.Service; @Service public interface BusinessService { public void placeOrder(Integer accountId,Integer goodId,Integer num); }
-
在service包内创建impl包,新建BusinessServiceImpl类,实现接口BusinessService,并实现其接口方法
import org.example.entity.Good; import org.example.entity.Order; import org.example.service.BusinessService; import org.example.utils.OrderStatus; import org.example.utils.URL; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class BusinessServiceImpl implements BusinessService { @Autowired private RestTemplate restTemplate; @Override public void placeOrder(Integer accountId, Integer goodId, Integer num) { // 查询商品信息 Good good = restTemplate.getForObject(String.format(URL.GOOD_INFO, goodId), Good.class); double allPay = good.getPrice() * num; Order order = new Order().setStatus(OrderStatus.CREATE.name()).setAccountId(accountId).setGoodId(goodId).setGoodCount(num).setAllPay(allPay); // 下订单 HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); HttpEntity<Order> httpEntity = new HttpEntity<>(order, httpHeaders); restTemplate.postForObject(URL.CREATE_ORDER, httpEntity, String.class); // 减库存 restTemplate.put(String.format(URL.GOOD_REDUCE, num, goodId), null); // 扣钱 restTemplate.put(String.format(URL.ACCOUNT_REDUCE, allPay, accountId), null); } }
-
在seata-tcc-springcloud-business模块中,创建controller包,新建BusinessController类,定义web请求接口的方法
import org.example.service.BusinessService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class BusinessController { @Autowired private BusinessService businessService; @GetMapping("placeOrder") public String placeOrder(Integer accountId, Integer goodId, Integer num) { businessService.placeOrder(accountId, goodId, num); return "success"; } }
-
在根包(org.example)下,新建一个TCCBusinessApplication启动类,编写Springboot启动代码
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication(exclude= {DataSourceAutoConfiguration.class}) public class TCCBusinessApplication { public static void main(String[] args) { SpringApplication.run(TCCBusinessApplication.class, args); } @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }
-
启动Springboot工程
在HTTP请求工具中访问placeOrder的Get请求,参数是:accountId=1&goodId=1&num=1
HTTP请求工具
Business程序控制台
Order程序控制台
Good程序控制台
Account程序控制台
-
查看数据库数据变化
执行之前
order数据库
good数据库
account数据库
执行之后
order数据库
good数据库
account数据库
-
恢复数据库原始数据
# 进入MySQLDB3容器的MySQL数据库 docker exec -it MySQLDB3 mysql -u root -p -- 还原t_account表数据 update db3.t_account set money=200 where id=1; # 进入MySQLDB4容器的MySQL数据库 docker exec -it MySQLDB4 mysql -u root -p -- 还原t_good表数据 update db4.t_good set stock=200 where id=1; # 进入MySQLDB5容器的MySQL数据库 docker exec -it MySQLDB5 mysql -u root -p -- 清空t_order表数据 truncate db5.t_order;
执行结果截图:
10.2.3 微服务 跨数据库跨服务 改造一(基础改造,未解决幂等性、空回滚、悬挂问题)
-
在seata-tcc-springcloud-good模块和seata-tcc-springcloud-account模块中,加入标识字段;在seata-tcc-springcloud-order模块,分布式事务失败后可以直接删除事务记录,所以不需要加入标识字段
# 进入MySQLDB3容器的MySQL数据库 docker exec -it MySQLDB3 mysql -u root -p -- db3数据库中加入money_change标识字段 ALTER TABLE db3.t_account ADD money_change int NULL DEFAULT 0 COMMENT '金额变化'; # 进入MySQLDB4容器的MySQL数据库 docker exec -it MySQLDB4 mysql -u root -p -- db4数据库中加入stock_change标识字段 ALTER TABLE db4.t_good ADD stock_change bigint NULL DEFAULT 0 COMMENT '库存变化';
执行效果截图
-
在seata-tcc-springcloud-common模块中,找到pom.xml文件,加入spring-cloud-starter-alibaba-seata包依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
-
在seata-tcc-springcloud-order、seata-at-springcloud-good、seata-at-springcloud-account、seata-at-springcloud-business模块中,找到application.yml文件,文件中加入Seata配置信息
seata: service: vgroup-mapping: default_tx_group: default grouplist: default: localhost:8091 tx-service-group: default_tx_group enable-auto-data-source-proxy: false # 开启自动代理 默认值是true
-
在seata-tcc-springcloud-common模块中,找到Account实体类和Good实体类,加入对应的新增的标识字段
import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) @TableName("t_account") public class Account { private Integer id; private String name; private Integer money; private Integer moneyChange; }
import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.experimental.Accessors; @Data @Accessors(chain = true) @TableName("t_good") public class Good { private Integer id; private String name; private double price; private int stock; private int stockChange; }
-
在seata-tcc-springcloud-order模块中,创建utils包,新建OrderStatus枚举类
public enum OrderStatus { CREATE,UPDATING,FINISH }
-
在seata-tcc-springcloud-order模块中,创建tccaction包,新建OrderAction类,编写TCC事务声明接口
import io.seata.rm.tcc.api.BusinessActionContext; import io.seata.rm.tcc.api.BusinessActionContextParameter; import io.seata.rm.tcc.api.LocalTCC; import io.seata.rm.tcc.api.TwoPhaseBusinessAction; import org.example.entity.Order; @LocalTCC public interface OrderAction { @TwoPhaseBusinessAction(name = "addOrder") void addOrder(BusinessActionContext context,@BusinessActionContextParameter("order") Order order); void commit(BusinessActionContext context); void rollback(BusinessActionContext context); }
-
在tccaction包下,创建impl包,新建OrderActionimpl类,继承OrderAction接口类,实现TCC事务声明接口方法
import io.seata.rm.tcc.api.BusinessActionContext; import org.example.entity.Order; import org.example.mapper.OrderMapper; import org.example.tccaction.OrderAction; import org.example.utils.OrderStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Component public class OrderActionImpl implements OrderAction { @Autowired private OrderMapper orderMapper; @Override @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void addOrder(BusinessActionContext context, Order order) { orderMapper.insert(order); } @Override public void commit(BusinessActionContext context) { Order order = context.getActionContext("order", Order.class); order.setStatus(OrderStatus.FINISH.name()); orderMapper.updateById(order); } @Override public void rollback(BusinessActionContext context) { Order order = context.getActionContext("order", Order.class); orderMapper.deleteById(order); } }
-
在seata-tcc-springcloud-order模块中,找到OrderServiceImpl类,将原本调用OrderMapper接口改为调用OrderAction接口类
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import io.seata.rm.tcc.api.BusinessActionContextUtil; import org.example.entity.Order; import org.example.mapper.OrderMapper; import org.example.service.OrderService; import org.example.tccaction.OrderAction; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Random; @Service public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService { // @Autowired // private OrderMapper orderMapper; @Autowired private OrderAction orderAction; @Override public void addOrder(Order order) { if (order.getId() == null) { Random rand = new Random(); int temp = rand.nextInt(100000); order.setId(temp); } orderAction.addOrder(BusinessActionContextUtil.getContext(), order); // orderMapper.insert(order); } }
-
在seata-tcc-springcloud-good模块中,找到GoodMapper接口类,实现对库存的提交和回滚操作,并完善减库存操作
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; import org.example.entity.Good; import org.springframework.stereotype.Repository; @Repository public interface GoodMapper extends BaseMapper<Good> { @Update("update t_good set stock=stock-#{stock},stock_change=stock_change+#{stock} where id=#{id}") public void updateGoodStock(@Param("stock") Integer stock, @Param("id") Integer id); @Update("update t_good set stock_change=stock_change-#{stock} where id=#{id}") public void commitGoodStock(@Param("stock") Integer stock, @Param("id") Integer id); @Update("update t_good set stock=stock+#{stock},stock_change=stock_change-#{stock} where id=#{id}") public void rollbackGoodStock(@Param("stock") Integer stock, @Param("id") Integer id); }
-
在seata-tcc-springcloud-good模块中,创建tccaction包,新建GoodAction类,编写TCC事务声明接口
import io.seata.rm.tcc.api.BusinessActionContext; import io.seata.rm.tcc.api.BusinessActionContextParameter; import io.seata.rm.tcc.api.LocalTCC; import io.seata.rm.tcc.api.TwoPhaseBusinessAction; @LocalTCC public interface GoodAction { @TwoPhaseBusinessAction(name = "reduceGoodStock") public void reduceGoodStock(BusinessActionContext context, @BusinessActionContextParameter("num") int num, @BusinessActionContextParameter("goodId") int goodId); public void commit(BusinessActionContext context); public void rollback(BusinessActionContext context); }
-
在tccaction包下,创建impl包,新建GoodActionImpl类,继承GoodAction接口类,实现TCC事务声明接口方法
import io.seata.rm.tcc.api.BusinessActionContext; import org.example.entity.Good; import org.example.mapper.GoodMapper; import org.example.tccaction.GoodAction; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Component public class GoodActionImpl implements GoodAction { @Autowired private GoodMapper goodMapper; @Override @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void reduceGoodStock(BusinessActionContext context, int num, int goodId) { Good good = goodMapper.selectById(goodId); if (good.getStock() < num) { throw new RuntimeException("Good库存不足"); } goodMapper.updateGoodStock(num, goodId); } @Override public void commit(BusinessActionContext context) { int num = Integer.parseInt(context.getActionContext("num").toString()); int goodId = Integer.parseInt(context.getActionContext("goodId").toString()); goodMapper.commitGoodStock(num, goodId); } @Override public void rollback(BusinessActionContext context) { int num = Integer.parseInt(context.getActionContext("num").toString()); int goodId = Integer.parseInt(context.getActionContext("goodId").toString()); goodMapper.rollbackGoodStock(num, goodId); } }
-
在seata-tcc-springcloud-good模块中,找到GoodServiceImpl类,将原本调用GoodMapper接口改为调用GoodAction接口类
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import io.seata.rm.tcc.api.BusinessActionContextUtil; import org.example.entity.Good; import org.example.mapper.GoodMapper; import org.example.service.GoodService; import org.example.tccaction.GoodAction; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class GoodServiceImpl extends ServiceImpl<GoodMapper, Good> implements GoodService { // @Autowired // private GoodMapper goodMapper; @Autowired private GoodAction goodAction; @Override public void reduceGoodStock(int num, int goodId) { goodAction.reduceGoodStock(BusinessActionContextUtil.getContext(), num, goodId); // Good good = goodMapper.selectById(goodId); // if (good.getStock() < num) { // throw new RuntimeException("Good库存不足"); // } // goodMapper.updateGoodStock(num, goodId); } }
-
在seata-at-springcloud-account模块中,找到AccountMapper接口类,实现对库存的提交和回滚操作,并完善减库存操作
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; import org.example.entity.Account; import org.springframework.stereotype.Repository; @Repository public interface AccountMapper extends BaseMapper<Account> { @Update("update t_account set money=money-#{money},money_change=money_change+#{money} where id=#{id}") public void reduceAccountMoney(@Param("money") double money, @Param("id") Integer id); @Update("update t_account set money_change=money_change-#{money} where id=#{id}") public void commitAccountMoney(@Param("money") double money, @Param("id") Integer id); @Update("update t_account set money=money+#{money},money_change=money_change-#{money} where id=#{id}") public void rollbackAccountMoney(@Param("money") double money, @Param("id") Integer id); }
-
在seata-tcc-springcloud-account模块中,创建tccaction包,新建AccountAction类,编写TCC事务声明接口
import io.seata.rm.tcc.api.BusinessActionContext; import io.seata.rm.tcc.api.BusinessActionContextParameter; import io.seata.rm.tcc.api.LocalTCC; import io.seata.rm.tcc.api.TwoPhaseBusinessAction; @LocalTCC public interface AccountAction { @TwoPhaseBusinessAction(name = "reduceAccountMoney") public void reduceAccountMoney(BusinessActionContext context, @BusinessActionContextParameter("money") double money, @BusinessActionContextParameter("id") Integer id); public void commit(BusinessActionContext context); public void rollback(BusinessActionContext context); }
-
在tccaction包下,创建impl包,新建AccountActionImpl类,继承AccountAction接口类,实现TCC事务声明接口方法
import io.seata.rm.tcc.api.BusinessActionContext; import org.example.entity.Account; import org.example.mapper.AccountMapper; import org.example.tccaction.AccountAction; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Component public class AccountActionImpl implements AccountAction { @Autowired private AccountMapper accountMapper; @Override @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void reduceAccountMoney(BusinessActionContext context, double money, Integer id) { Account account = accountMapper.selectById(id); if (account.getMoney() < money) { throw new RuntimeException("account 账户余额不足"); } accountMapper.reduceAccountMoney(money, id); } @Override public void commit(BusinessActionContext context) { double money = Double.parseDouble(context.getActionContext("money").toString()); int id = Integer.parseInt(context.getActionContext("id").toString()); accountMapper.commitAccountMoney(money, id); } @Override public void rollback(BusinessActionContext context) { double money = Double.parseDouble(context.getActionContext("money").toString()); int id = Integer.parseInt(context.getActionContext("id").toString()); accountMapper.rollbackAccountMoney(money, id); } }
-
在seata-tcc-springcloud-account模块中,找到AccountServiceImpl类,将原本调用AccountMapper接口改为调用AccountAction接口类
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import io.seata.rm.tcc.api.BusinessActionContextUtil; import org.example.entity.Account; import org.example.mapper.AccountMapper; import org.example.service.AccountService; import org.example.tccaction.AccountAction; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService { // @Autowired // private AccountMapper accountMapper; @Autowired private AccountAction accountAction; @Override public void reduceAccountMoney(double money, Integer id) { accountAction.reduceAccountMoney(BusinessActionContextUtil.getContext(), money, id); // Account account = accountMapper.selectById(id); // if (account.getMoney() < money) { // throw new RuntimeException("account 账户余额不足"); // } // accountMapper.reduceAccountMoney(money, id); } }
-
在seata-tcc-springcloud-business模块中,找到BusinessServiceImpl实现类,加入全局事务注解
import io.seata.spring.annotation.GlobalTransactional; import org.example.entity.Good; import org.example.entity.Order; import org.example.service.BusinessService; import org.example.utils.OrderStatus; import org.example.utils.URL; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class BusinessServiceImpl implements BusinessService { @Autowired private RestTemplate restTemplate; @Override @GlobalTransactional public void placeOrder(Integer accountId, Integer goodId, Integer num) { // 查询商品信息 Good good = restTemplate.getForObject(String.format(URL.GOOD_INFO, goodId), Good.class); double allPay = good.getPrice() * num; Order order = new Order().setStatus(OrderStatus.CREATE.name()).setAccountId(accountId).setGoodId(goodId).setGoodCount(num).setAllPay(allPay); // 下订单 HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); HttpEntity<Order> httpEntity = new HttpEntity<>(order, httpHeaders); restTemplate.postForObject(URL.CREATE_ORDER, httpEntity, String.class); // 减库存 restTemplate.put(String.format(URL.GOOD_REDUCE, num, goodId), null); // 扣钱 restTemplate.put(String.format(URL.ACCOUNT_REDUCE, allPay, accountId), null); } }
10.2.3 微服务 跨数据库跨服务 改造二(基于基础改造,解决幂等性、空回滚、悬挂问题)
-
在涉及到分布式事务的数据库中,添加try_log数据库表、commit_log数据库表、rollback_log数据库表
# 进入MySQLDB3容器的MySQL数据库 docker exec -it MySQLDB3 mysql -u root -p -- db3数据库中加入三张数据库表标识字段 CREATE TABLE db3.try_log ( xid varchar(100) NULL COMMENT '事务ID', create_time DATETIME NULL COMMENT '创建数据' )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; CREATE TABLE db3.commit_log ( xid varchar(100) NULL COMMENT '事务ID', create_time DATETIME NULL COMMENT '创建数据' )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; CREATE TABLE db3.rollback_log ( xid varchar(100) NULL COMMENT '事务ID', create_time DATETIME NULL COMMENT '创建数据' )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; # 进入MySQLDB4容器的MySQL数据库 docker exec -it MySQLDB4 mysql -u root -p -- db4数据库中加入三张数据库表标识字段 CREATE TABLE db4.try_log ( xid varchar(100) NULL COMMENT '事务ID', create_time DATETIME NULL COMMENT '创建数据' )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; CREATE TABLE db4.commit_log ( xid varchar(100) NULL COMMENT '事务ID', create_time DATETIME NULL COMMENT '创建数据' )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; CREATE TABLE db4.rollback_log ( xid varchar(100) NULL COMMENT '事务ID', create_time DATETIME NULL COMMENT '创建数据' )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; # 进入MySQLDB5容器的MySQL数据库 docker exec -it MySQLDB5 mysql -u root -p -- db5数据库中加入三张数据库表标识字段 CREATE TABLE db5.try_log ( xid varchar(100) NULL COMMENT '事务ID', create_time DATETIME NULL COMMENT '创建数据' )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; CREATE TABLE db5.commit_log ( xid varchar(100) NULL COMMENT '事务ID', create_time DATETIME NULL COMMENT '创建数据' )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; CREATE TABLE db5.rollback_log ( xid varchar(100) NULL COMMENT '事务ID', create_time DATETIME NULL COMMENT '创建数据' )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-
在seata-tcc-springcloud-common模块中,创建mapper包,新建TccMapper接口类
import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Select; public interface TccMapper { @Insert("insert into try_log values(#{xid},now())") public void insertTryLog(String xid); @Select("select count(1) from try_log where xid=#{xid}") public int existsTryLog(String xid); @Delete("delete from try_log where xid=#{xid}") public int deleteTryLog(String xid); @Insert("insert into commit_log values(#{xid},now())") public void insertCommitLog(String xid); @Select("select count(1) from commit_log where xid=#{xid}") public int existsCommitLog(String xid); @Delete("delete from commit_log where xid=#{xid}") public int deleteCommitLog(String xid); @Insert("insert into rollback_log values(#{xid},now())") public void insertRollbackLog(String xid); @Select("select count(1) from rollback_log where xid=#{xid}") public int existsRollbackLog(String xid); @Delete("delete from rollback_log where xid=#{xid}") public int deleteRollbackLog(String xid); }
-
在seata-tcc-springcloud-order模块中,找到mapper目录下,创建OrderTccMapper接口类,并继承TccMapper接口类
import org.springframework.stereotype.Repository; @Repository public interface OrderTccMapper extends TccMapper { }
-
在seata-tcc-springcloud-order模块中,找到OrderActionImpl实现类,注入OrderTccMapper接口类,并解决幂等、空回滚和悬挂问题
import io.seata.rm.tcc.api.BusinessActionContext; import org.example.entity.Order; import org.example.mapper.OrderMapper; import org.example.mapper.OrderTccMapper; import org.example.tccaction.OrderAction; import org.example.utils.OrderStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Component public class OrderActionImpl implements OrderAction { @Autowired private OrderMapper orderMapper; @Autowired private OrderTccMapper orderTccMapper; @Override @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void addOrder(BusinessActionContext context, Order order) { String xid = context.getXid(); // 幂等 if (orderTccMapper.existsTryLog(xid) > 0) { return; } // 悬挂 if (orderTccMapper.existsRollbackLog(xid) > 0 || orderTccMapper.existsCommitLog(xid) > 0) { return; } orderMapper.insert(order); orderTccMapper.insertTryLog(xid); } @Override public void commit(BusinessActionContext context) { String xid = context.getXid(); // 幂等 if (orderTccMapper.existsCommitLog(xid) > 0) { return; } // 空回滚 if (orderTccMapper.existsTryLog(xid) == 0) { return; } Order order = context.getActionContext("order", Order.class); order.setStatus(OrderStatus.FINISH.name()); orderMapper.updateById(order); orderTccMapper.insertCommitLog(xid); } @Override public void rollback(BusinessActionContext context) { String xid = context.getXid(); // 幂等 if (orderTccMapper.existsRollbackLog(xid) > 0) { return; } // 空回滚 if (orderTccMapper.existsTryLog(xid) == 0) { return; } Order order = context.getActionContext("order", Order.class); orderMapper.deleteById(order); orderTccMapper.insertRollbackLog(xid); } }
-
在seata-tcc-springcloud-good模块中,找到mapper目录下,创建GoodTccMapper接口类,并继承TccMapper接口类
import org.springframework.stereotype.Repository; @Repository public interface GoodTccMapper extends TccMapper { }
-
在seata-tcc-springcloud-good模块中,找到GoodActionImpl实现类,注入GoodTccMapper接口类,并解决幂等、空回滚和悬挂问题
import io.seata.rm.tcc.api.BusinessActionContext; import org.example.entity.Good; import org.example.mapper.GoodMapper; import org.example.mapper.GoodTccMapper; import org.example.tccaction.GoodAction; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Component public class GoodActionImpl implements GoodAction { @Autowired private GoodMapper goodMapper; @Autowired private GoodTccMapper goodTccMapper; @Override @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void reduceGoodStock(BusinessActionContext context, int num, int goodId) { String xid = context.getXid(); // 幂等 if (goodTccMapper.existsTryLog(xid) > 0) { return; } // 悬挂 if (goodTccMapper.existsRollbackLog(xid) > 0 || goodTccMapper.existsCommitLog(xid) > 0) { return; } Good good = goodMapper.selectById(goodId); if (good.getStock() < num) { throw new RuntimeException("Good库存不足"); } goodMapper.updateGoodStock(num, goodId); goodTccMapper.insertTryLog(xid); } @Override public void commit(BusinessActionContext context) { String xid = context.getXid(); // 幂等 if (goodTccMapper.existsCommitLog(xid) > 0) { return; } // 空回滚 if (goodTccMapper.existsTryLog(xid) == 0) { return; } int num = Integer.parseInt(context.getActionContext("num").toString()); int goodId = Integer.parseInt(context.getActionContext("goodId").toString()); goodMapper.commitGoodStock(num, goodId); goodTccMapper.insertCommitLog(xid); } @Override public void rollback(BusinessActionContext context) { String xid = context.getXid(); // 幂等 if (goodTccMapper.existsRollbackLog(xid) > 0) { return; } // 空回滚 if (goodTccMapper.existsTryLog(xid) == 0) { return; } int num = Integer.parseInt(context.getActionContext("num").toString()); int goodId = Integer.parseInt(context.getActionContext("goodId").toString()); goodMapper.rollbackGoodStock(num, goodId); goodTccMapper.insertRollbackLog(xid); } }
-
在seata-tcc-springcloud-account模块中,找到mapper目录下,创建AccountTccMapper接口类,并继承TccMapper接口类
import org.springframework.stereotype.Repository; @Repository public interface AccountTccMapper extends TccMapper { }
-
在seata-tcc-springcloud-account模块中,找到AccountActionImpl实现类,注入AccountTccMapper接口类,并解决幂等、空回滚和悬挂问题
import io.seata.rm.tcc.api.BusinessActionContext; import org.example.entity.Account; import org.example.mapper.AccountMapper; import org.example.mapper.AccountTccMapper; import org.example.tccaction.AccountAction; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @Component public class AccountActionImpl implements AccountAction { @Autowired private AccountMapper accountMapper; @Autowired private AccountTccMapper accountTccMapper; @Override @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void reduceAccountMoney(BusinessActionContext context, double money, Integer id) { String xid = context.getXid(); //幂等性 if (accountTccMapper.existsTryLog(xid) > 0) { return; } // 悬挂 if (accountTccMapper.existsRollbackLog(xid) > 0 || accountTccMapper.existsCommitLog(xid) > 0) { return; } Account account = accountMapper.selectById(id); if (account.getMoney() < money) { throw new RuntimeException("account 账户余额不足"); } accountMapper.reduceAccountMoney(money, id); accountTccMapper.insertTryLog(xid); } @Override public void commit(BusinessActionContext context) { String xid = context.getXid(); // 幂等 if (accountTccMapper.existsCommitLog(xid) > 0) { return; } // 空回滚 if (accountTccMapper.existsTryLog(xid) == 0) { return; } double money = Double.parseDouble(context.getActionContext("money").toString()); int id = Integer.parseInt(context.getActionContext("id").toString()); accountMapper.commitAccountMoney(money, id); accountTccMapper.insertCommitLog(xid); } @Override public void rollback(BusinessActionContext context) { String xid = context.getXid(); // 幂等 if (accountTccMapper.existsRollbackLog(xid) > 0) { return; } // 空回滚 if (accountTccMapper.existsTryLog(xid) == 0) { return; } double money = Double.parseDouble(context.getActionContext("money").toString()); int id = Integer.parseInt(context.getActionContext("id").toString()); accountMapper.rollbackAccountMoney(money, id); accountTccMapper.insertRollbackLog(xid); } }
10.2.3 微服务 跨数据库跨服务 测试
-
正确请求分布式事务,正确处理
在HTTP请求工具中发送两次placeOrder的Get成功请求,参数为:accountId=1&goodId=1&num=1
HTTP请求工具
business程序控制台
order程序控制台
good程序控制台
account程序控制台
-
查看数据库数据变化
执行之前
order数据库
good数据库
account数据库
执行之后
order数据库
good数据库
account数据库
-
正确请求分布式事务,异常处理
在HTTP请求工具中发送两次placeOrder的Get成功请求,参数为:accountId=1&goodId=1&num=1
HTTP请求工具
business程序控制台
order程序控制台
good程序控制台
account程序控制台
-
查看数据库数据变化
执行之前
order数据库
good数据库
account数据库
执行之后
order数据库
good数据库
account数据库
11. 弱一致性:Saga
11.1 什么是Saga模式
Saga模式是一种长事务解决方案,适用于微服务架构下的分布式事务场景。也是基于两阶段提交
Saga模式的核心思想是将一个复杂的长事务拆分成多个本地短事务,每个短事务都应该是独立的、无状态的、可重试的。如果某个短事务执行失败,可以通过执行补偿操作(Compensating Transaction)来撤销之前已经完成的短事务,从而保持事务的一致性。
11.2 Seata Saga模式的主要组件
Saga模式的主要组件包括:
- 事务协调器(Transaction Coordinator,TC):负责维护全局事务的运行状态,触发并协调各个微服务的短事务和补偿操作。
- 事务管理器(Transaction Manager,TM):负责触发并管理微服务中的短事务,向TC报告事务状态。
- 微服务(Microservices):实现业务逻辑,执行本地短事务和补偿操作。
11.3 Seata Saga模式的执行流程
Saga模式的执行流程如下:
- TM向TC发起全局事务请求,TC为全局事务生成一个全局事务ID。
- TM根据全局事务ID触发并管理微服务的短事务。
- 如果某个短事务执行失败,TM通知TC并暂停全局事务。
- TC根据全局事务ID找到对应的补偿操作,并向相关微服务发送执行补偿操作的请求。
- 补偿操作执行成功后,TC通知TM全局事务已完成。
11.3 Seata Saga模式的优缺点
Saga模式的优点:
- 适用性强:Saga模式适用于微服务架构下的分布式事务场景,可以处理各种类型的分布式事务问题。
- 易于实现:Saga模式将复杂的长事务拆分成多个简单的短事务,降低了实现难度。
- 高性能:短事务的执行速度通常较快,因此Saga模式具有较高的性能。
- 可伸缩性:Saga模式支持横向扩展,可以轻松应对高并发场景。
Saga模式的缺点:
- 事务隔离性较低:由于Saga模式中的短事务是顺序执行的,可能存在数据脏读、不可重复读和幻读等问题。
- 补偿操作可能失败:补偿操作也可能面临执行失败的风险,需要设计相应的重试和回滚机制。
11.4 Seata Saga模式的使用流程
- 引入依赖
在项目中引入 Seata Saga 的相关依赖,例如客户端库和注册中心。对于 Spring Boot 项目,可以使用 Spring Boot 集成的 Seata Saga 依赖。 - 配置 Seata Saga
根据项目需求,配置 Seata Saga 的相关参数,如事务分组、事务超时时间等。同时,确保注册中心(如 Nacos、Zookeeper 或 Consul)已正确配置并运行。 - 定义事务接口
为每个需要分布式事务的业务场景定义一个事务接口。这个接口包括所有需要执行的子事务,以及子事务的执行顺序。 - 实现事务接口
为每个事务接口实现一个具体的事务处理类。在这个类中,调用子事务的方法,并确保在调用子事务方法时,使用 Seata Saga 提供的事务注解(如@SagaStart
、@SagaRecord
和@SagaEnd
)。 - 注册事务处理类
将实现的事务处理类注册到 Seata Saga 的事务处理器中,这样 Seata Saga 就可以识别并执行相应的事务处理逻辑。 - 调用事务接口
在需要执行分布式事务的业务场景中,调用定义的事务接口。Seata Saga 会自动执行事务处理类的逻辑,并确保分布式事务的一致性。 - 异常处理
为事务处理类实现异常处理逻辑,以便在子事务执行失败时,可以回滚整个分布式事务。同时,需要确保在异常处理逻辑中使用 Seata Saga 提供的异常处理注解(如@SagaFailure
)。 - 监控与调试
使用 Seata Saga 提供的监控和调试工具,如日志、指标和追踪,来监控分布式事务的执行情况,以便在出现问题时能够迅速定位和解决。
11.5 Seata Saga模式与Seata TCC模式、Seata AT模式的相同点和不同点
相同点:
- 都属于分布式事务解决方案:这三种模式都是为了解决分布式系统中的分布式事务问题而设计的。它们都通过提供一种机制来确保在分布式环境下,分布式事务能够满足原子性、一致性、隔离性和持久性(ACID)的特性。
- 都基于Seata框架:这三种模式都基于Seata框架实现,共享Seata的核心功能,如全局事务协调、事务补偿、超时控制等。
不同点:
- 原理和实现方式:
- Seata Saga 模式:基于事件驱动的长事务解决方案,将一个长事务拆分成多个本地短事务,通过事件驱动各个短事务的调用顺序,确保整个分布式事务的一致性。适用于业务流程长、业务执行顺序明确的场景。
- Seata TCC 模式:采用Try-Confirm-Cancel(尝试-确认-取消)机制,通过补偿操作来实现分布式事务的一致性。适用于业务逻辑相对简单,对数据一致性要求较高的场景。
- Seata AT 模式:基于两阶段提交(2PC)协议实现,通过事务管理器和资源管理器的协作,确保分布式事务的一致性。适用于对性能要求较高,对数据一致性要求较低的场景。
- 适用场景:
- Seata Saga 模式:适用于业务流程长、业务执行顺序明确的场景,如金融行业的资金转账、电商领域的订单创建等。
- Seata TCC 模式:适用于业务逻辑相对简单,对数据一致性要求较高的场景,如电商领域的库存扣减、订单取消等。
- Seata AT 模式:适用于对性能要求较高,对数据一致性要求较低的场景,如高并发的秒杀系统、实时竞价系统等。
12. Seata 高可用
12.1 Seata 数据单独存储
将Seata数据存储模式由原本的file改为db(seata.store.mode)
12.1.1 Windows环境下部署Seata,并调整存储模式
规划:数据存储模式由原本的file改为db,db数据库选择MySQL
-
点击进入seata下载页面,点击soure和binary,下载源码包和编译之后的包
-
解压seata-server-1.6.1.zip,进入seata的script/server/db目录下
-
将mysql.sql导入到MySQL数据库的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_status_gmt_modified` (`status` , `gmt_modified`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -- 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 = utf8mb4; -- the table to store lock data CREATE TABLE IF NOT EXISTS `lock_table` ( `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(128), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking', `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_status` (`status`), KEY `idx_branch_id` (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; CREATE TABLE IF NOT EXISTS `distributed_lock` ( `lock_key` CHAR(20) NOT NULL, `lock_value` VARCHAR(20) NOT NULL, `expire` BIGINT, primary key (`lock_key`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
执行结果截图
-
在解压seata-server-1.6.1.zip包中,进入seata/conf目录下,找到application.yml文件,配置store存储模式
seata: config: # support: nacos, consul, apollo, zk, etcd3 type: file registry: # support: nacos, eureka, redis, zk, consul, etcd3, sofa type: file store: # support: file 、 db 、 redis mode: db db: datasource: druid db-type: mysql driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata?serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true user: root password: ZYMzym111 min-conn: 10 max-conn: 100 global-table: global_table branch-table: branch_table lock-table: lock_table distributed-lock-table: distributed_lock query-limit: 1000 max-wait: 5000
-
在解压seata-server-1.6.1.zip包中,进入seata的bin目录下
-
通过cmd进入当前目录,并执行seata-server.bat文件
12.1.2 Docker环境下部署Seata,并调整存储模式
规划:数据存储模式由原本的file改为db,db数据库选择MySQL
-
MySQL规划
- 创建一个MySQL数据库容器,名称是MySQLDB
- 容器名为MySQLDB端口对外映射为3306
# 查看MySQL镜像 docker search mysql # 拉取MySQL镜像 docker pull mysql # 查看拉取的镜像 docker images # 运行名为MySQLDB容器名的MySQL镜像 docker run -d --name MySQLDB -p3306:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest # 查看运行的容器 docker ps
-
执行mysql.sql,导入到MySQL数据库的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_status_gmt_modified` (`status` , `gmt_modified`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -- 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 = utf8mb4; -- the table to store lock data CREATE TABLE IF NOT EXISTS `lock_table` ( `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(128), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking', `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_status` (`status`), KEY `idx_branch_id` (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; CREATE TABLE IF NOT EXISTS `distributed_lock` ( `lock_key` CHAR(20) NOT NULL, `lock_value` VARCHAR(20) NOT NULL, `expire` BIGINT, primary key (`lock_key`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
执行结果截图
-
在Linux系统的根目录下,创建seata/config目录
mkdir -p /seata/config
-
在Linux系统的/seata/config目录下,拷贝一个seata的application.yml文件,配置store存储模式
-
seata容器规划
- 创建一个seata分布式容器,名称是seata-server
- 容器名为seata-server端口对外映射为7091和8091
# 查看seata镜像 docker search seata # 拉取seata镜像 docker pull seataio/seata-server # 查看拉取的镜像 docker images # 运行名为seata-server容器名的seataio/seata-server镜像 docker run -d --name seata-server -p7091:7091 -p8091:8091 -v /seata/config/application.yml:/seata-server/resources/application.yml seataio/seata-server:latest # 查看运行的容器 docker ps
执行结果截图
12.1.3 Docer-Conpose环境下部署Seata,并调整存储模式
-
在Linux系统的/seata目录下,新建一个init.sql
-- -------------------------------- The script used when storeMode is 'db' -------------------------------- CREATE DATABASE IF NOT EXISTS `seata`; USE seata; -- 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_status_gmt_modified` (`status` , `gmt_modified`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -- 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 = utf8mb4; -- the table to store lock data CREATE TABLE IF NOT EXISTS `lock_table` ( `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(128), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking', `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_status` (`status`), KEY `idx_branch_id` (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; CREATE TABLE IF NOT EXISTS `distributed_lock` ( `lock_key` CHAR(20) NOT NULL, `lock_value` VARCHAR(20) NOT NULL, `expire` BIGINT, primary key (`lock_key`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
-
在Linux系统的/seata目录下,新建一个application.yml
# Copyright 1999-2019 Seata.io Group. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. server: port: 7091 spring: application: name: seata-server logging: config: classpath:logback-spring.xml file: path: ${user.home}/logs/seata extend: logstash-appender: destination: 127.0.0.1:4560 kafka-appender: bootstrap-servers: 127.0.0.1:9092 topic: logback_to_logstash console: user: username: seata password: seata seata: config: # support: nacos, consul, apollo, zk, etcd3 type: file registry: # support: nacos, eureka, redis, zk, consul, etcd3, sofa type: file store: # support: file 、 db 、 redis mode: db db: datasource: druid db-type: mysql driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://MySQLDB/seata?serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true user: root password: ZYMzym111 min-conn: 10 max-conn: 100 global-table: global_table branch-table: branch_table lock-table: lock_table distributed-lock-table: distributed_lock query-limit: 1000 max-wait: 5000 # server: # service-port: 8091 #If not configured, the default is '${server.port} + 1000' security: secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017 tokenValidityInMilliseconds: 1800000 ignore: urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
-
在Linux系统的/seata目录下,新建一个docker-compose.yml
version: "3.8" services: #docker run -d --name MySQLDB -p3306:3306 -e MYSQL_ROOT_PASSWORD=ZYMzym111 -e LANG="C.UTF-8" mysql:latest db: image: mysql:latest container_name: MySQLDB ports: - 3306:3306 command: --max_connections=1000 --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci --default-authentication-plugin=mysql_native_password environment: MYSQL_ROOT_PASSWORD: password TZ: Asia/Shanghai LANG: C.UTF-8 volumes: - mysql-data:/var/lib/mysql - mysql-config:/etc/mysql - /seata/init.sql:/docker-entrypoint-initdb.d/init.sql restart: always networks: - net #docker run -d --name seata-server -p7091:7091 -p8091:8091 -v /seata/config/application.yml:/seata-server/resources/application.yml seataio/seata-server:latest tx: image: seataio/seata-server:latest container_name: seata-server ports: - 7091:7091 - 8091:8091 volumes: - /seata/application.yml:/seata-server/resources/application.yml networks: - net restart: always depends_on: - db networks: net: volumes: mysql-data: mysql-config:
12.2 Seata 集群部署
将Seata注册模式由原本的file改为nacos(seata.registry.type)
12.2.1 Windows环境下集群Seata
规划:注册模式由原本的file改为nacos
seata下载页面:https://seata.io/zh-cn/blog/download.html
-
下载Nacos的ZIP安装包
下载地址:https://github.com/alibaba/nacos/releases/download/2.1.1/nacos-server-2.1.1.zip
-
解压Nacos压缩包
-
进入bin目录
-
通过命令提示符进入该目录层级
-
执行startup.cmd -m standalone命令
-
启动成功,浏览器访问Nacos页面
访问地址:http://localhost:8848/nacos
用户名:nacos;密码:nacos
-
下载seata的ZIP安装包,点击soure和binary,下载源码包和编译之后的包
-
在解压seata-server-1.6.1.zip包中,进入seata/conf目录下,找到application.yml文件,配置registry存储模式
seata: config: # support: nacos, consul, apollo, zk, etcd3 type: file registry: # support: nacos, eureka, redis, zk, consul, etcd3, sofa type: nacos nacos: application: seata-server server-addr: 127.0.0.1:8848 group: SEATA_GROUP namespace: cluster: default username: password: context-path: ##if use MSE Nacos with auth, mutex with username/password attribute #access-key: #secret-key:
-
在解压seata-server-1.6.1.zip包中,进入seata的bin目录下
-
通过cmd进入当前目录,并执行seata-server.bat文件
-
项目中调整application.yml配置
seata: service: vgroup-mapping: default_tx_group: default tx-service-group: default_tx_group registry: type: nacos nacos: application: seata-server server-addr: localhost:8848 group: SEATA_GROUP namespace: cluster: default username: password:
12.2.1 Docker环境下集群Seata
规划:注册模式由原本的file改为nacos
-
规划Nacos
- 创建一个nacos容器,名称是nacos-server
- 容器名为nacos-server端口对外映射为8848、9848和9849
# 查看nacos镜像 docker search nacos # 拉取nacos镜像 docker pull nacos/nacos-server # 查看拉取的镜像 docker images # 运行名为nacos-server容器名的nacos/nacos-server镜像 docker run -d --name nacos-server -p8848:8848 -p9848:9848 -p9849:9849 -e MODE=standalone -e JVM_XMS=512m -e JVM_XMX=512m -e JVM_XMN=256m nacos/nacos-server:latest # 查看运行的容器 docker ps
-
在Linux系统的根目录下,创建seata/config目录
mkdir -p /seata/config
-
在Linux系统的/seata/config目录下,拷贝一个seata的application.yml文件,配置registry
-
seata容器规划
- 创建一个seata分布式容器,名称是seata-server
- 容器名为seata-server端口对外映射为7091和8091
# 查看seata镜像 docker search seata # 拉取seata镜像 docker pull seataio/seata-server # 查看拉取的镜像 docker images # 运行名为seata-server1容器名的seataio/seata-server镜像 docker run -d --name seata-server1 -p7091:7091 -p8091:8091 -e SEATA_IP=127.0.0.1 -e SERVER_NODE=1 -e SEATA_PORT=8091 -v /seata/config/application.yml:/seata-server/resources/application.yml seataio/seata-server:latest # 运行名为seata-server2容器名的seataio/seata-server镜像 docker run -d --name seata-server2 -p7092:7091 -p8092:8091 -e SEATA_IP=127.0.0.1 -e SERVER_NODE=2 -e SEATA_PORT=8092 -v /seata/config/application.yml:/seata-server/resources/application.yml seataio/seata-server:latest # 运行名为seata-server3容器名的seataio/seata-server镜像 docker run -d --name seata-server3 -p7093:7091 -p8093:8091 -e SEATA_IP=127.0.0.1 -e SERVER_NODE=3 -e SEATA_PORT=8093 -v /seata/config/application.yml:/seata-server/resources/application.yml seataio/seata-server:latest # 查看运行的容器 docker ps
执行结果截图
-
项目中调整application.yml配置
seata: service: vgroup-mapping: default_tx_group: default tx-service-group: default_tx_group registry: type: nacos nacos: application: seata-server server-addr: localhost:8848 group: SEATA_GROUP namespace: cluster: default username: password:
12.2.3 Docer-Conpose环境下部署集群Seata
-
在Linux系统的/seata目录下,新建一个application.yml
# Copyright 1999-2019 Seata.io Group. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. server: port: 7091 spring: application: name: seata-server logging: config: classpath:logback-spring.xml file: path: ${user.home}/logs/seata extend: logstash-appender: destination: 127.0.0.1:4560 kafka-appender: bootstrap-servers: 127.0.0.1:9092 topic: logback_to_logstash console: user: username: seata password: seata seata: config: # support: nacos, consul, apollo, zk, etcd3 type: file registry: # support: nacos, eureka, redis, zk, consul, etcd3, sofa type: nacos nacos: application: seata-server server-addr: localhost:8848 group: SEATA_GROUP namespace: cluster: default username: password: context-path: ##if use MSE Nacos with auth, mutex with username/password attribute #access-key: #secret-key: store: # support: file 、 db 、 redis mode: file # server: # service-port: 8091 #If not configured, the default is '${server.port} + 1000' security: secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017 tokenValidityInMilliseconds: 1800000 ignore: urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
-
在Linux系统的/seata目录下,新建一个docker-compose.yml
version: "3.8" services: #docker run -d --name nacos-server -p8848:8848 -p9848:9848 -p9849:9849 -e MODE=standalone -e JVM_XMS=512m -e JVM_XMX=512m -e JVM_XMN=256m nacos/nacos-server:latest nacos: image: nacos/nacos-server:latest container_name: nacos-server ports: - 8848:8848 - 9848:9848 - 9849:9849 environment: MODE: standalon JVM_XMS: 512m JVM_XMX: 512m JVM_XMN: 256m restart: always networks: - net #docker run -d --name seata-server1 -p7091:7091 -p8091:8091 -e SEATA_IP=127.0.0.1 -e SERVER_NODE=1 -e SEATA_PORT=8091 -v /seata/config/application.yml:/seata-server/resources/application.yml seataio/seata-server:latest seata1: image: seataio/seata-server:latest container_name: seata-server ports: - 7091:7091 - 8091:8091 volumes: - /seata/config/application.yml:/seata-server/resources/application.yml environment: SERVER_NODE: 1 SEATA_IP: 127.0.0.1 SEATA_PORT: 8091 networks: - net restart: always depends_on: - nacos seata2: image: seataio/seata-server:latest container_name: seata-server ports: - 7092:7091 - 8092:8091 volumes: - /seata/config/application.yml:/seata-server/resources/application.yml environment: SERVER_NODE: 2 SEATA_IP: 127.0.0.1 SEATA_PORT: 8092 networks: - net restart: always depends_on: - nacos seata3: image: seataio/seata-server:latest container_name: seata-server ports: - 7093:7091 - 8093:8091 volumes: - /seata/config/application.yml:/seata-server/resources/application.yml environment: SERVER_NODE: 3 SEATA_IP: 127.0.0.1 SEATA_PORT: 8093 networks: - net restart: always depends_on: - nacos networks: net: volumes: mysql-data: mysql-config:
13. 总结
本篇文章,涉及到的技术有:
- 基础技术
- JAVA基础、MySQL数据库、Linux系统操作命令、Docker、Docker-Compose
- 框架技术
- SpringBoot、SpringCloud、SpringCloudAlibaba
- 组件技术
- JDBC、MyBatis、Mybatis-plus
- Druid、ShardingSphere
- RestTemplate、OpenFeign、Sentinel
- Nacos、Seata
评论区