高级JAVA开发 分布式事务部分

高级JAVA开发 分布式事务部分

  • 本地事务
    • ACID原则
      • 隔离性:Mysql的4个事务隔离级别
  • 分布式事务
    • 分布式系统中的理论
      • CAP 原则(布鲁尔定理)
      • BASE 理论
        • 什么是BASE理论
    • 分布式事务中的理论
      • XA规范
      • 2PC(tow phase commit) 两阶段提交
      • TCC(Try-Confirm-Cancel)补偿事务
      • 3PC(tow phase commit) 三阶段提交
    • 分布式事务的实现方案
      • 基于 JTA 实现的分布式事务
      • Seata 提供的分布式事务解决方案
        • 事务模型
        • AT (Automatic Transaction)模式
          • AT 模式 的前提:
          • 整体机制
          • 隔离性
            • 写隔离
            • 读隔离
        • TCC 模式
          • TCC设计举例
          • 使用TCC模式需要注意的问题
            • 允许空回滚
            • 防悬挂控制
            • 幂等控制
        • Saga 模式
        • XA 模式
      • 基于 MQ 实现的『最终一致性』分布式事务解决方案
        • 基于『本地消息表』和 MQ 实现的『最终一致性』分布式事务解决方案
        • 基于『可靠消息队列(RocketMQ)』实现的『最终一致性』分布式事务解决方案

参考和摘自:
分布式事务的4种模式

本地事务

ACID原则

A:原子性(Atomicity):
	一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。
	事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

C:一致性(Consistency):
	事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。
		如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。
		如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。

I:隔离性(Isolation):
	指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。
	由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。
	事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。

D:持久性(Durability):
	指的是只要事务成功结束,它对数据库所做的更新就必须保存下来。

隔离性:Mysql的4个事务隔离级别

1、读未提交(Read Uncommited)(可能产生脏读,幻读,不可重复读):
	该隔离级别允许脏读取,其隔离级别最低;
	比如:
		事务A和事务B同时进行,事务A在整个执行阶段,会将某数据的值从1开始一直加到10,然后进行事务提交,
		此时,事务B能够看到这个数据项在事务A操作过程中的所有中间值(如1变成2,2变成3等)

2、读已提交(Read Commited)(可能产生不可重复读,幻读):
	读已提交只允许获取已经提交的数据。
	比如:
		事务A和事务B同时进行,事务A进行+1操作。
		此时,事务B无法看到这个数据项在事务A操作过程中的所有中间值,只能看到最终的10。
		但是事务B可能出现第一读取到1,第二次读取到事务A提交的数据10,造成不可重复读。
	不可重复读指的是:同事务内,同一条数据两次读取到不同值的情况。
	幻读指的是:同事务内,同一查询条件两次读取到不同条数的情况。

3、可重复读(Repeatable Read)(可能产生幻读):
	就是保证在事务处理过程中,多次读取同一个数据时,其值都和事务开始时刻是一致的,
	因此该事务级别禁止不可重复读取和脏读取,但是有可能出现幻影数据。

4、串行化:
	是最严格的事务隔离级别,它要求所有事务被串行执行,即事务只能一个接一个的进行处理,不能并发执行。

Mysql InnoDB引擎 RR隔离级别下产生幻读例子

数据准备:
	DROP TABLE IF EXISTS `test`;
	CREATE TABLE `test` (
	  `id` int(1) NOT NULL AUTO_INCREMENT,
	  `name` varchar(8) DEFAULT NULL,
	  PRIMARY KEY (`id`)
	) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8;
	BEGIN;
		INSERT INTO `test` VALUES
			 ('0', '小罗'), ('5', '小黄'), ('10', '小明'), ('15', '小红'), ('20', '小紫'), ('25', '小黑');
	COMMIT;

操作例子:
---- | -------------------------------------------------- | -----------------------------------------------------
Time |                   Session1 TX1                     |             Session2 TX2 
---- | -------------------------------------------------- | -----------------------------------------------------
     | BEGIN;                                             |
  t1 | -- return row num 0	                              |
	 | SELECT * FROM `test` WHERE `id`> 10 AND `id` < 15; |
---- | -------------------------------------------------- | -----------------------------------------------------
	 |                                                    | BEGIN;
  t2 |                                                    | INSERT INTO `test` (`id`, `name`) VALUES (12, '李西');
	 |                                                    | COMMIT;
