一 seate分布式事务
1 业务
分布式事务的产生,是由于数据库的拆分(拆成多个数据库了,即本来数据只存储在一个数据库中,现在分布地存储在多个数据库中)和分布式的应用架构(微服务)带来的。
常见的分布式事务应用场景有哪些?在常规情况下,我们在一个进程(java程序/项目)中操作一个数据库,这属于本地事务。但如果在一个进程中操作多个数据库,或者在多个进程中操作一个或多个数据库,就产生了分布式事务;
2 需求
- 仍然保证分布式事务的ACID、事务隔离级别、事务的传播级别
3 解决方案
(1)功能"集":AT、TCC、SAGA和XA
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务;Seata为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的分布式解决方案;
(2)"神工具":seata
4 完成学习
(1) 思想、思路
- 本地的事务:数据库本地的事务(ACID、事务隔离性、事务传递性)
- 分布式事务:如果在一个进程(java程序/项目)中操作多个数据库,或者在多个进程(java程序/项目)中操作一个或多个数据库,就产生了分布式事务;
(2) 体系组织
- Alibaba的产品、开源 、AT/TCC/SAGA和XA
- TCC事务模式:资金交易相关、比较严谨的事务管理模式、如银行
- XA事务模式:正在开发中...,其他事务模式已经实现;
- 目前使用的流行度情况是:AT > TCC 或 AT+TCC混合 > Saga;
- 列表显示,大部分公司使用的是AT模式。原理是,改良后的两阶段提交方案。
- TCC是三阶段提交的方案。
- 数据库分库分表就产生了分布式事务;
- 项目拆分服务化也产生了分布式事务;
(3) 原理,流程
在Seata的架构中,一共有三个角色:
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚;
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务;
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源(cat:就是管理和操作具体的,各个分支的那些事务),与TC交互以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚;
其中TC为单独部署的 Server 服务端,TM和RM为嵌入到应用中的 Client 客户端;
- Cat:案例1
- 比如要对3个数据库进行操作。
- TM是整个事务的发启者(人)。
- TC是整个事务的协调者(人)。
- RM是管理和操作具体的,各个分支的那些事务。
在Seata中,一个分布式事务的生命周期如下:
TM请求TC开启一个全局事务,TC会生成一个XID作为该全局事务的编号,XID会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起;(ps:即在3个微服务中都可以拿到这个全局事务的编号。而3个微服务中的事务,都是全局事务的子事务(分支事务)。这里RM,相当于一个子事务(分支事务,或称为本地事务)。)
RM请求TC将本地事务注册为全局事务的分支事务,通过全局事务的XID进行关联;
TM请求TC告诉XID对应的全局事务是进行提交还是回滚;
TC驱动RM将XID对应的自己的本地事务进行提交还是回滚;(ps:TC就会把3个微服务中的每个RM提交)
ps:如上图所示:
- 有3个微服务(每个Microservices是代表一个微服务)。
- 第1个微服务
- 有1个TM,事务管理器,事务的发起者。事务开始,即第1 ~ 3个微服务对数据库的所有操作,都要包含在此事务中。要么都提交成功,要么都回滚。
- 有2个RM,即第1个微服务内部也要操作数据库,即也有自己的事务处理。
- 第2个微服务(由第1个微服务调用)
- 有2个RM,即第1个微服务内部也要操作数据库,即也有自己的事务处理。
- 第3个微服务(由第2个微服务调用)
- 有2个RM,即第1个微服务内部也要操作数据库,即也有自己的事务处理。
- TC去协调所有的事务处理。
5 TC Server运行环境部署(TC,中间件,是单独的服务器)
(1)单机环境:学习或测试。linux。
- 下载:必须下二进制binary
- 解压:tar -zxvf seata-server-1.3.0.tar.gz。得seate目录。
- bin目录:启动脚本
- .bat是window下启动
- .sh是linux下启动
- conf目录:配置文件
- lib目录:jar包
- java语言写的。
- 包含自己开发的一些jar包。
- 还依赖了一些第三方jar包。
- LICENSE:许可证
- bin目录:启动脚本
-
切换:cd seata
-
启动
-
注:默认seata-server.sh脚本设置的jvm内存参数2G,我们再虚拟机里面做实验,可以改小一点;
-
vim /seate/bin/seata-server.sh
-
-
前台启动命令:./seata-server.sh
-
后台启动命令:nohup + &号
-
端口被占用:
-
netstat -alnp | grep 8125
-
kill - 9 1899
-
-
启动内容解析:/home/seate/seata/logs/start.out
-
默认端口:8091
-
-
数据持久化:
-
修改数据持久化模式:/home/seate/seata/conf/application.yml ->mode: file
-
file:/home/seate/seata/bin/sessionStore/root.data
-
因为我们没有修改任何配置文件,默认情况seata使用的是file模式进行数据持久化,所以可以看到用于持久化的本地文件 root.data
-
0kb。因为没有开启事务、没有注册事务、没有注册服务、没有开启事务操作。
-
-
-
验证:jps -l
-
(2)集群环境:生产。linux。
- 业务:Seata TC Server集群
- 需求:避免TC服务器的单点故障,实现高可用。
- 解决方案:集群部署TC Server
- 搭建
- 思想、思路
-
多个 Seata TC Server 通过 db 数据库或者redis实现全局事务会话信息的共享;
-
每个Seata TC Server注册自己到注册中心上(nacos),应用从注册中心获得Seata TC Server实例。
-
-
体系组织
-
模式如下图所示:
- 第一步:我们有一个应用程序,这个应用程序要实现分布式事务。应用程序中在依赖了jar包后,相当于自带了TM和RM。
- 第二步:部署2个TC Server,都做为一个微服务注册到注册中心上。2个TC Server共享同一份分布式事务编号、状态、TM和RM数据,数据可以持久化到file、db、redis等。
- 第三步:应用程序连接TC Server时。首先,会去注册中心获取注册的TC Server的实例(从而知道TC Server的IP和Port),其次,通过IP和Port去连接具体的TC Server。
-
- 具体搭建步骤
- 第一步:安装nacos
- 第二步:安装mysql
- 第三步:安装2个TC Server
- 第四步:2个TC Server数据库环境的准备,并都连接对应的mysql数据库
- 第1步:mysql中初始化 Seata TC Server 的 db 数据库,在 MySQL 中,创建 seata 数据库,并在该库下执行如下SQL脚本:使用seata-1.3.0\script\server\db脚本(网盘有共享)
-
第2步:修改 seata/conf/file.conf 配置文件,修改使用 db 数据库,实现 Seata TC Server 的全局事务会话信息的共享;
(1)mode = "db"
(2)数据库的连接信息:
driverClassName = "com.mysql.cj.jdbc.Driver"
url = "jdbc:mysql://39.99.163.122:3306/seata"
user = "mysql"
password = "UoT1R8[09/VsfXoO5>6YteB"
-
第3步:检查数据库驱动与数据库版本是否匹配。
-
第五步:2个TC Server注册到nacos中
-
修改 seata/conf/registry.conf 配置文件,设置使用 Nacos 注册中心;
- type = "nacos"
- Nacos连接信息:
-
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = ""
password = ""
}
-
-
-
第六步:启动数据库
-
第七步:启动nacos;
-
第八步:启动两个 TC Server
-
解决端口冲突问题
-
执行 ./seata-server.sh -p 18091 -n 1 命令,启动第一个TC Server;
-p:Seata TC Server 监听的端口;
-n:Server node,在多个 TC Server 时,需区分各自节点,用于生成不同区间的 transactionId 事务编号,以免冲突;
执行 ./seata-server.sh -p 28091 -n 2 命令,启动第二个TC Server;
-
-
-
第九步:验证:打开Nacos注册中心控制台,可以看到有两个Seata TC Server 实例;
-
第十步:应用测试
- 思想、思路
6 AT模式事务案例1:单体应用多数据源分布式事务
- 单体应用:就一个应用。即一个系统,操作两个或多个数据库。在Spring Boot单体项目中,如果使用了多数据源,就需要考虑多个数据源的数据一致性,即产生了分布式事务的问题,我们采用Seata的AT事务模式来解决该分布式事务问题;就相当于我们这个图:
-
以电商购物下单为例。如下图所示,是一个springboot项目,操作了3个数据库。
-
程序运行流程:
-
第一步:有一个订单Controller,即OrderController。用户请求OrderController,发起一个下单操作。
-
第二步:OrderController调用OrderService
-
第三步:OrderService调用ProductService,减库存。
-
第四步:OrderService调用AccountService,减用户余额。
-
第五步:真正插入订单记录。
-
-
分布式事务分析:
-
事务包含了更新库存数据库、更新账户数据库、更新订单数据库,而且事务内的3个操作要么都成功,要么都失败(全局回滚)。
-
-
案例演示:
-
第一步:环境准备。准备数据库(3个)、表(订单表、库存表、账户表、undo_log表)和数据
-
注意:其中每个库中都要额外创建一个undo_log表,它是 Seata AT模式必须创建的表,主要用于分支事务的回滚。
-
字段固定。官方文档(官网)有sql语句。
-
-
-
第二步:开发一个SpringBoot单体应用(即仅仅一个应用程序)
-
第1步:pom.xml,添加依赖。web依赖、lombok依赖、mysql依赖、mybatis依赖、seata依赖、动态数据源(作者是mybatisplus开发者。开源。加入此依赖的可以在一个项目中创建多个数据源来连接多个数据库。)、父依赖、编译设置
-
第2步:application.properties配置
-
第3步:写代码
-
第a步:程序入口类。main方法。
-
第b步:XxxController
-
注入XxxService
-
-
第c步:XxxService。接口和实现类。
-
@OS注解,选择数据源。
-
@GlobalTransactional //seata全局事务注解,开启全局事务,然后就有事务了。类似于spring事务注解@Transaction。这个地方的注解,相当于TM,即全局事务的发起者。此注解放在方法上,此方法有全局分布式事务管理。此注解放在类上,此类的所有方法都有全局的分布式事务管理。
-
注解定义了超时时间
-
注解定义了什么时候提交,如何提交
-
注解定义了什么时候回滚,如何回滚,及异常后回调的方法
-
可以修改事务的隔离性
-
可以修改方法的事务的传播性
-
-
方法。方法体内容:
-
减库存(数据源1)。RM1,资源管理者1。
-
减余额(数据源2)。RM2,资源管理者2。
-
添加订单记录(数据源3)。RM3,资源管理者3。
-
-
-
第d步:mybatis的mapper
-
第e步:实体类
-
-
-
第三步:测试。
-
启动程序,并访问:http://localhost:8080/order?userId=1&productId=1
-
-
第四步:效果展示
-
订单添加、减库存、减余额,3个操作要么都成功,要么都回滚(全局回滚)。
-
注:有了seata事务(@GlobalTransactional )以后,可以不加spring的事务(@Transaction)
-
-
-
7 AT模式事务案例2:微服务的分布式事务
- 微服务的分布式事务:也就是在springcloud alibaba里面引用seata来管理分布式事务。这种模式下,就相当于我们这个图:
- 第一步:我们会有3个微服务(订单微服务、产品微服务、账户微服务)。
- 第二步:订单微服务中,OrderController调用OrderService。
- 第三步:订单微服务中,OrderService发起了一个远程的feign调用,调用的是产品微服务,对产品库存进行更新操作。
- 第四步:订单微服务中,OrderService发起了一个远程的feign调用,调用的是账户微服务,对账户余额进行更新操作。
- 第五步:前面操作都成功后,订单微服务中的OrderService在订单数据库中插入订单记录。
- 案例演示
- 第一步:环境准备。准备数据库(3个)、表(订单表、库存表、账户表、undo_log表)和数据
- 第二步:程序开发。springboot、springcloud allibaba、feign、seata
- 通用项目commons
- 普通的maven项目就可以了
- 它主要编写的是model类、feign接口
- pom.xml中加入lombok依赖、加入feign(open feign)依赖
- 包含:订单feign接口、产品feign接口、账户feign接口。订单实体类、产品实体类、账户实体类。
- 开发订单微服务应用程序
- 第1步:pom.xml。引入依赖,web依赖、nacos依赖(服务注册与发现)、lombok依赖、mysql依赖、mybatis依赖、seata依赖、通用项目Commons(普通的maven项目就可以了,它主要编写的是model类、feign接口)、父依赖、编译设置
- 第2步:application.properties。
- 第a步:配置端口
- 第b步:配置项目名称
- 第c步:订单数据库4要素
- 第d步:连接nacos,服务注册与发现
- 第e步:seata的相关的配置
- 如下图所示:
- 第3步:编码
- 第a步:程序入口,主类,main方法(运行)
- 注解:开启服务的注册与发现
- 注解:开启feign(远程)调用功能
- 第b步:XxxController
- 注入XxxService
- 第c步:XxxService
-
@GlobalTransactional //seata全局事务注解,开启全局事务,然后就有事务了。类似于spring事务注解@Transaction。这个地方的注解,相当于TM,即全局事务的发起者。此注解放在方法上,此方法有全局分布式事务管理。此注解放在类上,此类的所有方法都有全局的分布式事务管理。
-
注解定义了超时时间
-
注解定义了什么时候提交,如何提交
-
注解定义了什么时候回滚,如何回滚,及异常后回调的方法
-
可以修改事务的隔离性
-
可以修改方法的事务的传播性
-
-
方法。方法体内容:
-
feign远程调用。减库存(数据源1)。RM1,资源管理者1。
-
feign远程调用。减余额(数据源2)。RM2,资源管理者2。
-
feign远程调用。添加订单记录(数据源3)。RM3,资源管理者3。
-
-
- 第a步:程序入口,主类,main方法(运行)
- 开发产品微服务应用程序
- 基本同上
- 开发账户微服务应用程序
- 基本同上
- 通用项目commons
-
第三步:启动naocs,登录、账户两nacos
-
第三步:测试。
-
启动3个程序,并访问。
-
-
第四步:效果展示
-
订单添加、减库存、减余额,3个操作要么都成功,要么都回滚(不管在哪个地方招聘异常,只要不try/catch,那么都会全局回滚)。
-
注:有了seata事务(@GlobalTransactional )以后,可以不加spring的事务(@Transaction)
-
8 AT事务模式分布式事务工作机制
-
前提
-
基于支持本地 ACID 事务的关系型数据库;(mysql、oracle)
-
Java 应用,通过JDBC访问数据库;
-
-
整体机制:就是两阶段提交协议的演变:
-
一阶段:“业务数据“和“回滚日志记录(即undo_log表)“在同一个本地事务中提交,释放本地锁和连接资源;(本地事务,就已经在数据库持久化了)
-
二阶段:
-
如果没有异常,就异步化提交,非常快速地完成;(正常情况,那就提交了。提交时,同步一下TC Server的状态,删除回滚日志(undo_log表)。)
-
如果有异常就回滚,通过一阶段的回滚日志进行反向补偿;(回滚的操作有:比如订单删除,库存加回去,余额加回去)
-
-
-
具体举例说明整个AT分支的工作过程:
业务表:product
Field Type Key
id bigint(20) PRI
name varchar(100)
since varchar(100)
AT分支事务的业务逻辑:
update product set name = 'GTS' where name = 'TXC';
一阶段过程:
1、解析SQL,得到SQL的类型(UPDATE),表(product),条件(where name = 'TXC')等相关的信息;
2、查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据;
select id, name, since from product where name = 'TXC';
得到前镜像:
id name since
1 TXC 2014
3、执行业务 SQL:更新这条记录的 name 为 'GTS';
4、查询后镜像:根据前镜像的结果,通过 主键 定位数据;
select id, name, since from product where id = 1;
得到后镜像:
id name since
1 GTS 2014
5、插入回滚日志:把前后镜像数据以及业务SQL相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中;
6、分支事务提交前,向TC注册分支,申请product表中,主键值等于1的记录的全局锁(在当前的同一个全局事务id范围内是可以申请到全局锁的,不同的全局事务id才会排斥);
7、本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交;
8、将本地事务提交的结果上报给TC;
二阶段-回滚
1、收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作;
2、通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录;
3、数据校验:拿 UNDO LOG 中的后镜像与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改,这种情况,需要人工来处理;
4、根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
update product set name = 'TXC' where id = 1;
5、提交本地事务,并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC;
二阶段-提交
1、收到TC的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给TC;
2、异步任务阶段的分支提交请求将异步和批量地删除相应UNDO LOG记录;
回滚日志表:
Field Type
branch_id bigint PK
xid varchar(100)
context varchar(128)
rollback_info longblob
log_status tinyint
log_created datetime
log_modified datetime
SQL建表语句:
CREATE TABLE `undo_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`branch_id` bigint NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int 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=4 DEFAULT CHARSET=utf8;
9 读写隔离:AT事务模式在多线程环境下,会不会存在数据一致性的问题?(可以用JMeter进行测试)
(1)业务:AT事务模式。多线程环境下。数据一致性的问题。
-
此前有,微服务架构下有订单微服务、产品微服务、账户微服务,3个服务都各自操作一个数据库(订单数据库,库存数据库,账户数据库)。
-
多线程:如果有多个线程(10个、50个或100个用户)同时都通过订单微服务(去添加订单记录、减库存、减余额),那么如何保证数据的一致性?
(2)需求:如何保证各个数据库的数据一致性,避免脏读写(读写脏数据)。
(3)解决方案:seata。写隔离、读隔离。
(4)写隔离:解决多线程下写数据的冲突。避免脏写(写脏数据)。
- AT事务模式是两阶段提交协议的演变。
- 一阶段本地事务提交前,需要确保先拿到全局锁;
- 全局全局,即分布式全局锁就一把。
- 分布式全局锁是seata给我们提供的,在多线程情况下,只有一个线程能够拿到全局锁。
- 分布式全局锁是从TC服务器那里拿的。
- 拿 全局锁 的尝试被限制在一定范围内(源码中是尝试10次),超出范围(10次尝试)将放弃获取全局锁,并回滚本地事务,同时释放数据库的本地锁(行锁);
- 如果拿不到 全局锁 ,不能提交本地事务;
- 如果拿到全局锁,那就可以正常地修改数据库数据并持久化。
- 一阶段本地事务提交前,需要确保先拿到全局锁;
- 官方文档以一个示例来说明:
- 两个或者多个全局事务 tx1 和 tx2,分别并发对 a 表的 m 字段进行更新操作,m 的初始值 1000;
- 正常提交情况:
- 假设tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900,本地事务提交前,先拿到该记录的 全局锁 ,拿到了全局锁,本地提交并释放本地锁;
- tx2后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800,本地事务提交前,尝试拿该记录的 全局锁 ,tx1全局提交前,该记录的全局锁一直会被 tx1 持有,tx2 需要重试等待 全局锁 ;如果在尝试范围内都获取不到全局锁,tx2下的所有分支事务都将回滚。
- tx1的二阶段全局提交完成,同时释放全局锁。此时,tx2才有机会拿到全局锁(如果还在尝试的话),也才有机会进行二阶段的全局提交(完成提交本地事务)。
- 如下图所示:
- 回滚的情况
- 如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚;
- 此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功;
- 因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题;
- 如下图所示:
- 底层原理
- 本地锁,即数据库的行级锁。
- 底层其实就是通过jdbc来set数据库的自动提交等于0,然后在全局提交事务时commit手动提交。只是在手动commit事务之前,seata使用aop的方式增加了对锁的判断。
(5)读隔离:解决多线程下读数据的冲突。避免脏读(读脏数据)。
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,而Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted);
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过在你的sql语句中加入 SELECT FOR UPDATE 语句的代理;这样才能改掉Seata读未提交的全局隔离级别,从而使全局变成读已提交或以上的隔离级别。
SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试,这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回;
出于总体性能上的考虑,Seata目前的方案并没有对所有SELECT语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句;
10 TC Server集群环境的使用案例
对于SpringBoot单体应用:
1、添加nacos客户端依赖;
<!-- nacos-client:排除掉旧版本,依赖新版本 -->
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.3.1</version>
</dependency>
2、配置application.properties文件
#----------------------------------------------------------
# Seata应用编号,默认为${spring.application.name}
seata.application-id=springcloud-order-seata
# Seata事务组编号,用于TC集群名
seata.tx-service-group=springcloud-order-seata-group
# 虚拟组和分组的映射
seata.service.vgroup-mapping.springcloud-order-seata-group=default
#seata-spring-boot-starter 1.1版本少一些配置项
seata.enabled=true
seata.registry.type=nacos
seata.registry.nacos.cluster=default
seata.registry.nacos.server-addr=192.168.172.128:8848
seata.registry.nacos.group=SEATA_GROUP
seata.registry.nacos.application=seata-server
#----------------------------------------------------------
对于Spring Cloud Alibaba微服务应用:
则不需要加nacos的jar包依赖,application.properties文件配置完全一样;
11 TCC模式的理解与应用
(1)TCC事务模式执行机制
业务需求:AT模式(只用于关系型数据库)基本上能满足我们使用分布式事务大部分需求,但涉及非关系型数据库与中间件的操作、跨公司服务的调用、跨语言的应用调用就需要结合TCC模式;
流原:TCC事务模式下,一个分布式的全局事务,整体也是两阶段提交(Try - [Comfirm/Cancel],共3个步骤)的模型,在Seata中,AT模式与TCC模式事实上都是基于两阶段提交,它们的区别在于:AT模式基于支持本地ACID事务的关系型数据库:
下面是AT事务模式的3个步骤:
- 第一步:prepare行为:在本地事务中,一并提交“业务数据更新“和”相应回滚日志记录”;(seata帮我们实现)
- 第二步:commit 行为:马上成功结束,自动异步批量清理回滚日志;(seata帮我们实现)
- 第三步:rollback 行为:通过回滚日志,自动生成补偿操作,完成数据回滚;(seata帮我们实现)
而TCC 模式,需要我们人为编写代码实现提交和回滚:
- 第一步: prepare 行为:调用自定义的 prepare 逻辑;(真正要做的事情(逻辑操作),比如插入订单,更新库存,更新余额)
- 第一步:commit 行为:调用自定义的 commit 逻辑;(自己写代码实现,seata不帮我们实现)
- 第一步:rollback 行为:调用自定义的 rollback 逻辑;(自己写代码实现,seata不帮我们实现)
所以TCC模式,就是把自定义的分支事务的提交和回滚,并把分支事务纳入到全局事务管理中;
通俗来说,Seata的TCC模式就是手工版本的AT模式(手工写代码),它允许你自定义两阶段的处理逻辑而不需要依赖AT模式的undo_log回滚表(因为手动);
TCC事务模式应用实践
(2)案例1:基于SpringBoot单体应用的TCC事务
- 第一步:添加依赖。同AT。
- 第1步:nacos。目的是使用TC Server集群。
- 第二步:配置文件。同AT。
- 第三步:代码
- 第1步:程序入口类。同AT。
- 第2步:XxxController。同AT。
- 第3步:XxxService
- @LocalTCC注解。带参数方便在回滚方法中获取,并回滚。
- 案例:
@LocalTCC
public interface AccountService {
/**
* 扣除余额
* 定义两阶段提交
* name = reduceStock为一阶段try方法
* commitMethod = commitTcc 为二阶段确认方法
* rollbackMethod = cancel 为二阶段取消方法
* BusinessActionContextParameter注解 可传递参数到二阶段方法
*
* @param userId 用户ID
* @param money 扣减金额
* @throws Exception 失败时抛出异常
*/
@TwoPhaseBusinessAction(name = "reduceBalance", commitMethod = "commitTcc", rollbackMethod = "cancelTcc")
void reduceBalance(@BusinessActionContextParameter(paramName = "userId") Integer userId,
@BusinessActionContextParameter(paramName = "money") BigDecimal money);
/**
* 确认方法、可以另命名,但要保证与commitMethod一致
* context可以传递try方法的参数
*
* @param context 上下文
* @return boolean
*/
boolean commitTcc(BusinessActionContext context);
/**
* 二阶段取消方法
*
* @param context 上下文
* @return boolean
*/
boolean cancelTcc(BusinessActionContext context);
}
- 第4步:XxxServiceImpl
- 业务方法。同AT。
- @OS注解:指定连接的数据源。同AT。
- @GlobalTransactional注解:全局事务。同AT。
- 手动编写提交逻辑代码:实现commitTcc()方法
- true提交成功
- 如:
- 手动编写回滚逻辑代码:实现cancelTcc()方法
- true回滚成功
- 如:
- 业务方法。同AT。
- 第5步:mapper。同AT。
- 第6步:实体类。同AT。
- 第四步:测试
- 如果哪步都成功了,seata会自动帮我们调用commitTcc()方法。
- 如果哪步有失败的,seata会自动帮我们调用cancelTcc()方法。
@LocalTCC注解标识此TCC为本地模式,即该事务是本地调用,非RPC调用,@LocalTCC一定需要注解在接口上,此接口可以是寻常的业务接口,只要实现了TCC的两阶段提交对应方法即可;
@TwoPhaseBusinessAction,该注解标识为TCC模式,注解try方法,其中name为当前tcc方法的bean名称,写方法名便可(全局唯一),commitMethod指提交方法,rollbackMethod指事务回滚方法,指定好三个方法之后,Seata会根据事务的成功或失败,通过动态代理去帮我们自动调用提交或者回滚;
@BusinessActionContextParameter 注解可以将参数传递到二阶段(commitMethod/rollbackMethod)的方法;
BusinessActionContext 是指TCC事务上下文,携带了业务方法的参数;
(3)案例2:基于Spring Cloud Alibaba的TCC分布式事务
- 第一步:依赖。同单体。
- 第二步:配置文件。同单体。
- 第三步:编码。同单体。
具体代码实现和springboot单体应用的代码实现几乎没有区别,具体参考Git上提交的代码;
由于Seata出现时间并不长,也在不断的改进中,在实际面试中应该不会问大家比较底层的实现,同学们如果感兴趣的话,基于我们已有的源码阅读经验,可以看一下Seata的源码,它如何进行事务隔离保证数据一致性,官方提供的文档并不详细;
二 spring事务
1 思想、思路
本章内容主要包含两部分:Spring所使用的操作数据库的技术之一,JDBC模板的使用;
另一部分则为Spring对于事务的管理。Spring与Dao部分,是Spring的两大核心技术IOC与AOP的典型应用体现:
- 对于JDBC模板的使用,是IOC的应用,是将JDBC模板对象注入给了Dao层的实现类。
- 对于Spring的事务管理,是AOP的应用,将事务作为切面织入到了Service层的业务方法中。
事务原本是数据库中的概念,在Dao层。但一般情况下,需要将事务提升到业务层,即Service层。这样做是为了能够使用事务的特性来管理具体的业务。
2 体系组织(3种事务管理、2个事务相关的接口)
(1)Spring中三种方式来实现对事务的管理:
- 使用Spring的事务代理工厂管理事务。
- 使用Spring的事务注解管理事务。
- 使用AspectJ的AOP配置管理事务。
(2)Spring的2个事务相关的接口。
- 事务管理器接口
- 事务管理器是PlatformTransactionManager 接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息:
- 常用的两个实现类:
PlatformTransactionManager接口有两个常用的实现类。
DataSoureTransactionManager:使用JDBC或iBatis进行持久化数据时使用。
HibernateTransactionManager:使用Hibernate进行持久化数据时使用。 - Spring 的回滚方式:
Spring事务的默认回滚方式是:发生运行时异常时回滚,发生受查异常时提交。不过,对于受查异常,程序员也可以手工设置其回滚方式。
- 事务管理器是PlatformTransactionManager 接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息:
- 事务定义接口。事务定义接口TransactionDefinition中定义了事务描述相关的三类常量:
- 事务隔离级别
- 事务传播行为
- 事务默认超时时限
- 及对它们的操作
3 流原
- 程序举例环境搭建
- Step1:创建数据库表
- Step2:创建实体类
- Step3:定义dao接口
- Step4:定义dao实现类
- Step5:定义异常类
- Step6:定义Service接口
- Step7:定义service的实现类
- Step8:Spring 配置文件中添加最全约束
- Step9:修改Spring配置文件内容
- 配置cp30数据源
- IOC、Dao、Service
- Step10:定义测试类
- 使用Spring的事务代理工厂管理事务
- Step1:复制项目
- Step2:导入jar包
- Step3:在容器中添加事务管理器DataSourceTransactionManager:
- Step4:在容器中添加事务代理TransactionProxyFactoryBean
- Step5:修改测试类
- 使用Spring的事务注解管理事务
- Step1:复制项目
- Step2:在容器中添加事务管理器
- Step3:在Service实现类方法上添加注解
- Step4:修改配置文件内容
- Step5:修改测试类
- 使用AspectJ的AOP配置管理事务(重点)
- Step1:复制项目
- Step2:导入jar包
- Step3:在容器中添加事务管理器
- Step4:配置事务通知
- Step5:配置顾问。指定将配置好的事务通知,织入给谁。
- Step6:修改测试类
三 mybatis事务
1 Mybatis与jdbc的关系
2 事务控制基本操作流程
(1)mybatis.xml中配置事务管制器类型
(2)java代码
3 默认需要手动提交事务(事务管理器)
- <transactionManager type="JDBC"/> 该标签用于指定 MyBatis所使用的事务管理器。
- MyBatis 支持两种事务管理器类型:JDBC 与 MANAGED。
- JDBC:使用 JDBC 的事务管理机制。即,通过 Connection 的 commit()方法提交,通过 rollback()方法回滚。但默认情况下,MyBatis 将自动提交功能关闭了,改为了手动提交。即程序中需要显式的对事务进行提交或回滚。从日志的输出信息中可以看到。
-
MANAGED :由容器来管理事务的整个生命周期(如 Spring 容器)。
-
注:推荐手动提交事务。因为复杂业务中一个事务会包括多个DML操作,自动提交只能做到一个事务只有一个DML操作。
例如:```java public class StudentServiceImpl implements StudentService {
public boolean addStudent(Student student) { boolean b = false; SqlSession sqlSession = MyBatisUtil.getSqlSession(); try{ StudentDAO studentDAO = sqlSession.getMapper(StudentDAO.class); int i = studentDAO.insertStudent(student); b = i>0; sqlSession.commit(); }catch (Exception e){ sqlSession.rollback(); } return b; }
} ```
4 自动提交事务
例如:MyBatisUtil优化
```java public class MyBatisUtil {
private static SqlSessionFactory factory; private static final ThreadLocal<SqlSession> local = new ThreadLocal<SqlSession>(); static{ try { InputStream is = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); factory = builder.build(is); } catch (IOException e) { e.printStackTrace(); } } public static SqlSessionFactory getFactory(){ return factory; } private static SqlSession getSqlSession(boolean isAutoCommit){ SqlSession sqlSession = local.get(); if(sqlSession == null ){ sqlSession = factory.openSession(isAutoCommit); local.set(sqlSession); } return sqlSession; } //手动事务管理 public static SqlSession getSqlSession(){ return getSqlSession(false); } //自动事务提交 public static <T extends Object>T getMapper(Class<T> c){ SqlSession sqlSession = getSqlSession(true); return sqlSession.getMapper(c); }
} ```
业务逻辑层自动事务管理
```java public class StudentServiceImpl implements StudentService {
private StudentDAO studentDAO = MyBatisUtil.getMapper(StudentDAO.class); public boolean addStudent(Student student) { int i = studentDAO.insertStudent(student); boolean b = i>0; return b; }
} ```
四 hibernate事务
1 事务控制基本操作流程
在Hibernate 中,可以通过代码来操作管理事务,如:
- 通过“Transaction tx =session. beginTransaction(),"开启一个事务;
- 持久化操作后,
- 通过“tx. commit();”提交事务;
- 如果事务出现异常,又通过“tx rollback);"操作来撤销事务(事务回滚)。
2 事务的并发问题
(1)读并发
(2)写并发
3 加锁机制
4 Hibernate 并发问题解决
(1)设置 Hibernate 事务隔离级别
Hibernate 建议设置事务隔离级别为 4 级,即可重复读。从 Hibernate 框架解压目录\project\etc\hibernate.properties 的默认设置中可以看到。
Hibernate 配置文件中可以对隔离级别进行设置。
(2)Hibernate 实现乐观
在 Hibernate 映射文件的<class/>标签中,有一个子标签<version/>,其 name 属性用于指 定作为版本的属性名称。其还有一个子标签<timestamp/>用于指定作为时间戳的属性名称。 这两个子标签的用法相同,不同的是,作为版本的属性要求其类型为 int,而作为时间 戳的属性,要求为其类型为 java.sql.timestamp。
(3)Hibernate 实现悲观
1)添加排他锁
对于排他锁,其加锁的时间为通过 get()方法执行 select 查询后,解锁时间为当前事务结 束。这期间,当前事务是可以修改被其加锁的数据的,但其它事务是无法对该数据进行修改的。
查看控制台输出的 SQL 语句,可看到 select 语句后添加了 for update。证明添加了写锁。
对于共享锁,其加锁的时间也为通过 get()方法执行 select 查询后,但其解锁时间则是在其查询的数据被检索出来后的瞬间。即从程序运行来看,get()方法开始执行则加上了读锁,get()方法运行结束,则读锁解除。所以,加了读锁的 get()方法后的修改语句,与读锁是没有任何关系的。
从后台运行的 SQL 中可以看到,多了 lock in share mode,说明添加了共享锁。
5 service层的事务管理(JTA)
到这我们已经设置了事务的隔离级别,那么我们在真正进行事务管理的时候,需要考虑事务的应用的场景,也就是说我们的事务控制不应该是在DAO层实现的,应该在Service层实现,并且在Service中调用多个DAQ实现一个业务逻辑的操作。具体操作如下显示:
五 jdbc事务
1 事务控制基本工作流程
- 自动提交:在JDBC中,事务操作缺省是自动提交。
- 一条对数据库的DML(insert、update、delete)代表一项事务操作
- 操作成功后,系统将自动调用commit()提交,否则自动调用rollback()回滚
- 事务开始的边界则不是那么明显了,它会开始于组成当前事务的所有statement中的第一个被执行的时候。
- 事务结束的边界是commit或者rollback方法的调用
- 手动提交:在JDBC中,获得Connection对象开始事务、提交或回滚事务、关闭连接。其事务策略是
- 第1步:获得Connection对象(建立一个数据库连接)
- 第2步:打开事务,同时关闭事务的自动提交:conn.setAutoComit(false);//true等价于1, false等价于0
- 第3步:手动提交事务:conn.commit();
- 第4步:手动回滚事务 conn.rollback();
- 比如:出现异常,在异常捕获时调用rollback()进行回滚,回复至数据初始状态
- 部分提交:
- 当只想撤销事务中的部分操作时可使用SavePoint SavePoint sp = connection.setSavepoint(); connection.rollerbak(sp);connection.commit();
2 service层事务控制:基本方法
- 第一步:获得Connection对象(建立一个数据库连接)
- 第二步:关闭事务的自动提交:conn.setAutoComit(false);//true等价于1, false等价于0
- 第三步:手动提交事务:conn.commit();
- 第四步:手动回滚事务 conn.rollback();
- 出现异常
3 service层事务控制:传递connection法,跨多线程(污染接口)
为了解决线程中Connection对象不同步的问题,可以将Connection对象通过service传递给各个DAO方法吗?
- 如果使用传递Connection,容易造成接口污染(BadSmell)。
- 定义接口是为了更容易更换实现,而将Connection定义在接口中,会造成污染当前接口。
4 service层事务控制:ThreadLocal法,跨多线程
可以将整个线程中(单线程)中,存储一个共享值。线程拥有一个类似Map的属性,键值对结构<ThreadLocal对象,值>。一个线程共享同一个ThreadLocal,在整个流程中任一环节可以存值或取值。
5 service层事务控制:跨越多个数据源的事务,使用JTA容器实现事务
- 跨越多个数据源的事务,使用JTA容器实现事务。 分成两阶段提交。
- javax.transaction.UserTransaction tx = (UserTransaction)ctx.lookup(“jndiName"); tx.begin();
- //connection1 connection2 (可能来自不同的数据库)…
- tx.commit();//tx.rollback();
6 隔离级别多线程并发读取数据时的正确性
六 jdbc的本质
七 mysql事务
1 事务的业务与需求
正是因为做某件事的时候,需要多条DML语句共同联合起来才能完成,所以需要事务的存在。如果任何一件复杂的事儿都能一条DML语句搞定,那么事务则没有存在的价值了。
2 思想思路
-
一个事务其实就是一个完整的业务逻辑。
3 流原:事务是怎么做到多条DML语句同时成功和同时失败的呢?
- 事务开启:Start Transaction
- InnoDB存储引擎:提供一组用来记录事务性活动的日志文件。
- 在事务的执行过程中,每一条DML的操作都会记录到“事务性活动的日志文件”中。
- 提交事务:commit
- 清空事务性活动的日志文件,将数据全部彻底持久化到数据库表中。
- 提交事务标志着,事务的结束。并且是一种全部成功的结束。
- 回滚事务?(roolbak:回滚永远都是只能回滚到上一次的提交点!)
- 清空事务性活动的日志文件,并且将之前所有的DML操作全部撤销
- 回滚事务标志着,事务的结束。并且是一种全部失败的结束。
4 mysql事务自动提交
- mysql默认情况下是支持自动提交事务的(自动提交)。
- 什么是自动提交?每执行一条DML语句,则提交一次!
- 这种自动提交实际上是不符合我们的开发习惯,因为一个业务通常是需要多条DML语句共同执行才能完成的,为了保证数据的安全,必须要求同时成功之后再提交,所以不能执行一条就提交一条。
- 自动提交与非自动提交的特点
-
启用自动提交模式:
-
如果自动提交模式被启用,则单条 DML 语句将缺省地开始一个新的事务。
-
如果该语句执行成功,事务将自动提交,并永久地保存该语句的执行结果。
-
如果语句执行失败,事务将自动回滚,并取消该语句的结果。
-
在自动提交模式下,仍可使用 START TRANSACTION 语句来显式地启动事务。这时,一个事务仍可包含 多条语句,直到这些语句被统一提交或回滚。
-
-
禁用自动提交模式:
-
如果禁用自动提交,事务可以跨越多条语句。
-
在这种情况下,事务可以用 COMMIT 和 ROLLBACK 语句来显式地提交或回滚。
-
-
- 查看mysql自动提交事务模式
-
mysql> SHOW VARIABLES LIKE 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | +---------------+-------+ 1 row in set, 1 warning (0.04 sec) 结果显示,autocommit 的值是 ON,表示系统开启自动提交模式。
-
- 开启或关闭mysql自动提交事务功能
- 在 MySQL 中,可以使用 SET autocommit 语句设置事务的自动提交模式,语法格式如下:
SET autocommit = 0|1|ON|OFF;
对取值的说明: - 值为 0 和值为 OFF:关闭事务自动提交。如果关闭自动提交,用户将会一直处于某个事务中,只有提交或回滚后才会结束当前事务,重新开始一个新事务。
- 值为 1 和值为 ON:开启事务自动提交。如果开启自动提交,则每执行一条 SQL 语句,事务都会提交一次。
- 在 MySQL 中,可以使用 SET autocommit 语句设置事务的自动提交模式,语法格式如下:
5 事务的4大特性
事务包括4个特性:ACID
- 原子性:不可再分。
- 一致性:在同一个事务当中,所有操作必须同时成功,或者同时失败,以保证前后数据的一致性。
- 隔离性:
教室A和教室B之间有一道墙,这道墙就是隔离性。
两个事务同时去操作一张表:A事务在操作一张表的时候,另一个事务B也操作这张表会那样???这就是隔离性。
相当于多线程并发访问同一张表一样,此时就会有多线程带来的线程安全问题。为了解决事务之间的线程安全问题,就要使用事务的隔离性。 - 持久性:将数据持久化到硬盘上的数据库中。
6 重点研究一下事务的隔离性!!!
-
事务的隔离级别:A教室和B教室中间有一道墙,这道墙可以很厚,也可以很薄。这道墙越厚,表示隔离级别就越高。
-
分类:
-
读未提交(最低级别):没有提交就读到了
-
原因:事务A可以读取到事务B未提交的数据。
-
问题:脏数据,脏读现象
-
-
读已提交:提交之后才能读到。数据绝对真实,oracle默认。
-
原因:事务A只能读取到事务B提交之后的数据。
-
解决:读未提交
-
问题:不可重复读
什么是不可重复读取数据呢?在事务开启之后,第一次读到的数据是3条,当前事务还没有结束,可能第二次再读取的时候,读到的数据是4条,3不等于4称为不可重复读取。
-
-
可重复读:即使提交之后也读不到,永远读取的都是事务A刚开启事务时的数据。数据不够真实,出现幻影读。mysql默认。
-
原因:事务A开启之后,不管是多久,每一次在事务A中读取到的数据都是一致的。即使事务B将数据已经修改,并且提交了,事务A读取到的数据还是没有发生改变,这就是可重复读。
-
解决:不可重复读
-
问题:可以会出现幻影读。每一次读取到的数据都是幻象。不够真实!
-
案例:以下需求应该使用什么样的隔离级别?可重复读即可。
下午1点开始开启了事务,只要事务不结束,到下午3点,读到的数据还是那样!
原理:就是做了数据的一个备份,或者说对数据进行了一个快照。
-
-
序列化/串行化(最高级别):
-
原因:事务排队,需要等待其他事务的提交,不能并发!有类似于java中的synchronized,线程同步(事务同步)。
-
解决:解决了以上所有问题(读未提交、读已提交、可重复读)
-
问题:每一次读取到的数据都是最真实的,并且效率是最低的。
-
-
7 事务的提交与回滚演示
- 第一步:创建表
- 第二步:查询表中数据
- 第三步:开启事务
- 第四步:插入数据
- 第五步:查看数据
- 第六步:修改数据
- 第七步:查看数据
- 第八步:回滚事务
- 第九步:查看数据
8 事务隔离级别设置
高性能MySQL学习总结及实验验证:
数据库隔离级别有四种,应用《高性能mysql》一书中的说明:
然后说说修改事务隔离级别的方法:
- 全局修改,修改mysql.ini配置文件,在最后加上:
- #可选参数有:READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE.
- [mysqld]
- transaction-isolation = REPEATABLE-READ
- 说明:这里全局默认是REPEATABLE-READ,其实MySQL本来默认也是这个级别
- 对当前session修改,在登录mysql客户端后,执行命令:
- set session transaction isolation level read uncommitted;
八 oracle事务
1oracle数据库的事务由下列语句组成:
- 一组DML语句,修改的数据在他们中保持一致
- 一个DDL (Data Define Language) 语句
- 一个DCL (Data Control Language)语句
2 事务的开始与结束
- 开始于:第一个执行的DML语句
- 结束于:
- commit 或 rollback
- DDL or DCL语句(隐式的提交事务)
- 用户连接异常,或用户断开连接(隐式的回滚)
- 系统崩溃(隐式的回滚)
3 事务的自动提交设置
因为 oracle 的这种机制,所以有的工具增加了进行自动提交的设置,就是对于需要显示提交的,工具检测出来后,自动的给加上 commit。看着的效果就是不需要执行 commit 就能生效,其实是后台在后面自动给你执行了 commit。
Oracle SQL Developer启用(关闭)自动提交事务,设置自动commit
若把AUTOCOMMIT设置为ON,则在插入、修改、删除语句执行后,系统将自动进行提交,这就是自动提交。其格式为:SQL>SET AUTOCOMMIT ON;