本地事务
Spring中的@Transactional注解
@Transactional
是用于处理本地事务与之相对的,跨越多个服务、多个数据库、多张数据表的则被称作是分布式事务。
事务传播行为
propagation
【Spring学习34】Spring事务(4):事务属性之7种传播行为
事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。
例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。演示
- @Test测试的时候会对事务自动进行回滚
想要不会滚设置
@Rollback(value = false)
- @Test测试的时候会对事务自动进行回滚
@Transactional
注解失效的场景注解的方法必须为public
类
TransactionalRepositoryProxyPostProcessor
中的方法computeTransactionAttribute
首先判断方法是不是public的propagation配置错误
rollBackFor属性设置错误:默认只对RuntimeException或者Error进行回滚
数据库中的记录并未进行回滚。由于IOException是Exception类型的
配置ROLLBACK类型
同一个类中方法调用,导致事务失效
由于事务方法是代理对象进行调用的,而同类中方法的调用是this,导致事务失效
解决方法:使用当前类的代理对象调用
需要引入aop依赖
主动catch导致事务失效
- 数据库引擎MyISAM不支持事务
分布式一致性
- 从分布式一致性谈到CAP理论、BASE理论
- 一致性级别:
1、强一致性
这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大
2、弱一致性
这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不久承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态
3、最终一致性
最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型
CAP定理
- 简单理解就是说:无法设计出一个系统在存在网络分区的时候,既能保证系统数据一致性又能保证系统的可用性
- An Illustrated Proof of the CAP Theorem
- CAP理论中的P到底是个什么意思?
[
](https://www.zhihu.com/question/54105974/answer/139037688)
CAP 定理(也以计算机科学家Eric Brewer的名字命名为Brewer 定理)指出,任何分布式数据存储只能提供以下三个保证中的两个:
一致性Consistency:每次读取都会收到最近的写入或错误。
可用性Availability:每个请求都会收到一个(非错误)响应,但不能保证它包含最近的写入。
- 分区容错Partition tolerance:尽管整个系统出现了网络分区,系统还是可以继续运行的
当发生网络分区故障时,必须决定是否
- 取消操作并因此降低可用性但确保一致性或
- 继续操作,从而提供可用性,但存在不一致的风险。
因此,如果存在网络分区,则必须在一致性和可用性之间进行选择。请注意,CAP 定理中定义的一致性与ACID数据库事务中保证的一致性完全不同。
- CAP 经常被误解为在任何时候都可以选择放弃三个保证中的哪一个。事实上,只有在发生网络分区或故障时,才会在一致性和可用性之间进行选择。在没有网络故障的情况下,可以同时满足可用性和一致性。
- 实际上分区在分布式系统中,并不受控。所以一般认为P是必须提供的,所以AC需要权衡。
- 为什么在网络故障时,只能从一致性和可用性中选择其中的一个?(反证法)
Raft算法
- 一文搞懂Raft算法
- Raft算法论文
- Raft算法动态演示过程
- Raft 为什么是更易理解的分布式一致性算法
- Raft
[ræft]
算法一个共识算法(consensus algorithm),所谓共识,就是多个节点对某个事情达成一致的看法,即使是在部分节点故障、网络延时、网络分割的情况下。在分布式系统中,共识算法更多用于提高系统的容错性 - Raft算法就是一种leader-based的共识算法,与之相应的是leaderless的共识算法。
正常过程
- 首先,Raft算法中,分布式系统中的每一个节点有3种状态:Follower,Leader,Candidate
- 过程如下:
- 初始化的时候,每一个节点都是Follower状态
- 如果Followers无法获取Leader的心跳,那么Follower成为Candidate
- Candidate向Followers发送投票的请求,Followers回复投票请求
- 如果Candidate获得大多数的投票就成为Leader。以上过程称为选举(Leader Election)
- 所有的客户端请求都直接与Leader进行交互,每一个客户端的命令都会作为一个Entry加入到Leader节点的日志中。此时Log的Entry状态是uncommitted,因此不会更新各个节点的值
- 为了更新各个节点的值,Leader节点会发请求将Entry复制给各个Follower节点
- Leader节点收到大多数Follower节点已经将Entry写入各自log的响应之后,将Leader节点的值更新为客户端设置的值,此时Leader节点值的状态是committed
- Leader通知各个Follower:Entry已经committed;然后各节点进行数据更新。此过程称为Log Replication
- 至此,所有的节点的数据完成了一致性保证
Leader Election
- 两个超时timeout:一个是election timeout,另一个是heartbeat timeout
- election timeout:Follower无法获取到Leader的心跳直至成为Candidate的时间,大小是随机的,范围是150~300ms,一定程度避免了多个Follower同时成为Candidate
- heartbeat timeout:Leader给Follower发送Append Entries messages的间隔
- 以下过程是只有一个Candidate的过程
- 当Follower成为Candidate后,开启一个新的选举任期(election term)
- 首先,先投给自己一票
- 然后给其他节点发送Request Vote messages,如果接收到消息的节点在此任期(term)内没有投过票,此时会将票投给Candidate。然后重新设置election timeout时间
- 一旦Candidate获取大多数投票就成为Leader,其他节点成为Follower
- Leader发送Append Entries messages给Followers,Append Entries messages会每隔一定时间(heartbeat timeout)发送给Followers
- Followers回复每一个Append Entries messages
- 当前选举任期会一直持续到某个Follower无法在heartbeat timeout时间内接收到Leader的心跳而成为新的Candidate,就会重复以上过程
- Requiring a majority of votes guarantees that only one leader can be elected per term:需要大多数投票保证了每一个选举任期内只有一个Leader
- 如果多个节点同时成为Candidate,可能会造成平票(split vote)。如果产生了平票
- 那么会等到election timeout(随机150~300ms间)结束后重新开始新的选举任期(election term)
- 直到某一个节点成为Leader
- Candidate想要获取到Follower的票,必须保证Candidate比Follower知道的多
- Leader如果发现其他节点比自己更新,则主动切换到Follower。
- 如果Candidate知道的内容一样多,按照先来先得的方式获取Follower的选票
Log Replication
- 一旦有了Leader,我们需要将所有的变更复制给其他节点,此过程是通过Leader定期发送心跳信息同时携带客户端请求的指令按照执行顺序发给Follower实现的
- 共识算法的实现一般是基于复制状态机(Replicated state machines),何为复制状态机:
If two identical, deterministic processes begin in the same state and get the same inputs in the same order, they will produce the same output and end in the same state.
简单来说:相同的初识状态 + 相同的输入 = 相同的结束状态。引文中有一个很重要的词deterministic
,就是说不同节点要以相同且确定性的函数来处理输入,而不要引入一下不确定的值,比如本地时间等。如何保证所有节点 get the same inputs in the same order
,使用replicated log是一个很不错的注意,log具有持久化、保序的特点,是大多数分布式系统的基石。
因此,可以这么说,在raft中,leader将客户端请求(command)封装到一个个log entry,将这些log entries复制(replicate)到所有follower节点,然后大家按相同顺序应用(apply)log entry中的command,则状态肯定是一致的。
- 正常过程如下:
- 首先,Client发送一个变更操作给Leader,这个变更命令entry追加进了Leader的log中,此时在Leader中,entry的状态是uncommitted
- 然后这些变更Entries会在下次发送心跳信息的时候携带给Follower
- 当Leader接收到大多数Follower的响应信息之后,entry的状态变为committed,同时执行Entry中的命令,然后将执行结果响应给Client
- 然后Leader通知各个Follower根据log进行数据的更新
当有网络分区的时候,Raft仍然可以保证数据的一致性
有网络分区(分区间的节点无法通信)的时候,每一个分区都会产生一个Leader
并且显而易见的是两个分区的term并不相同,因为没有Leader的一个分区重新选举,term在之前的基础上改变
假设Client1发送一个请求给分区A(假设是2个节点的分区),由于需要大多数Follower节点的响应(此时只有一个Follower),Leader才能将Entry的状态从uncommitted改为committed。所以Client1并不会接收到Leader的响应信息,分区A的数据并不会进行修改
- 而另一个客户端Client2给分区B(假设是3个节点的分区),Leader将Entry发送给各个Follower,然后Follower发送响应信息,可以获取大多数Follower的响应。所以此时分区B的Entry状态从uncomitted变为committed,同时执行了命令。并将执行结果响应给客户端
- 当网络分区消除后,分区A(2节点)中节点(包含Leader和Follower)将会回滚各自的uncommitted状态的entry,并且同步新的Leader的log数据。因为Leader收到了更高term的消息,分区A的Leader就会转为Follower
BASE理论
BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结, 是基于CAP定理逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。接下来看一下BASE中的三要素:
1、基本可用
基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性——注意,这绝不等价于系统不可用。比如:
(1)响应时间上的损失。正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障,查询结果的响应时间增加了1~2秒
(2)系统功能上的损失:正常情况下,在一个电子商务网站上进行购物的时候,消费者几乎能够顺利完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面
2、软状态
软状态指允许系统中的数据存在中间状态(成功和失败之间的状态),并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时
3、最终一致性
最终一致性强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
总的来说,BASE理论面向的是大型高可用可扩展的分布式系统,和传统的事物ACID特性是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。但同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此在具体的分布式系统架构设计过程中,ACID特性和BASE理论往往又会结合在一起。
分布式事务问题
- 演变
- 最初,我们单体应用,一般都是单个应用程序和单个数据库在同一个机器上
- 再者,数据变大之后进行分库分表。一般是一个应用程序对应多个数据库在同一个机器上
- 分布式事务,分布式架构中,可能一个微服务对应一个数据库,甚至这些数据库可能不在同一个机器上。比如一个订单包含三个微服务:下单,扣款减库存。这三个微服务和对应的数据库在三台机器上,同时这三个步骤需要作为一个事务,这种涉及到多数据源的统一调度就是分布式事务
分布式事务:一次业务操作需要跨多个数据源或者需要跨多个系统进行远程调用,就会产生分布式事务问题
此时每一个服务内部的数据一致性由本地事务进行保证
但是全局的数据一致性无法保证,需要使用分布式事务解决
常见分布式事务解决方案
两阶段提交和三阶段提交
- Two/Three Phase Commit:两阶段提交/三阶段提交,也简写为2PC和3PC
两阶段指的是:
- 准备阶段:事务协调者给事务参与者发起询问请求让事务参与者准备资源,事务参与者回复yes(表示已经准备好资源)或者no(表示无法获取本地资源)
- 提交(或回滚)阶段:
- 如果各个参与者都回复yes,那么事务协调者向所有事务参与者发起事务提交操作。事务参与者执行本地事务提交并向协调者发送ACK
- 如果存在参与者回复no或者未收到参与者的回复,则协调者向所有参与者发起事务回滚操作,然后所有参与者收到后各自执行本地事务回滚操作并向协调者发送 ACK。
两阶段存在的问题:
- 准备阶段需要等待所有的参与者返回,这个阶段资源是被锁住的
- 提交阶段发出指令前,协调者宕机。所有的参与者都收不到提交或回滚指令,导致所有参与者“不知所措”;
- 在提交阶段,协调者向所有的参与者发送了提交指令,如果一个参与者未返回 ACK,那么协调者不知道这个参与者内部发生了什么(由于网络二将军问题的存在,这个参与者可能根本没收到提交指令,一直处于等待接收提交指令的状态;也可能收到了,并成功执行了本地提交,但返回的 ACK 由于网络故障未送到协调者上),也就无法决定下一步是否进行全体参与者的回滚。
- 三阶段:询问、准备、提交(回滚)阶段。3PC 利用超时机制解决了 2PC 的同步阻塞问题,避免资源被永久锁定,进一步加强了整个事务过程的可靠性。但是 3PC 同样无法应对类似的宕机问题,只不过出现多数据源中数据不一致问题的概率更小。
TCC方案
TCC 是 Try、Confirm、Cancel 三个词的缩写,其本质是一个应用层面上的 2PC,同样分为两个阶段:
- 准备阶段。协调者调用所有的每个微服务提供的 try 接口,将整个全局事务涉及到的资源锁定住,若锁定成功 try 接口向协调者返回 yes。
提交阶段。若所有的服务的 try 接口在阶段一都返回 yes,则进入提交阶段,协调者调用所有服务的 confirm 接口,各个服务进行事务提交。如果有任何一个服务的 try 接口在阶段一返回 no 或者超时,则协调者调用所有服务的 cancel 接口。
如何解决confirm或者cancel失败的问题?
- 通过不断重试来解决。因为try阶段已经锁住了所需要的资源,所以confirm或者cancel阶段可以一直重试
- 重试过程,也即业务的confirm或者cancel需要保证幂等性
可靠消息服务
- 基于最终一致性的思想,一些分布式事务的解决方案通过引进消息服务来进行完成。相关的实现方案版本较多,这里根据补偿职责归属于消息生产者或者消费者进行划分,分别将其称为可靠消息和最大努力送达两种实现模式。
- 可靠消息:一般分为事务的发起者A和事务的其它参与者B:
- 事务发起者A执行本地事务
- 事务发起者A通过MQ将需要执行的事务信息发送给事务参与者B
- 事务参与者B接收到消息后执行本地事务
这里有一个假设,就是消息服务必须是可靠的。也即发起者A本地事务成功后,消息一定可以发送成功。同时消息可以正确投递给消费者并且可以持久化保存
事务参与者B必须确保消息最终一定可以消费,如果失败需要多次重试
事务B执行失败会重试,但是不会导致事务A回滚
- 最大努力送达:不同于可靠消息模式,最大努力送达模式下,由业务主动方进行一定次数的尝试(最大努力),最终一致性保证的职责由业务被动方负责。这种模式下,业务被动方的改造成本更高。
Seata概述
- 是什么
- Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
Seata术语
Seata分TC、TM和RM三个角色,TC(Server端)为单独服务端部署,TM和RM(Client端)由业务系统集成。
Seata分布式事务处理过程
- 分布式事务的全局唯一ID(XID)+三组件模型(TC、TM、RM)
- 处理过程
如何使用
Seata-Server安装
- 下载,source是源码,binary是编译之后的文件。可以直接下载binary文件
本文以下使用的Seata版本为1.3.0,一定一定一定要读解压之后的README.md文件(里面会有说存储的SQL以及一些初始化配置)
对应的SpringCloud Alibaba版本我使用的是2.2.6.RELEASE
一定一定要参考新手文档
- 一定一定注意和SpringCloud Alibaba版本的搭配使用,否则会出现不支持的情况,版本搭配
解压
unzip xxx.zip
目录如下
修改
file.conf
中事务日志存储方式好习惯:修改之前先进行备份
通常采用存储到数据的方式
需要修改的地方如下,注意驱动名需要根据MySQL的版本选择
同时需要创建seata数据库以及对应的三张表
建表的SQL存放在Github上面,可以在README-zh.md文件中看到
注意选择对应的分支
全局事务会话信息由3块内容构成,全局事务—>分支事务—>全局锁,对应表global_table、branch_table、lock_table
创建seata数据库并执行对应的SQL
修改注册中心以及配置中心
reistry.conf
注册中心
配置中心
先启动Nacos,再启动Seata
Seata启动方式在bin目录下
查看Nacos中的Seata-server
到这里,已经可以在Nacos中看到Seata Server了
初始化Nacos配置项
配置内容首先查看README-zh.md文件,config-center
脚本的README.md
需要配置的内容如下
下载config-center文件夹,查看nacos-config.sh启动文件,可以看到按照他的目录不用改变即可
修改配置文件,主要是两部分:事务分组以及存储方式
启动脚本,将config.txt中的配置项注册到Nacos配置中心
重启Seata Server并查看配置中心,配置项已经配置成功,后续可以动态修改
当然,服务也注册成功
快速开始
- 从一个微服务示例开始
这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单(Business)时,会在订单服务(Order)中创建一个订单,然后通过远程调用库存服务(Storage)来扣减下单商品的库存,
再通过远程调用账户服务(Account)来扣减用户账户里面的余额,
最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
创建数据库和表
创建三个数据库
创建业务表
在订单数据库下建立订单表t_order
在库存数据库创建一张表t_storage并插入一条记录
在账户数据库创建一个账户表t_account并新建一个账户
最终创建的数据库和表如下
CREATE TABLE t_order (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`count` INT(11) DEFAULT NULL COMMENT '数量',
`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
`status` INT(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结'
) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
SELECT * FROM t_order;
CREATE TABLE t_storage (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`used` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '100', '0', '100');
SELECT * FROM t_storage;
CREATE TABLE t_account (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额',
`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');
SELECT * FROM t_account;
- 创建回滚日志表(undo-log)
对应的SQL存储于Github上,undo-log输入Seata Client端
注意,在每个需要开启seata事务操作的数据库下都需要建立此表。
最终创建的数据库以及对应的表如下
业务模块
- 业务需求:下订单—>改库存—>扣余额—>修改订单状态
- 配置部分可以参考官网快速开始
订单模块
创建Module
修改pom文件
注意这里的Seata版本与安装的版本最好一致
spring-cloud-starter-alibaba-seata内嵌的seata版本与我们安装的版本可能并不相同
再次提醒一定要按照官方推荐的版本搭配的来,否则可能会有不适配的问题
在父项目的pom文件中,使用的SpringCloud Alibaba版本
<dependencies>
<!--nacos 服务发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- nacos 配置中心依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--web-actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--mysql-druid-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<!--spring cloud alibaba 2.2.6.RELEASE-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
配置文件修改
bootstrap.yml配置文件
application.yaml配置
主启动类
业务类
首先将数据库对应的实体类以及Mapper创建好
然后就是主要的订单业务类,其中需要调用库存和账户对应的Feign接口
Storage和Account的Feign接口
库存模块
和订单模块相似,实现订单模块调用的Feign接口即可
账户模块
和订单模块相似,实现订单模块调用的Feign接口即可
测试
首先启动三个服务,这里需要说明一下,如果是服务器的话,记得开启Seata默认端口8091,否则会一直报错
还有一点就是Feign调用默认时长是1s,否则就返回TimeOut异常,需要在配置文件中设置超时时长
#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000
流程出现异常
未使用分布式事务
假如减库存和减账户之间出现了异常,而没有分布式事务处理
就会导致库存减少了,但是未成功扣款,当然订单状态也是未完成
他们应该作为一个整体进行处理。
使用Seata的分布式事务
给业务方法上添加注解
再通过浏览器测试
日志已经打印出来库存减少了
看数据库
至此就实现了通过添加一个注解,解决了分布式事务的问题
Seata AT模式
- TC、TM、RM
步骤:
- TM向TC注册全局事务记录
- RM向TC汇报资源准备状态
- TM通知TC提交或者回滚分布式事务(事务一阶段结束)
- TC汇总事务信息,决定分布式事务是提交还是回滚
- TC通知所有RM提交或者回滚资源(事务二阶段结束)
具体流程演示
一阶段
Debug到异常之前
- 解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where)等相关的信息。
- 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
- 执行业务 SQL
- 查询后镜像:根据前镜像的结果,通过 主键 定位数据。
- 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到
UNDO_LOG
表中。 - 提交前,向 TC 注册分支:申请
product
表中,主键值等于 1 的记录的 全局锁 。 - 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
- 将本地事务提交的结果上报给 TC。
- 二阶段
- 提交
- 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
- 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录
- 回滚
- 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
- 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
- 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
- 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
- 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
- 提交
Debug演示(以异常回滚为例)
首先拦截执行的方法,判断是否有注解
判断有注解后面进入到
TransactionalTemplate
的execute()
中,这里是TM的核心步骤开启全局事务
进入到方法中,有一个
triggerBeforeBegin()
和后面的triggerAfterBegin()
进去的话没什么流程,暂且不谈进入到begin方法中,此方法所在的类是
DefaultGlobalTransaction
,这个类有很多重要信息接着看begin方法是如何执行的
进入到的类
DefaultTransactionManager
这个类也很重要,主要完成的是和Seata Server进行通信(使用Netty)begin
方法和Seata Server端完成通信之后,获取到全局唯一的XID此时seata数据库的全局事务表中已经有记录了
执行业务方法
最终通过反射调用的方式进入到业务方法中
查看此时的seata_order数据库
查看undo_log的rollback_info,用于后续回滚操作使用
后面减库存也一样
查看此时的分支事务表
异常处理
后面抛出异常,被catch到
进行事务回滚
和之前begin一样的思路
和Seata Server进行通信,进行回滚操作(TC进行)
后续客户端就是将异常往外抛出,交给MVC进行处理
而Server端的TC:
TC异步删除global_table、branch_table,RM端回滚数据并删除undo_log
可见,数据库中已执行的业务SQL以及undo_log还有global_table、branch_table中的数据全都被删除了
-
Others