---- | -------------------------------------------------- | -----------------------------------------------------
     | -- return row num 0	未幻读                         | 
     | SELECT * FROM `test` WHERE `id`> 10 AND `id` < 15; |   
	 |                                                    | 
     | --  update TX2 提交的数据                            | 
  t3 | update `test` set `name` = 'aaa' where id = 12 ;   |
 	 |                                                    | 
     | -- return row num 1	产生幻读                       | 
     | SELECT * FROM `test` WHERE `id`> 10 AND `id` < 15; |
     | COMMIT;                                            |
---- | -------------------------------------------------- | -----------------------------------------------------

Tips:
	Mysql RR级别下有MVCC机制,解决了部分幻读问题(第二次读取)。
	TX1的更新操作是多步的:读取id为12的数据行,更新数据,写入数据行。
	更新操作读取时的操作是『当前读』,也就是读取到了TX2提交的最新数据,在此基础上做更新操作。
	TX1第三次读取时,可以读取到本事务做的更新操作,这时也就产生了幻读。

	如果TX1在第一次读取时改成这样:
		SELECT * FROM `test` WHERE `id`> 10 AND `id` < 15 FOR UPDATE;
	TX2在INSERT操作时会阻塞住,直到TX1 COMMIT,此时TX1 select 就不会产生幻读问题。

分布式事务

分布式系统中的理论

CAP 原则(布鲁尔定理)

在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

一致性(C):
	在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)

可用性(A):
	在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)

分区容错性(P):
	分区容错性是指系统能够容忍节点之间的网络通信的故障。
	以实际效果而言,分区相当于对通信的时限要求。
	系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。

CAP 在分布式系统中的权衡:

CA:
	在分布式系统中,如果要求所有节点数据的一致性(C)又要求所有节点的可用性(A),
	那么在发生分区现象时(分区无法避免,因为存在多个节点)为了保证一致性(C),
	分布式系统整体只能拒绝所有请求停止服务,等待节点分区问题解决后再继续提供服务。
	此时已经违背了可用性(A)。
	所以分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。

CP:
	放弃可用性(A),追求一致性和分区容忍性。在产生分区问题时,放弃可用性。ZooKeeper 其实就是追求的强一致。

AP:
	放弃一致性(C)(这里说的一致性是强一致性) 追求分区容错性和可用性,这是很多分布式系统设计时的选择,比如 Eueaka。

CAP 理论中是忽略网络延迟的,也就是当事务提交时,从节点 A 到节点 B 没有延迟,但是在现实中这个是明显不可能的,所以总会有一定的时间是不一致。

BASE 理论

什么是BASE理论

是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。
是对 CAP 中 AP 的一个扩展,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

基本可用:
	分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。

软状态:
	允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是 CAP 中的不一致。

最终一致:
	最终一致是指经过一段时间后,所有节点数据都将会达到一致。

BASE 解决了 CAP 理论中没有网络延迟的情况,在 BASE 中用软状态和最终一致,保证了延迟后的一致性。
BASE 和 ACID 是相反的,它完全不同于 ACID 的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。

分布式事务中的理论

XA规范

X/Open组织 定义的一套DTP分布式事务的模型和规范。
XA DTP分布式事务模型中的四个角色:

AP(Application Program,应用程序)
TM(Transaction Manager,事务管理器)
RM(Resource Manager,资源管理器)通常指数据库
CRM(Communication Resource Manager,通信资源管理器)

XA是DTP模型定义TM和RM之前通讯的接口规范。XA接口函数由数据库厂商提供。TM中间件用它来通知数据库事务的开始、结束以及提交、回滚等。

2PC(tow phase commit) 两阶段提交

根据XA思想衍生出来的一致性协议。

参与2PC的角色:

协调者(coordinator)
参与者(participants, 或cohort)

保证事务在提交时,协调者和参与者处于一致性状态,如果其中有一个参与者出现了故障或者网络问题,不能及时的回应协调者,那么这次事务就宣告失败或者出现阻塞。

两阶段:

1、准备阶段:
	事务协调者(事务管理器 TM)给每个参与者(资源管理器 RM)发送Prepare消息,
	每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交
	
2、提交阶段
	如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;
	否则,发送提交(Commit)消息;
	参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。
	(注意:必须在最后阶段释放锁资源)

2PC的缺点:
二阶段提交看起来确实能够提供原子性的操作,但是不幸的是,二阶段提交还是有几个缺点的:

1、同步阻塞问题。
	在事务执行过程中,所有参与节点都是事务阻塞型的。
	参与者占有公共资源时,其他第三方节点访问公共资源则会处于阻塞。

2、单点故障。
	在2PC中由协调者进行协调,一旦协调者发生故障,参与者会阻塞。
	尤其在第二阶段commit阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。
	注:如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题

3、数据不一致。
	在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常;
	或者在发送commit请求过程中协调者发生了故障,导致只有一部分参与者接受到了commit请求。
	而在这部分参与者接到commit请求之后就会执行commit操作。
	但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。

4、二阶段无法解决的问题:
	协调者发出commit消息,并且只有部分参与者收到消息,此时协调者和收到消息的参与者发生宕机。
	那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,集群中不能判断出事务是否被已经提交。

TCC(Try-Confirm-Cancel)补偿事务

TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:

Try 阶段:
	主要是对业务系统做检测及资源预留。

Confirm 阶段:
	主要是对业务系统做确认提交。
	Try 阶段执行成功并开始执行 Confirm 阶段时,默认 Confirm 阶段是不会出错的。
	即:只要 Try 成功,Confirm 一定成功。

Cancel 阶段:
	主要是在业务执行错误需要回滚的状态下执行的业务取消,预留资源释放。

举个栗子:

分布式系统中一个订单支付后会协调以下服务共同完成一系列操作:
	订单服务 - 修改订单状态为已支付
	库存服务 - 库存扣减
	积分服务 - 给用户增加积分
	仓储服务 - 创建销售出库单
如果其中一个环节操作失败,那么其余服务应该回滚到订单支付前的状态。

如果应用TCC解决方案,那么各个服务需要提供以下支持:
	订单服务 
		- 提供修改订单状态为 UPDATING 的接口		(Try)
		- 提供修改订单状态为 PAYED 的接口			(Confirm)
		- 提供修改订单状态为 UNPAID 的接口			(Cancel)
	库存服务 
		- 提供 冻结库存 接口						(Try)
		- 提供 冻结库存提交 接口					(Confirm)
		- 提供 冻结库存回滚 接口					(Cancel)
	积分服务 
		- 提供 增加积分 状态为 INEFFECTIVE 接口		(Try)
		- 提供 修改积分状态为 EFFECTIVE 接口		(Confirm)
		- 提供 删除积分记录 接口					(Cancel)
	仓储服务
		- 提供 创建出库单 状态为 INEFFECTIVE 接口	(Try)		
		- 提供 修改出库单状态为 EFFECTIVE 接口		(Confirm)	
		- 提供 删除出库单 接口						(Cancel)

第一阶段:执行所有 Try 接口 预留资源
第二阶段:如果有服务 Try 阶段预留资源失败,那么执行 Try 成功服务的 Cancel 接口,反之执行所有 Confirm 接口。

TCC 事务框架要记录一些分布式事务的活动日志,保存分布式事务运行的各个阶段和状态。
比如发现某个服务的 Cancel 或者 Confirm 一直没成功,会不停的重试调用它的 Cancel 或者 Confirm 逻辑,务必要它成功!

优势与劣势:

优势:
	- TCC 分布式事务的实现方式的在于,可以让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。
	
缺点:
	- 对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。
	- 现难度较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。
	  为了满足一致性的要求,confirm和cancel接口还必须实现幂等。

2PC 和 TCC 的异同点:

从模式上来将,TCC类似2PC。抽象成逻辑即:
	- 第一步试着操作数据,第二步则是在第一步基础上做确认动作或取消动作。

但两者从根本上来说是截然不同的:
	- 2PC更看重的是事务处理阶段、RM提供底层支持(一般是兼容XA)、Prepare、Commit和Rollback。
	  一个完整的事务生命周期是:begin -> 业务逻辑 -> 逐个RM Prepare -> Commit/Rollback。
	- TCC则更看重在业务层面的分步处理。													
	  一个完整的事务生命周期是:begin -> 业务逻辑(逐个try业务) -> Comfirm业务/Cancel业务。

3PC(tow phase commit) 三阶段提交

在两阶段提交的基础上增加了CanCommit阶段,并引入了超时机制。
一旦事务参与者迟迟没有收到协调者的Commit请求,就会自动进行本地commit,
这样相对有效地解决了协调者单点故障的问题。
但是性能问题和不一致问题仍然没有根本解决。

CanCommit阶段:
	事务的协调者向所有参与者询问“你们是否可以完成本次事务?”,
	如果参与者节点认为自身可以完成事务就返回“YES”,否则“NO”。
	而在实际的场景中参与者节点会对自身逻辑进行事务尝试,
	其实说白了就是检查下自身状态的健康性,看有没有能力进行事务操作。

PreCommit阶段:
	在阶段一中,如果所有的参与者都返回Yes的话,那么就会进入PreCommit阶段进行事务预提交。
	此时分布式事务协调者会向所有的参与者节点发送PreCommit请求。
	参与者收到后开始执行事务操作,并将Undo和Redo信息记录到事务日志中。
	参与者执行完事务操作后(此时属于未提交事务的状态),就会向协调者反馈“Ack”表示我已经准备好提交了,并等待协调者的下一步指令。
	
	否则,如果阶段一中有任何一个参与者节点返回的结果是No响应,
	或者协调者在等待参与者节点反馈的过程中超时(2PC中只有协调者可以超时,参与者没有超时机制),
	整个分布式事务就会中断,协调者就会向所有的参与者发送“abort”请求。

DoCommit阶段:
	在阶段二中如果所有的参与者节点都可以进行PreCommit提交,那么协调者就会从“预提交状态”->“提交状态”。
	然后向所有的参与者节点发送"doCommit"请求。
	参与者节点在收到提交请求后就会各自执行事务提交操作,
	并向协调者节点反馈“Ack”消息,协调者收到所有参与者的Ack消息后完成事务。
	
	相反,如果有一个参与者节点未完成PreCommit的反馈或者反馈超时,
	那么协调者都会向所有的参与者节点发送abort请求,从而中断事务。

相比较2PC而言,3PC对于协调者(Coordinator)和参与者(Partcipant)都设置了超时时间,而2PC只有协调者才拥有超时机制。
这个优化点主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题。因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
另外,通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。

3PC依然没有完全解决数据不一致的问题。

分布式事务的实现方案

基于 JTA 实现的分布式事务

何为 JTA:

Java Transaction API,分布式事务的编程 API,基于 XA DTP 模型和规范。
在J2EE中,单库的事务是通过JDBC事务来支持的,如果是跨多个库的事务,是通过 JTA API 来支持的。
通过 JTA API 可以协调和管理横跨多个数据库的分布式事务。
狭义的说,JTA 只是一套接口。

开源的实现 JTA TM 的提供商:

Java Open Transaction Manager (JOTM)
JBoss TS
Bitronix Transaction Manager (BTM)
Atomikos
Narayana

JTA RM的提供商(XA接口):

一般数据库都会提供实现,比如 Mysql、Oracle

Mysql XA 事务支持官方文档:
Mysql XA Documentation
目前Mysql的XA模式仅限定在InnoDB引擎下。

处理流程:

- 事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交。
- 事务协调器要求每个数据库提交数据,或者回滚数据。

优缺点:

优点:
	尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于 MySQL 是从 5.5 开始支持。

缺点(同2PC缺点):
	同步阻塞:
		在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。
	
	单点问题:
		事务管理器在整个流程中扮演的角色很关键,如果其宕机。
		比如:
			在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,
			资源管理器就会一直阻塞,导致数据库无法使用。
	
	数据不一致:
		两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能。
		比如:
			在第二阶段中,假设协调者发出了事务 Commit 的通知,
			但是因为网络问题该通知仅被一部分参与者所收到并执行了 Commit 操作,
			其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。

总的来说,XA 协议比较简单,成本较低,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其最致命的弱点。

实现栗子移步文章:https://blog.csdn.net/zhouhao88410234/article/details/91872872

Seata 提供的分布式事务解决方案

转载和参考自:
http://seata.io/zh-cn/docs/overview/what-is-seata.html
https://juejin.im/post/5d54effe6fb9a06aeb10b646
https://www.jianshu.com/p/0ed828c2019a

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

Seata 中有三大模块,分别是 TM(Transaction Manager)、RM(Resource Manager) 和 TC(Transaction Coordinator):

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

TM - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。

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

其中 TM 和 RM 是作为 Seata 的客户端与业务系统集成在一起,TC 作为 Seata 的服务端独立部署。

在 Seata 中,分布式事务的执行流程:

1. TM 开启分布式事务(TM 向 TC 注册全局事务记录);

2. 按业务场景,编排数据库、服务等事务内资源(RM 向 TC 汇报资源准备状态);
 
3. TM 结束分布式事务,事务一阶段结束(TM 通知 TC 提交/回滚分布式事务);
 
4. TC 汇总事务信息,决定分布式事务是提交还是回滚;
 
5. TC 通知所有 RM 提交/回滚 资源,事务二阶段结束;

事务模型

高级JAVA开发 分布式事务部分_第1张图片

  • TM 定义全局事务的边界。
  • RM 负责定义分支事务的边界和行为。
  • TC 跟 TM 和 RM 交互(开启、提交、回滚全局事务;分支注册、状态上报和分支的提交、回滚),做全局的协调。

AT (Automatic Transaction)模式

AT 模式 的前提:
基于支持本地 ACID 事务的关系型数据库。
Java 应用,通过 JDBC 访问数据库。
整体机制

整体分为两阶段:执行阶段、完成阶段。是两阶段提交协议的演变:

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

一阶段(执行阶段)
高级JAVA开发 分布式事务部分_第2张图片
Seata 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用 本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务 中提交。
这样,可以保证:任何提交的业务数据的更新一定有相应的回滚日志存在。
基于这样的机制,分支的本地事务便可以在全局事务的 执行阶段 提交,马上释放本地事务锁定的资源。

二阶段(完成阶段)

  • 如果决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),完成阶段 可以非常快速地结束。
    高级JAVA开发 分布式事务部分_第3张图片

  • 如果决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。
    高级JAVA开发 分布式事务部分_第4张图片

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

以一个示例来说明:

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

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

高级JAVA开发 分布式事务部分_第5张图片
tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。

高级JAVA开发 分布式事务部分_第6张图片
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。

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

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

读隔离

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

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

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

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

全局锁是由 TC 也就是 server 来集中维护,而不是在数据库维护的。这样做有两点好处:

一方面:锁的释放非常快
	  尤其是在全局提交的情况下,收到全局提交的请求,锁马上就释放掉了,不需要与 RM 或数据库进行一轮交互。
另外一方面:因为锁不是数据库维护的,从数据库层面看,数据没有锁定。
      这也就是给极端情况下,业务 降级 提供了方便,事务协调器异常导致的一部分异常事务,不会 block 后面业务的继续进行。

TCC 模式

TCC模式需要对业务模型进行拆分,把原一次操作成功的业务拆分成两阶段实现。

TCC设计举例

以“扣钱”场景为例,在接入 TCC 前,对 A 账户的扣钱,只需一条更新账户余额的 SQL 便能完成;但是在接入 TCC 之后,用户就需要考虑如何将原来一步就能完成的扣钱操作,拆成两阶段,实现成三个方法,并且保证一阶段 Try 成功的话 二阶段 Confirm 一定能成功。

高级JAVA开发 分布式事务部分_第8张图片
如上图所示,Try 方法作为一阶段准备方法,需要做资源的检查和预留。在扣钱场景下,Try 要做的事情是就是检查账户余额是否充足,预留转账资金,预留的方式就是冻结 A 账户的 转账资金。Try 方法执行之后,账号 A 余额虽然还是 100,但是其中 30 元已经被冻结了,不能被其他事务使用。
二阶段 Confirm 方法执行真正的扣钱操作。Confirm 会使用 Try 阶段冻结的资金,执行账号扣款。Confirm 方法执行之后,账号 A 在一阶段中冻结的 30 元已经被扣除,账号 A 余额变成 70 元 。
如果二阶段是回滚的话,就需要在 Cancel 方法内释放一阶段 Try 冻结的 30 元,使账号 A 的回到初始状态,100 元全部可用。
用户接入 TCC 模式,最重要的事情就是考虑如何将业务模型拆成 2 阶段,实现成 TCC 的 3 个方法,并且保证 Try 成功 Confirm 一定能成功。相对于 AT 模式,TCC 模式对业务代码有一定的侵入性,但是 TCC 模式无 AT 模式的全局行锁,TCC 性能会比 AT 模式高很多。

使用TCC模式需要注意的问题
允许空回滚
场景:
	- Try 未执行,Cancel 执行了
出现原因:
	- Try 超时(丢包)
	- 分布式事务回滚触发 Cancel
	- 未收到 Try,收到 Cancel

Cancel 接口设计时需要允许空回滚。

在 Try 接口因为丢包时没有收到,事务管理器会触发回滚,这时会触发 Cancel 接口,这时 Cancel 执行时发现没有对应的事务 xid 或主键时,需要返回回滚成功。让事务服务管理器认为已回滚,否则会不断重试,而 Cancel 又没有对应的业务数据可以进行回滚。

防悬挂控制
场景:
	- Cancel 比 Try 先执行
出现原因:
	- Try超时(拥堵)
	- 分布式事务回滚触发 Cancel
	- 拥堵的 Try 到达

此时需要允许空回滚,但要拒绝空回滚后的Try操作

悬挂的意思是:Cancel 比 Try 接口先执行,出现的原因是 Try 由于网络拥堵而超时,事务管理器生成回滚,触发 Cancel 接口,而最终又收到了 Try 接口调用,但是 Cancel 比 Try 先到。按照前面允许空回滚的逻辑,回滚会返回成功,事务管理器认为事务已回滚成功,则此时的 Try 接口不应该执行,否则会产生数据不一致,所以我们在 Cancel 空回滚返回成功之前先记录该条事务 xid 或业务主键,标识这条记录已经回滚过,Try 接口先检查这条事务xid或业务主键如果已经标记为回滚成功过,则不执行 Try 的业务操作。

幂等控制

Try、Confirm、Cancel 三个方法均需要保证幂等性。

幂等性的意思是:对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的。因为网络抖动或拥堵可能会超时,事务管理器会对资源进行重试操作,所以很可能一个业务操作会被重复调用,为了不因为重复调用而多次占用资源,需要对服务设计时进行幂等控制,通常我们可以用事务 xid 或业务主键判重来控制。

Saga 模式

http://seata.io/zh-cn/docs/user/saga.html

XA 模式

https://blog.csdn.net/weixin_47067712/article/details/106090353

基于 MQ 实现的『最终一致性』分布式事务解决方案

基于 MQ实现的『最终一致性』分布式事务解决方案 不适用强一致性的场景。
比如 系统A是下单服务、系统B是仓储服务。在下单服务成功后,仓储服务消费到消息时点无法保证仓储数据是否满足要求。
此种解决方案适用于对时延要求不高的场景。比如。系统A为下单服务、系统B为积分服务。下单成功后总会保证积分累加成功、但什么时候成功就不一定了。

基于『本地消息表』和 MQ 实现的『最终一致性』分布式事务解决方案

高级JAVA开发 分布式事务部分_第9张图片
执行步骤:

1. 系统A执行本地业务,写入A系统消息表数据,生成唯一msgId,state设置为待处理,重复投递次数设置为0,此操作保证在一个事务里。
2. 如果步骤1执行成功,尝试向MQ发送带msgId消息,通知系统B进行下一步处理。
	此操作可能出现失败(MQ宕机、MQ网络超时等等),不过没关系,我们已经保存住了任务存根(A系统消息表数据)。
3. 消费者接收数据。
4. 处理消息,在写入DB时开启事务,保证业务处理操作和B系统消息表操作同时成功,并且消息表的msgId为唯一键,既能保证幂等,又能记录消费成功的消息。
5. 定时任务系统C轮询查询A系统消息表 state 为待处理的消息 与 B系统消息表比对。
	A、B同时存在的消息:
		处理成功的消息,修改A表消息状态为已处理。
	A存在、B不存在的消息:重复投递消息,增加A消息表重复投递次数,次数超过阈值报警。
		可能B处理失败
		可能A投递消息失败
		可能B还没来得及处理积压在MQ中
		可能B正在处理消息还没来得及提交事务

基于『可靠消息队列(RocketMQ)』实现的『最终一致性』分布式事务解决方案

高级JAVA开发 分布式事务部分_第10张图片
执行步骤:

首先,MQ需要开启持久化,保证消息在MQ环节不丢失

1. 注册回查监听
		如果步骤4执行失败,MQ会定时发送通知询问是否需要提交或者回滚
		在此监听中实现查询步骤3的业务状态返回给MQ
2. 向MQ发送消息
		消息处于Prepared状态,拿到msgId
3. 步骤2执行成功后开启本地事务,执行本地业务
   步骤2执行失败则流程结束
4. 步骤3执行成功向MQ发送Commit消息,表示可以向下游投递
   步骤3执行失败向MQ发送Rollback消息,取消投递
		如果步骤4执行失败,则依靠步骤1中的回查机制来确认消息是否需要投递
5. 消费者接收到消息投递
		如果步骤6或者7失败,这里会收到重复投递的消息
6. 消费者开启本地事务处理消息,并且保证消息的幂等性
7. 手动ACK给MQ,确认消息消费成功

你可能感兴趣的:(事务,技术栈)