最后的分布式事务 有用

Posted 十一vs十一

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了最后的分布式事务 有用相关的知识,希望对你有一定的参考价值。

第1章 分布式事务
课程安排
第一篇章:
分布式事务基础知识
分布式事务的解决方案分析
2PC方案:
atomik
os
最终一致性方案
-事务消息:
RocketMQ
第二篇章:
最终一致性方案-本地消息表(seata框架AT模式)
最终一致性方案-TCC补偿:(seata框架TCC模式)
分布式事务解决方案的优劣分析
分布式事务解决方案再分析-秒杀超卖的解决思路
1 关于分布式事务
分布式事务是由本地事务演变而来的,而本地事务,相信很多同学都已经非常熟悉了,下面1.1小节是
针对本地事务的一个简述。
1.1 本地事务
1.1.1 事务的概念
事务指逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部不成功。从而确保了数
据的准确与安全。
1.1.2 事务的四大特性
1)原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
2)一致性(Consistency)
事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
例如转账前A有1000,B有1000。转账后A+B也得是2000。
3)隔离性(Isolation)
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,每个事务不能被其他事
务的操作数据所干扰,多个并发事务之间要相互隔离。
4)持久性(Durability)
+微信study322
专业一手超清完整
全网课程 超低价格持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障
也不应该对其有任何影响。
1.1.3 事务的隔离级别
1)不考虑隔离级别,会出现以下情况:(以下情况全是错误的)
脏读:一个线程中的事务读到了另外一个线程中未提交的数据。
不可重复读:一个线程中的事务读到了另外一个线程中已经提交的
update的数据(前后内容不一样)
虚读(幻读):一个线程中的事务读到了另外一个线程中已经提交的
insert的数据(前后条数不一样)
2)数据库共定义了四种隔离级别:
Serializable
:可避免脏读、不可重复读、虚读情况的发生。(串行化)
最高
Repeatable read:可避免脏读、不可重复读情况的发生。(有可能发生幻读) 第二
Read committed:可避免脏读情况发生。 第三
Read uncommitted:最低级别,以上情况均无法保证。(读未提交) 最低
注意:级别依次升高,效率依次降低
MySQL的默认隔离级别是:REPEATABLE READ。(Oracle的默认是:READ COMMITTED)
1.1.4 随着系统架构的变化本地事务的问题
在dubbo官方网址上提供了一个系统架构的演变图,它里面展示了从单体架构到SOA架构的演变:
当我们的项目架构不再是All in One时,那么我们的数据库也可能面临着单一数据库不够用的情况。当
我们部署了多台数据库之后,新的问题就产生了,由于每台数据库都有独立的本地事务,且事务的隔离
性确定了事务之间不能相互打扰。所以,在多数据库下,事务该如何控制呢?这就是我们本篇章要解决
的问题。
1.2 分布式事务
1.2.1 分布式事务的概念
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式
系统的不同节点之上。简单来说就是组成事务的各个单元处于不同数据库服务器上。
相信同学们都接触过这种场景,手机支付,付款方和收款方的银行账号不是同一家一行,不在同一地域
的情况。那么,我们就要保证付款方减去的金额,和收款方增加的金额保持一致。
在我们的实际开发中,分布式事务无处不在,比如,电商系统中的生成订单,账户扣款,减少库存,增
加会员积分等等,他们就是组成事务的各个单元,它们要么全部发生,要么全部不发生,从而保证最终
一致,数据准确。下一小节我们将分析分布式事务的应用架构(场景)。
+微信study322
专业一手超清完整
全网课程 超低价格1.2.2 分布式事务的应用架构
1)单一服务不同数据库架构
2)单一服务分库分表架构
+微信study322
专业一手超清完整
全网课程 超低价格3)多服务不同数据库库架构
1.2.3 分布式事务的解决方案分类
1)刚性事务
刚性事务指的就是遵循本地事务四大特性(ACID)的强一致性事务。它的特点就是强一致性,要求组成
事务的各个单元马上提交或者马上回滚,没有时间弹性,要求以同步的方式执行。通常在单体架构项目
中应用较多,一般都是企业级应用(或者局域网应用)。例如:生成合同时记录日志,付款成功后生成
凭据等等。但是,在时下流行的互联网项目中,这种刚性事务来解决分布式事务就会有很多的弊端。其
中最明显的或者说最致命的就是性能问题。如下图所示:
因为某个参与者不能自己提交事务,必须等待所有参与者执行OK了之后,一起提交事务,那么事务锁
住的时间就变得非常长,从而导致性能非常低下。基于此我们也就得出了一个结论,阶段越多,性能越
差。
2)柔性事务
+微信study322
专业一手超清完整
全网课程 超低价格柔性事务是针对刚性事务而说的,我们刚才分析了刚性事务,它有两个特点,第一个强一致性,第二个
近实时性(NRT)。而柔性事务的特点是不需要立刻马上执行(同步性),且不需要强一致性。它只要
满足基本可用和最终一致就可以了。要想真正的明白,需要从BASE理论和CAP理论说起。
1.2.4 CAP理论和BASE理论
1)CAP理论
CAP理论,又叫做CAP
原则,网上对他的概念描述非常的清晰,且一致。换句话说,我们在网上搜到的
CAP理论的描述,基本都是一样的。它的描述是这样的:
CAP指的是在一个分布式系统中,一致性(
Consistency)、可用性(Availability)、分区容错性
(Partition toler
ance)。其中,C,A,P的说明如下:
一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同
一份最新的数据副本)
可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端]的读写请求。(对数据更新
具备高可用性)
分区容错性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据
一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
CAP原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。因此在进行分布式架构设计
时,必须做出取舍。而对于分布式数据系统,分区容错性是基本要求,否则就失去了价值。因此设计分
布式数据系统,就是在一致性和可用性之间取一个平衡。对于大多数web应用,其实并不需要强一致
性,因此牺牲一致性而换取高可用性,是目前多数分布式数据库产品的方向。
+微信study322
专业一手超清完整
全网课程 超低价格事务类型
时间要求
一致性要求
应用类型
应用场景
刚性事务
立即
强一致性
企业级应用(单体架构)
订单/订单项/日志
柔性事务
有时间弹性
最终一致性
互联网应用(分布式架构)
订单/支付/库存
2)BASE理论
BASE理论是指,Basically Available(基本可用)、Soft-state(
软状态/柔性事务)、Eventual
Consistency(最终一致性)。
1、基本可用 BA:(Basically Available ):
指分布式系统在出现故障的时候,允许损失部分可用性,保证核心可用。但不等价于不可用。比如:搜
索引擎0.5秒返回查询结果,但由于故障,2秒响应查询结果;网页访问过大时,部分用户提供降级服务
等。简单来说就是基本可用。
2、软状态 S:( Soft State):
软状态是指允许系统存在中间状态,并且该中间状态不会影响系统整体可用性。即允许系统在不同节点
间副本同步的时候存在延时。简单来说就是状态可以在一段时间内不同步。
3、最终一致性
E:(Eventually Consistent ):
系统中的所有数据副本经过一定时间后,最终能够达到一致的状态,不需要实时保证系统数据的强一致
性。最终一致性是弱一致性的一种特殊情况。BASE理论面向的是大型高可用可扩展的分布式系统,通
过牺牲强一致性来获得可用性。ACID是传统数据库常用的概念设计,追求强一致性模型。简单来说就是
在一定的时间窗口内,
最终数据达成一致即可。
BASE理论是基于CAP原则演化而来,是对CAP中一致性和可用性权衡的结果。
核心思想:即使无法做到强一致性,但每个业务根据自身的特点,采用适当的方式来使系统达到最终一
致性。
3)再谈刚性事务和柔性事务的比较
2 分布式事务解决方案
2.1 二阶段提交(2PC)方案详述
2.1.1 DTP标准(DTP模型)
DTP标准,也称为DTP模型。它的全称是:Distributed Transaction Processing,是X/Open提出的一
个分布式事务行业标准。X/Open,即现在的open group,是一个独立的组织,官网地址。X/Open组
织主要由各大知名公司或者厂商进行支持,这些组织不光遵循X/Open组织定义的行业技术标准,也参
与到标准的制定。下图展示了open group目前主要成员(官网截图):
+微信study322
专业一手超清完整
全网课程 超低价格名称
简称
说明
应用程序(Application Program)
AP
用于定义事务边界(即定义事务的开始和结束),
并且在事务边界内对资源进行操作。
资源管理器
(Resource Manager)
RM
如数据库、文件系统等,并提供访问资源的方
式。
事务管理器
(Transaction
Manager )
TM
负责分配事务唯一标识,监控事务的执行进度,
并负责事务的提交、回滚等。
通信资源管理器(Communication
Resource Manager):
CRM
控制一个TM域(TM domain)内或者跨TM域的分
布式应用之间的通信。
通信协议(Communication
Protocol):
CP
提供CRM提供的分布式应用节点之间的底层通
信服务。
X/Open针对DTP提供了一下参考文档:
Distributed Transaction Processing: Reference Model和
Distributed Transaction Processing: The XA Specifification。它里面包含了DTP模型和XA规范。
在DTP规范中,定义了DTP模型和XA规范。DTP模型有5个基本元素组成,他们分别是:
一个DTP模型最少需要包含AP,TM和RM三部分组成,如下图所示:
这张图类似于我们之前提到的跨库事务的概念,即单个应用需要操作多个库。在这里就是一个AP需要操
作多个RM上的资源。AP通过TM来声明一个全局事务,然后操作不同的RM上的资源,最后通知TM来提
交或者回滚全局事务。
特别的,如果分布式事务需要跨多个应用,类似于我们前面的提到的分布式事务场景中的服务化,那么
每个模型实例中,还需要额外的加入一个通信资源管理器CRM。下图中演示了2个模型实例,如何通过
CRM来进行通信:
+微信study322
专业一手超清完整
全网课程 超低价格Tips:以上内容来源于官方文档。
2.1.2 XA协议规范
XA协议是资源管理器(数据库)与事务管理器的接口标准。目前,Oracle、Informix、DB2和Sybase
等各大数据库厂家都提供对XA的支持。XA协议采用两阶段提交方式来管理分布式事务。XA接口提供资
源管理器与事务管理器之间进行通信的标准接口。XA协议包括两套函数,以xa_开头的及以ax开头的。
XA规范中定义的RM 和 TM交互的接口如下图所示:
+微信study322
专业一手超清完整
全网课程 超低价格我们以DTP本地模型实例中,由AP、RMs和TM组成,不需要其他元素。AP、RM和TM之间,彼此都需
要进行交互,如下图所示:
但是,XA协议是指定RM和TM的交互规范。在官方文献中有着清晰的介绍,如下图:
XA协议是语言无关,平台无关的。但是,我们都知道开发语言有很多种,针对不同的开发语言,都会定
义自己针对XA协议的规范。
2.1.3 JTA
+微信study322
专业一手超清完整
全网课程 超低价格JTA,它是XA协议的Java版本规范。全称是Java Transaction API。它是JavaEE的13个规范之一。官方网
站提供了资源的下载:
+微信study322
专业一手超清完整
全网课程 超低价格
我们可以下载它的手册,源码和API文档。打开API看到里面的接口,如下图:这里面定义的都是标准,是基于Java语言开发分布式事务二阶段提交的标准。而要想真正的使用,必须
使用它的实现。在早期我们都学习过JavaEE容器技术(也有称为服务器或中间件的),像Tomcat,
weblogic ,websphere,JBOSS,Jetty,Resin等等。这里面有轻量级,也有重量级。凡是重量级服务
器都是完整实现了JavaEE规范的,也就是说可以直接使用JTA的实现。除了完整的实现了JavaEE规范的
容器外,还有单独针对JTA进行实现的,像atomikos,JOTM。
2.1.4 atomikos
atomikos是一款开源的,且实现了JTA规范的分布式事务解决方案,可以在github,官方网站上下载它
的资料。在github上可以看到它的亮点,如下图:
+微信study322
专业一手超清完整
全网课程 超低价格从上面,我们可以看出,它支持java的分布式事务管理,支持微服务,支持JDBC连接池和JMS消息机
制。在早期的二阶段提交方式的分布式事务技术选型中,它的出场几率是非常高的。
2.2 TCC补偿型方案
2.2.1 关于TCC
TCC分别指的是Try,Confifirm,Cancel。它是补偿型分布式事务解决方案。何为补偿呢?其实我们把
TCC这3个部分分别做什么捋清楚,就很容理解了。首先,我们先来看下它们的主要作用:
Try 阶段主要是对业务系统做检测及资源预留。
Confifirm 阶段主要是对业务系统做确认提交Try阶段执行成功并开始执行 Confifirm阶段时,默认
Confifirm阶段是不会出错的。即:只要Try成功,Confifirm一定成功。
Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
由此,我们可以得出结论,就是在Try阶段进行尝试提交事务,当Try执行OK了,Confifirm执行,且默认
认为它一定成功。但是当Try提交失败了,则由Cancel处理回滚和资源释放。
2.2.2 TCC流程图
+微信study322
专业一手超清完整
全网课程 超低价格2.2.3 TCC和2PC的优劣分析
TCC事务的处理流程与2PC两阶段提交做比较,首先TCC是柔性事务,只要符合最终一致性即可。而2PC
是刚性事务,它是强一致性的,在任何一个分布式阶段没有返回执行成功或失败的结果时,其事务一直
会处于等待状态。并且2PC是利用DTP模型和XA规范,要求数据库支持XA规范,且通常都是在跨库的
DB层面。
而TCC则在应用层面的处理,需要通过自己编写逻辑代码来实现补偿。它的优势在于,可以让应用自己
定义数据操作的粒度,使得降低锁冲突、提高吞吐量成为可能。而不足之处则在于对应用的侵入性非常
强,业务逻辑的每个分支都需要实现try、confifirm、cancel三个操作。此外,其实现难度也比较大,需
要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。
2.3 最终一致性方案
2.3.1 本地消息表
1)
简述
+微信study322
专业一手超清完整
全网课程 超低价格这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路
是来源于ebay。它和MQ事务消息的实现思路都是一样的,都是利用MQ通知不同的服务实现事务的操
作。不同的是,针对消息队列的信任情况,分成了两种不同的实现。本地消息表它是对消息队列的稳定
性处于不信任的态度,认为消息可能会出现丢失,或者消息队列的运行网络会出现阻塞,于是在数据库
中建立一张独立的表,用于存放事务执行的状态,配合消息队列实现事务的控制。
2)优缺点
优点:
一种非常经典的实现,避免了分布式事务,实现了最终一致性。
缺点:
消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
2.3.2 MQ
事务消息
1)简述
有一些第三方的MQ是支持事务消息的,比如RocketMQ,ActiveMQ,他们支持事务消息的方式也是类
似于采用的二阶段提交。但是有一些常用的MQ也不支持事务消息,比如 RabbitMQ 和 Kafka 都不支
持。
以阿里的 RocketMQ 中间件为例,其思路大致为:
第一阶段Prepared消息,会拿到消息的地址。
第二阶段执行本地事务。
第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送
失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者
确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继
续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。下图描述了它的工作原理:
+微信study322
专业一手超清完整
全网课程 超低价格分
共同点
优势
弊端
都需要自
己写业务
补偿代码
一种非常经典的实现,避免
了分布式事务,实现了最终
一致性。
消息表会耦合到业务系统中,如果没有
封装好的解决方案,会有很多杂活需要
处理。
MQ
都需要自
己写业务
补偿代码
实现了最终一致性,不需要
依赖本地数据库事务。
用消息队列的方式实现分布
式事务,效率较高
目前主流MQ中有ActiveMQ RocketMQ
支持事务消息
实现难度较大,和业务耦合比较紧密
2)它和本地消息表的对比
3 分布式事务实战-二阶段提交方案的具体应
用:atomikos
3.1 atomikos
3.1.1 概述
+微信study322
专业一手超清完整
全网课程 超低价格atomikos一套开源的,JTA规范的实现。它是基于二阶段提交思想实现分布式事务的控制。是分布式刚
性事务的一种解决方案。在当下互联网开发中选择此种解决方案的场景也不是很多了。
3.1.2 使用场景
通常情况下使用2pc提交方案的场景都是单服务多数据源(多数据库)的情况。在前面我们讲解分布式
事务开篇时已经介绍了项目架构的演进过程,在分布式架构或微服务架构中,它面临着多个服务间的调
用,这时就可能会出现其中一个服务处于ok状态,而另一个服务执行出现状况,因为二阶段提交方案中
参与者不能提交事务,要等待其他参与者都
ok了,才能一起提交,那么此时就会一直等待第二个服务执
行返回结果,造成事务一直锁住的状态,性能不高。所以一般是单体架构配多个数据库的项目居多。
3.2 二阶段提交的案例简介
3.2.1 场景说明
一个企业级应用项目:进销存系统。系统要对针对库存记录访问日志。并且,库存系统数据库和日志数
据库不是同一个数据库。
3.2.2 功能说明
3.2.3 项目简介
1)数据库表结构
+微信study322
专业一手超清完整
全网课程 超低价格2)提前准备的工程
3.3 测试案例
3.3.1 导入数据库
CREATE DATABASE `stockdb`;
DROP TABLE IF EXISTS `t_stock`;
CREATE TABLE `t_stock` (
`id` varchar(100) NOT NULL,
`product_id` varchar(100) DEFAULT NULL,
`product_name` varchar(200) DEFAULT NULL,
`stock_id` varchar(100) DEFAULT NULL,
`quantity` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+微信study322
专业一手超清完整
全网课程 超低价格3.3.2 导入工程
3.3.3 编写测试类和测试方法
CREATE DATABASE `logdb`;
DROP TABLE IF EXISTS `t_log_info`;
CREATE TABLE `t_log_info` (
`id` varchar(100) NOT NULL,
`method` varchar(100) DEFAULT NULL,
`action` varchar(500) DEFAULT NULL,
`username` varchar(100) DEFAULT NULL,
`create_time` date DEFAULT NULL,
PRIMARY KEY
(
`
id
`
)
) ENGINE=Inno
DB
D
EF
A
ULT CHARSET=utf8;
package com.itheima.test;
import com.itheima.domain.Stock;
import com.itheima.service.StockService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.UUID;
/**
* @author 黑马程序员
* @Company http://www.itheima.com
*/
@RunWith(SpringJUnit4ClassRunner.class)
+微信study322
专业一手超清完整
全网课程 超低价格3.3.4 编写atomikos核心代码
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class AtomikosTest
@Autowired
private StockService stockService;
@Test
public void testSave()
//1.创建对象
Stock
st
oc
k = new Stock();
//2.填
St
r
in
g
i
d
=
U
U
ID
.randomUUID().toString().replace("-","").toUpperCase();
st
o
c
k
.
se
t
I
d
(
id
)
;
s
t
o
c
k
.
s
e
t
P
r
o
d
u
c
t
Id
(
"1
0
30
10
2
3
0
1
111");
s
t
o
c
k
.
s
e
t
P
r
o
d
u
c
tN
am
e
(
"华
p
8
"
)
;
stock.setQuantity(100);
//3.执行保存
stockService.save(stock);
<!--
配置事务管理器atomikos事务管理器
-->
<bean id="atomikosTransactionManager"
class="com.atomikos.icatch.jta.UserTransactionManager" init-method="init"
destroy-method="close">
<property name="forceShutdown" value="false"/>
</bean>
<!--
本地事务管理器
-->
<bean id="atomikosUserTransaction"
class="com.atomikos.icatch.jta.UserTransactionImp">
<property name="transactionTimeout" value="300000"/>
</bean>
<!--JTA事务管理器-->
<bean id="springTransactionManager"
class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManager">
<ref bean="atomikosTransactionManager"/>
</property>
<property name="userTransaction">
<ref bean="atomikosUserTransaction"/>
</property>
<property name="allowCustomIsolationLevels" value="true"/>
</bean>
<!--数据源基础配置-->
<bean id="abstractXADataSource"
class="com.atomikos.jdbc.AtomikosDataSourceBean" init-method="init" destroy
method="close" abstract="true">
+微信study322
专业一手超清完整
全网课程 超低价格<property name="xaDataSourceClassName"
value="com.mysql.jdbc.jdbc2.optional.MysqlXADataSource"/>
<property name="poolSize" value="10"/>
<property name="minPoolSize" value="10"/>
<property name="maxPoolSize" value="30"/>
<property name="borrowConnectionTimeout" value="60"/>
<property name="reapTimeout" value="20"/>
<property name="maxIdleTime" value="60"/>
<property name="maintenanceInterval" value="60"/>
<prop
e
rt
y
na
m
e
=
"
t
e
s
tQ
u
e
ry
">
<
v
a
l
u
e>
S
E
L
EC
T
1
<
/v
a
l
ue
>
</
property>
</bea
n>
<!-- 数据库基本信息配置 -->
<bean id="dataSourceStock" parent="abstractXADataSource">
<property name="uniqueResourceName">
<value>dataSourceStock</value>
</property>
<!--数据库驱动-->
<property name="xaDataSourceClassName"
value="com.mysql.jdbc.jdbc2.optional.MysqlXADataSource"/>
<property name="xaProperties">
<props>
<prop key="URL">$jdbc.stock.url</prop>
<prop key="user">$jdbc.stock.username</prop>
<prop key="password">$jdbc.stock.password</prop>
</props>
</property>
</bean>
<!--日志数据源-->
<bean id="dataSourceLog" parent="abstractXADataSource">
<property name="uniqueResourceName">
<value>dataSourceLog</value>
</property>
<property name="xaDataSourceClassName"
value="com.mysql.jdbc.jdbc2.optional.MysqlXADataSource"/>
<property name="xaProperties">
<props>
<prop key="URL">$jdbc.log.url</prop>
<prop key="user">$jdbc.log.username</prop>
<prop key="password">$jdbc.log.password</prop>
</props>
</property>
</bean>
<!--SqlSessionFactoryBean的配置-->
<bean id="sqlSessionFactoryBeanStock"
class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="typeAliasesPackage" value="com.itheima.domain" />
<property name="dataSource" ref="dataSourceStock"/>
</bean>
<!--操作日志数据源的SqlSessionFactoryBean-->
+微信study322
专业一手超清完整
全网课程 超低价格3.4 atomikos执行原理分析及2pc总结
3.4.1 原理分析
以下内容来自IBM社区
很多开发人员都会对 JTA 的内部工作机制感兴趣:我编写的代码没有任何与事务资源(如数据库连接)
互动的代码,但是我的操作(数据库更新)却实实在在的被包含在了事务中,那 JTA 究竟是通过何种方
式来实现这种透明性的呢?
要理解 JTA 的实现原理首先需要了解其架构:它包括事务管理器
(Transaction Manager)和一个或多个支持 XA 协议的资源管理器 ( Resource Manager ) 两部分,
们可以将资源管理器看做任意类型的持久化数据存储;事务管理器则承担着所有事务参与单元的协调与
控制。
根据所面向对象的不同,我们可以将 JTA 的事务管理器和资源管理器理解为两个方面:面向开发人员的
使用接口(事务管理器)和面向服务提供商的实现接口(资源管理器)。其中开发接口的主要部分即为
上述示例中引用的 UserTransaction 对象,开发人员通过此接口在信息系统中实现分布式事务;而实现
接口则用来规范提供商(如数据库连接提供商)所提供的事务服务,它约定了事务的资源管理功能,使
得 JTA 可以在异构事务资源之间执行协同沟通。
以数据库为例,IBM 公司提供了实现分布式事务的数据库驱动程序,Oracle 也提供了实现分布式事务
的数据库驱动程序,
在同时使用 DB2 和 Oracle 两种数据库连接时, JTA 即可以根据约定的接口协调者
两种事务资源从而实现分布式事务。
正是基于统一规范的不同实现使得 JTA 可以协调与控制不同数据库或者 JMS 厂商的事务资源,其架构如
下图所示:
<bean id="sqlSessionFactoryBeanLog"
class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="typeAliasesPackage" value="com.itheima.domain" />
<property name="dataSource" ref="dataSourceLog"/>
</bean>
<!--包扫描,库存Dao包扫描-->
<bean id="mapperScannerConfigurerStock"
class="org.my
ba
t
i
s
.
s
p
r
in
g
.m
a
p
pe
r
.M
a
p
p
e
rS
c
a
nn
e
r
Co
n
fi
g
u
r
er
">
<prop
e
r
ty
n
a
m
e
=
"
ba
s
e
Pa
ck
a
g
e
"
v
a
l
u
e=
"
co
m
.i
t
h
ei
m
a
.d
ao.stock" />
<
p
ro
p
e
r
ty
na
m
e=
"s
q
lS
e
s
s
io
n
F
a
ctoryBeanName"
value="sq
l
S
e
s
si
o
n
Fa
c
to
ry
B
ea
n
St
o
c
k
"
/
>
</be
an
>
<!--
描,日志Dao包扫描-->
<bean id="mapperScannerConfigurerLog"
class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.itheima.dao.log" />
<property name="sqlSessionFactoryBeanName"
value="sqlSessionFactoryBeanLog" />
</bean>
+微信study322
专业一手超清完整
全网课程 超低价格开发人员使用开发人员接口,实现应用程序对全局事务的支持;各提供商(数据库,JMS 等)依据提供
商接口的规范提供事务资源管理功能;事务管理器( TransactionManager )将应用对分布式事务的使
用映射到实际的事务资源并在事务资源间进行协调与控制。 下面,本文将对包括 UserTransaction、
Transaction 和 TransactionManager 在内的三个主要接口以及其定义的方法进行介绍。
1)面向开发人员的UserTransaction
开发人员通常只使用此接口实现 JTA 事务管理,其定义了如下的方法:
begin()- 开始一个分布式事务,(在后台 TransactionManager 会创建一个 Transaction 事务对象
并把此对象通过 ThreadLocale 关联到当前线程上 )
commit()- 提交事务(在后台 TransactionManager 会从当前线程下取出事务对象并把此对象所
代表的事务提交)
rollback()- 回滚事务(在后台 TransactionManager 会从当前线程下取出事务对象并把此对象所
代表的事务回滚)
getStatus()- 返回关联到当前线程的分布式事务的状态 (Status 对象里边定义了所有的事务状态,
感兴趣的读者可以参考 API 文档 )
setRollbackOnly()- 标识关联到当前线程的分布式事务将被回滚
2)面向提供商的TransactionManager 和 Transaction
Transaction 代表了一个物理意义上的事务,在开发人员调用 UserTransaction.begin() 方法时
TransactionManager 会创建一个 Transaction 事务对象(标志着事务的开始)并把此对象通过
ThreadLocale 关联到当前线程。UserTransaction 接口中的 commit()、rollback(),getStatus() 等方法
都将最终委托给 Transaction 类的对应方法执行。Transaction 接口定义了如下的方法:
commit()- 协调不同的事务资源共同完成事务的提交
rollback()- 协调不同的事务资源共同完成事务的回滚
setRollbackOnly()- 标识关联到当前线程的分布式事务将被回滚
getStatus()- 返回关联到当前线程的分布式事务的状态
enListResource(XAResource xaRes, int flflag)- 将事务资源加入到当前的事务中(在上述示例
中,在对数据库 A 操作时 其所代表的事务资源将被关联到当前事务中,同样,在对数据库 B 操作
时其所代表的事务资源也将被关联到当前事务中)
delistResourc(XAResource xaRes, int flflag)- 将事务资源从当前事务中删除
registerSynchronization(Synchronization sync)- 回调接口,Hibernate 等 ORM 工具都有自
己的事务控制机制来保证事务,
但同时它们还需要一种回调机制以便在事务完成时得到通知从而
触发一些处理工作,如清除缓存等。这就涉及到了 Transaction 的回调接口
+微信study322
专业一手超清完整
全网课程 超低价格registerSynchronization。工具可以通过此接口将回调程序注入到事务中,当事务成功提交后,
回调程序将被激活。
TransactionManager 本身并不承担实际的事务处理功能,它更多的是充当用户接口和实现接口之间的
桥梁。下面列出了 TransactionManager 中定义的方法,可以看到此接口中的大部分事务方法与
UserTransaction 和 Transaction 相同。 在开发人员调用 UserTransaction.begin() 方法时
TransactionManager 会创建一个 Transaction 事务对象(标志着事务的开始)并把此对象通过
ThreadLocale 关联到当前线程上;同样 UserTransaction.commit() 会调用
TransactionManager.commit()
方法将从当前线程下取出事务对象 Transaction 并把此对象所代表的
事务提交,
即调用
Transaction.commit()
begin()-
开始事务
commit()
- 提交事务
rollback()
- 回滚事务
getStatus()
- 返回当前事务状态
setRollbackOnly()
getTransaction()- 返回关联到当前线程的事务
setTransactionTimeout(int seconds)- 设置事务超时时间
resume(Transaction tobj)- 继续当前线程关联的事务
suspend()- 挂起当前线程关联的事务
在系统开发过程中会遇到需要将事务资源暂时排除的操作,此时就需要调用 suspend() 方法将当前的事
务挂起:在此方法后面所做的任何操作将不会被包括在事务中,在非事务性操作完成后调用 resume()
以继续事务(注:
要进行此操作需要获得 TransactionManager 对象,
其获得方式在不同的 J2EE 应用
服务器上是不一样的)下面将通过具体的代码向读者介绍 JTA 实现原理。
下图列出了示例实现中涉及到的 Java 类,其中 UserTransactionImpl 实现了 UserTransaction 接口,
TransactionManagerImpl 实现了 TransactionManager 接口,TransactionImpl 实现了 Transaction
接口:
+微信study322
专业一手超清完整
全网课程 超低价格3.4.2 二阶段提交总结
二阶段提交作为早期分布式事务的解决方案,逐渐的淡出了主流方案的圈子。这里面其最重要的原因就
是它是刚性事务,即需要满足强一致性。它的优点就是可以在多数据库间实现事务控制,而摆脱单一数
据库使用事务的宿命。但是阻塞式这个缺点确是致命的,因为参与全局事务的数据库被动听从事务管理
器的命令,执行或放弃事务,如果运行事务管理器的机器宕机,那整个系统就不能用了。当然,在极端
情况下还可能同时影响其他系统,如果事务管理器挂了,但是这个数据库的表锁还没释放,因为数据库
还在等待事务管理器的命令,因此,使用这个数据库的其他应用也会收到影响。
4 分布式事务实战
-事务消息:RocketMQ
4.1 关于
RocketMQ
4.1.1 RokectMQ简介
RocketMQ是阿里巴巴开发的一款原生分布式消息队列,并且其内部声称不遵循任何规范(主要是
JMS),目前已经被apache软件基金会收录。它是目前实现市场上为数不多的支持事务消息的消息中间
件。
通过访问官方网址,看到如下页面:
通过点击Getting Started,前往详情页面,里面有下载专区,可以下载对应的RoketMQ。同时,它还
给我们提供了github网址,里面有中文的资料介绍,如下图显示:
+微信study322
专业一手超清完整
全网课程 超低价格这里面有RocketMQ的相关介绍,我们的课程主要是针对RokectMQ的事务消息的讲解,所以在课程中
就不给同学们逐一展开说明了,同学们可以根据自身情况前往了解。
4.1.2 下载安装
前往RocketMQ的下载页面,下载最新的版本,如下图所示:
+微信study322
专业一手超清完整
全网课程 超低价格4.2 RocketMQ中的事务消息
4.2.1 事务消息的简介
消息队列,相信同学们都很属熟悉了,在微服务架构中,两个服务间异步通信通常都会使用消息队列来
保证执行效率。例如,用户微服务和短信微服务,在用户注册时,给用户发送手机验证码短信时,就会
用到它。而如果同时需要事务的支持,那么通常会选择支持事务消息的消息队列来实现。
事务消息,它是消息队列中一种特殊的消息类型,只不过不是所有的消息队列产品都支持事务消息。目
前支持事务消息的队列一个是阿里的RocketMQ(已经被apache收录),一个是ActiveMQ。今天我们
课程使用的是RocketMQ。
RocketMQ事务消息(Transactional Message)是指应用本地事务和发送消息操作可以被定义到全局
事务中,要么同时成功,要么同时失败。RocketMQ的事务消息提供类似 X/Open XA 的分布事务功能,
通过事务消息能达到分布式事务的最终一致。
此处我们需要明确一件事,分布式事务和事务消息两者并没有关系,事务消息仅仅保证本地事务和MQ
消息发送形成整体的原子性,而投递到MQ服务器后,消费者是否能一定消费成功是无法保证的。
+微信study322
专业一手超清完整
全网课程 超低价格4.2.2 RocketMQ事务消息的执行流程图
4.3 事务消息的案例简介
4.3.1 场景说明
今日课程中我们以最常见的电商项目为例,从中节选一个场景:订单和支付。我们都知道,在下单成功
后,马上紧跟着的就是需要付款。只有付款成功了之后,订单的状态才会改为已付款,进而继续走出
库,发货,物流等等的流程,而如果订单迟迟不付款的话,超过一个时限之后就自动关闭了。下图描述
了场景的业务流程:
4.3.2 功能说明
4.3.3 项目简介
1)数据库表结构
+微信study322
专业一手超清完整
全网课程 超低价格2)提前准备的工程
4.4 案例实现
4.4.1 导入工程
4.4.2 导入坐标
1)父工程坐标
DROP TABLE IF EXISTS `pay`;
CREATE TABLE `pay` (
`id` varchar(100) NOT NULL,
`ispay` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `
pay` VALUES (\'1234\', \'0\');
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+微信study322
专业一手超清完整
全网课程 超低价格<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/>
</parent>
<depe
nd
e
nc
ie
s>
<d
e
pe
nd
en
cy>
<
g
r
o
u
p
Id
>
o
r
g
.
sp
ri
ng
f
r
am
e
w
o
rk
.
bo
o
t<
/
g
r
o
up
Id
>
<
a
r
ti
f
a
c
t
Id
>
s
p
ri
ng
-
bo
o
t
-
s
ta
rt
e
r
-t
e
s
t<
/
a
rt
i
factId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<!--锁定子模块使用的spring_cloud版本-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
+微信study322
专业一手超清完整
全网课程 超低价格2)支付服务坐标
3)订单服务坐标
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<
na
m
e>
S
p
ri
n
g
M
il
es
t
o
n

你们想看的分布式事务,三歪搞来了。

本文已收录至我的GitHub

今天我想和大家一起盘一盘分布式事务,会介绍常见的分布式事务实现方案和其优缺点以及适用的场景,并会带出它们的一些变体实现。

还会捎带一下分布式数据库对 2PC 的改进模型,看看分布式数据库是如何做的。

然后再分析一波分布式事务框架 Seata 的具体实现,看看分布式事务究竟是如何落地的,毕竟协议要落地才是有用的。

你们想看的分布式事务,三歪搞来了。

首先我们来提一下事务和分布式事务是什么。

事务

事务的 ACID 想必大家都熟知,这其实是严格意义上的定义,指的是事务的实现必须具备原子性、一致性、隔离性和持久性。

不过严格意义上的事务很难达到,像我们熟知的数据库就有各种隔离级别,隔离级别越高性能越低,所以往往我们都会从中找到属于自己的平衡,不会遵循严格意义上的事务

并且在我们平日的谈论中,所谓的事务往往简单的指代一系列的操作全部执行成功,或者全部失败,不会出现一些成功一些失败的情形。

清晰了平日我们对事务的定义之后,再来看看什么是分布式事务。

分布式事务

由于互联网的快速发展,以往的单体架构顶不住这么多的需求,这么复杂的业务,这么大的流量。

单体架构的优势在于前期快速搭建、快速上线,并且方法和模块之间都是内部调用,没有网络的开销更加的高效。

从某方面来说部署也方便,毕竟就一个包,扔上去。

不过随着企业的发展,业务的复杂度越来越高,内部耦合极其严重,导致牵一发而动全身,开发不易,测试不易。

并且无法根据热点服务进行动态的伸缩,比如商品服务访问量特别大,如果是单体架构的话我们只能把整个应用复制多份集群部署,浪费资源。

因此拆分势在必行,微服务架构就这么来了。

你们想看的分布式事务,三歪搞来了。

拆分之后服务之间的边界就清晰了,每个服务都能独立地运行,独立地部署,所以能以服务级别弹性伸缩了。

服务之间的本地调用变成了远程调用,链路更长了,一次调用的耗时更长了,但是总体的吞吐量更大了。

不过拆分之后还会引入其他复杂度,比如服务链路的监控、整体的监控、容错措施、弹性伸缩等等运维监控的问题,还有像分布式事务、分布式锁跟业务息息相关的问题等。

往往解决了一个痛点又会引入别的痛点,所以架构的演进都是权衡的结果,就看你们的系统更能忍受哪种痛点了。

而今天我们谈及的就是分布式事务这个痛点。

分布式事务是由多个本地事务组成的,分布式事务跨越了多设备,之间又经历的复杂的网络,可想而知想要实现严格的事务道路阻且长。

单机版事务都不会严格遵守事务的严格实现,更别说分布式事务了,所以在现实情况下我们只能实现残缺版的事务。

在明确了事务和分布式事务之后,我们就先来看看常见的分布式事务方案:2PC、3PC、TCC、本地消息、事务消息。

2PC

2PC,Two-phase commit protocol,即两阶段提交协议。它引入了一个事务协调者角色,来管理各个参与者(就是各数据库资源)。

整体分为两个阶段,分别是准备阶段和提交/回滚阶段。

我们先来看看第一个阶段,即准备阶段。

你们想看的分布式事务,三歪搞来了。

由事务协调者给每个参与者发送准备命令,每个参与者收到命令之后会执行相关事务操作,你可以认为除了事务的提交啥都做了。

然后每个参与者会返回响应告知协调者自己是否准备成功。

协调者收到每个参与者的响应之后就进入第二阶段,根据收集的响应,如果有一个参与者响应准备失败那么就向所有参与者发送回滚命令,反之发送提交命令。

你们想看的分布式事务,三歪搞来了。

这个协议其实很符合正常的思维,就像我们大学上课点名的时候,其实老师就是协调者的角色,我们都是参与者。

老师一个一个的点名,我们一个一个的喊到,最后老师收到所有同学的到之后就开始了今天的讲课。

而和点名有所不同的是,老师发现某几个学生不在还是能继续上课,而我们的事务可不允许这样

事务协调者在第一阶段未收到个别参与者的响应,则等待一定时间就会认为事务失败,会发送回滚命令,所以在 2PC 中事务协调者有超时机制。

我们再来分析一下 2PC 的优缺点。

2PC 的优点是能利用数据库自身的功能进行本地事务的提交和回滚,也就是说提交和回滚实际操作不需要我们实现,不侵入业务逻辑由数据库完成,在之后讲解 TCC 之后相信大家对这点会有所体会。

2PC 主要有三大缺点:同步阻塞、单点故障和数据不一致问题。

你们想看的分布式事务,三歪搞来了。

同步阻塞

可以看到在第一阶段执行了准备命令后,我们每个本地资源都处于锁定状态,因为除了事务的提交之外啥都做了。

所以这时候如果本地的其他请求要访问同一个资源,比如要修改商品表 id 等于 100 的那条数据,那么此时是被阻塞住的,必须等待前面事务的完结,收到提交/回滚命令执行完释放资源后,这个请求才能得以继续。

所以假设这个分布式事务涉及到很多参与者,然后有些参与者处理又特别复杂,特别慢,那么那些处理快的节点也得等着,所以说效率有点低。

单点故障

可以看到这个单点就是协调者,如果协调者挂了整个事务就执行不下去了

如果协调者在发送准备命令前挂了还行,毕竟每个资源都还未执行命令,那么资源是没被锁定的。

可怕的是在发送完准备命令之后挂了,这时候每个本地资源都执行完处于锁定状态了,都杵着了,这就很僵硬了,如果是某个热点资源都阻塞了,这估计就要GG了。

你们想看的分布式事务,三歪搞来了。

数据不一致问题

因为协调者和参与者之间的交流是经过网络的,而网络有时候就会抽风的或者发生局部网络异常。

那么就有可能导致某些参与者无法收到协调者的请求,而某些收到了。比如是提交请求,然后那些收到命令的参与者就提交事务了,此时就产生了数据不一致的问题。

小结一下 2PC

至此我们来先小结一些 2PC ,它是一个同步阻塞的强一致性两阶段提交协议,分别是准备阶段和提交/回滚阶段。

2PC 的优势在于对业务没有侵入,可以利用数据库自身机制来进行事务的提交和回滚。

它的缺点:是一个同步阻塞协议,会导致高延迟和性能的下降,并且存在协调者单点故障问题,极端情况下会有数据不一致的问题。

当然这只是协议,具体的落地还是可以变通了,比如协调者单点问题,我就搞个主从来实现协调者,对吧。

分布式数据库的 2PC 改进模型

可能有些人对分布式数据库不熟悉,没有关系,我们主要学的是思想,看看人家的思路。

我简单的讲下 Percolator 模型,它是基于分布式存储系统 BigTable 建立的模型,BigTable 是啥也不清楚的同学没有关系影响不大。

还是拿转账的例子来说,我现在有 200 块钱,你现在有 100 块钱,为了突出重点我也不按正常的结构来画这个表。

你们想看的分布式事务,三歪搞来了。

然后我要转 100 块给你。

你们想看的分布式事务,三歪搞来了。

此时事务管理器发起了准备请求,然后我账上的钱就少了,你账上的钱就多了,而且事务管理器还记录下这次操作的日志

此时的数据还是私有版本,别的事务是读不到的,简单的理解 Lock 上有值就还是私有的。

可以看到我的记录 Lock 标记的是 PK,你的记录标记的是指向我的记录指针,这个 PK 是随机选择的。

然后事务管理器会向被选择作为 PK 的那条记录发起提交指令。

你们想看的分布式事务,三歪搞来了。

此时就会把我的记录的锁给抹去了,这等于我的记录不再是私有版本了,别的事务就都能访问了。

那你的记录上还有锁啊?不用更新吗?

嘿嘿不需要及时更新,因为访问你的这条记录的时候会去根据指针找我的那个记录,发现记录已经提交了所以你的记录就可以被访问了。

有人说这效率不就差了,每次都要去找一次,别急。

后台会有个线程来扫描,然后更新把锁记录给去了。

这不就稳了嘛。

相比于 2PC 的改进

首先 Percolator 在提交阶段不需要和所有的参与者交互,主需要和一个参与者打交道,所以这个提交是原子的!解决了数据不一致问题

然后事务管理器会记录操作日志,这样当事务管理器挂了之后选举的新事务管理器就可以通过日志来得知当前的情况从而继续工作,解决了单点故障问题

并且 Percolator 还会有后台线程,会扫描事务状况,在事务管理器宕机之后会回滚各个参与者上的事务。

可以看到相对于 2PC 还是做了很多改进的,也是巧妙的。

其实分布式数据库还有别的事务模型,不过我也不太熟悉,就不多哔哔了,有兴趣的同学可以自行了解。

还是挺能拓宽思想的。

XA 规范

让我们再回来 2PC,既然说到 2PC 了那么也简单的提一下 XA 规范,XA 规范是基于两阶段提交的,它实现了两阶段提交协议。

在说 XA 规范之前又得先提一下 DTP 模型,即 Distributed Transaction Processing,这模型规范了分布式事务的模型设计。

而 XA 规范又约束了 DTP 模型中的事务管理器(TM) 和资源管理器(RM)之间的交互,简单的说就是你们两之间要按照一定的格式规范来交流!

我们先来看下 XA 约束下的 DTP 模型。

你们想看的分布式事务,三歪搞来了。
  • AP 应用程序,就是我们的应用,事务的发起者。
  • RM 资源管理器,简单的认为就是数据库,具备事务提交和回滚能力,对应我们上面的 2PC 就是参与者。
  • TM 事务管理器,就是协调者了,和每个 RM 通信。

简单的说就是 AP 通过 TM 来定义事务操作,TM 和 RM 之间会通过 XA 规范进行通信,执行两阶段提交,而 AP 的资源是从 RM 拿的。

从模型上看有三个角色,而实际实现可以由一个角色实现两个功能,比如 AP 来实现 TM 的功能,TM 没必要抽出来单独部署。

MySQL XA

知晓了 DTP 之后,我们就来看看 XA 在 MySQL 中是如何操作的,不过只有 InnoDB 支持。

简单的说就是要先定义一个全局唯一的 XID,然后告知每个事务分支要进行的操作。

可以看到图中执行了两个操作,分别是改名字和插入日志,等于先注册下要做的事情,通过 XA START XID 和 XA END XID 来包裹要执行的 SQL。

你们想看的分布式事务,三歪搞来了。

然后需要发送准备命令,来执行第一阶段,也就是除了事务的提交啥都干了的阶段。

你们想看的分布式事务,三歪搞来了。

然后根据准备的情况来选择执行提交事务命令还是回滚事务命令。

你们想看的分布式事务,三歪搞来了。

基本上就是这么个流程,不过 MySQL XA 的性能不高这点是需要注意的。

可以看到虽说 2PC 有缺点,但是还是有基于 2PC 的落地实现的,而 3PC 的引出是为了解决 2PC 的一些缺点,但是它整体下来开销更大,也解决不了网络分区的问题,我也没有找到 3PC 的落地实现。

不过我还是稍微提一下,知晓一下就行,纯理论。

3PC

3PC 的引入是为了解决 2PC 同步阻塞和减少数据不一致的情况。

3PC 也就是多了一个阶段,一个询问的阶段,分别是准备、预提交和提交这三个阶段。

准备阶段单纯就是协调者去访问参与者,类似于你还好吗?能接请求不。

预提交其实就是 2PC 的准备阶段,除了事务的提交啥都干了。

提交阶段和 2PC 的提交一致。

你们想看的分布式事务,三歪搞来了。

3PC 多了一个阶段其实就是在执行事务之前来确认参与者是否正常,防止个别参与者不正常的情况下,其他参与者都执行了事务,锁定资源。

出发点是好的,但是绝大部分情况下肯定是正常的,所以每次都多了一个交互阶段就很不划算。

然后 3PC 在参与者处也引入了超时机制,这样在协调者挂了的情况下,如果已经到了提交阶段了,参与者等半天没收到协调者的情况的话就会自动提交事务。

不过万一协调者发的是回滚命令呢?你看这就出错了,数据不一致了。

还有维基百科上说 2PC 参与者准备阶段之后,如果协调者挂了,参与者是无法得知整体的情况的,因为大局是协调者掌控的,所以参与者相互之间的状况它们不清楚。

而 3PC 经过了第一阶段的确认,即使协调者挂了参与者也知道自己所处预提交阶段是因为已经得到准备阶段所有参与者的认可了。

简单的说就像加了个围栏,使得各参与者的状态得以统一。

小结 2PC 和 3PC

从上面已经知晓了 2PC 是一个强一致性的同步阻塞协议,性能已经是比较差的了。

而 3PC 的出发点是为了解决 2PC 的缺点,但是多了一个阶段就多了一次通讯的开销,而且是绝大部分情况下无用的通讯。

虽说引入参与者超时来解决协调者挂了的阻塞问题,但是数据还是会不一致。

可以看到 3PC 的引入并没什么实际突破,而且性能更差了,所以实际只有 2PC 的落地实现。

再提一下,2PC 还是 3PC 都是协议,可以认为是一种指导思想,和真正的落地还是有差别的。

你们想看的分布式事务,三歪搞来了。

TCC

不知道大家注意到没,不管是 2PC 还是 3PC 都是依赖于数据库的事务提交和回滚。

而有时候一些业务它不仅仅涉及到数据库,可能是发送一条短信,也可能是上传一张图片。

所以说事务的提交和回滚就得提升到业务层面而不是数据库层面了,而 TCC 就是一种业务层面或者是应用层的两阶段提交

TCC 分为指代 Try、Confirm、Cancel ,也就是业务层面需要写对应的三个方法,主要用于跨数据库、跨服务的业务操作的数据一致性问题。

TCC 分为两个阶段,第一阶段是资源检查预留阶段即 Try,第二阶段是提交或回滚,如果是提交的话就是执行真正的业务操作,如果是回滚则是执行预留资源的取消,恢复初始状态。

比如有一个扣款服务,我需要写 Try 方法,用来冻结扣款资金,还需要一个 Confirm 方法来执行真正的扣款,最后还需要提供 Cancel 来进行冻结操作的回滚,对应的一个事务的所有服务都需要提供这三个方法。

可以看到本来就一个方法,现在需要膨胀成三个方法,所以说 TCC 对业务有很大的侵入,像如果没有冻结的那个字段,还需要改表结构。

我们来看下流程。

你们想看的分布式事务,三歪搞来了。

虽说对业务有侵入,但是 TCC 没有资源的阻塞,每一个方法都是直接提交事务的,如果出错是通过业务层面的 Cancel 来进行补偿,所以也称补偿性事务方法。

这里有人说那要是所有人 Try 都成功了,都执行 Comfirm 了,但是个别 Confirm 失败了怎么办?

这时候只能是不停地重试调失败了的 Confirm 直到成功为止,如果真的不行只能记录下来,到时候人工介入了。

TCC 的注意点

这几个点很关键,在实现的时候一定得注意了。

你们想看的分布式事务,三歪搞来了。

幂等问题,因为网络调用无法保证请求一定能到达,所以都会有重调机制,因此对于 Try、Confirm、Cancel 三个方法都需要幂等实现,避免重复执行产生错误。

空回滚问题,指的是 Try 方法由于网络问题没收到超时了,此时事务管理器就会发出 Cancel 命令,那么需要支持 Cancel  在未执行 Try 的情况下能正常的 Cancel。

悬挂问题,这个问题也是指 Try 方法由于网络阻塞超时触发了事务管理器发出了 Cancel 命令,但是执行了 Cancel 命令之后 Try 请求到了,你说气不气

这都 Cancel 了你来个 Try,对于事务管理器来说这时候事务已经是结束了的,这冻结操作就被“悬挂”了,所以空回滚之后还得记录一下,防止 Try 的再调用。

TCC 变体

上面我们说的是通用型的 TCC,它需要改造以前的实现,但是有一种情况是无法改造的,就是你调用的是别的公司的接口

没有 Try 的 TCC

比如坐飞机需要换乘,换乘的又是不同的航空公司,比如从 A 飞到 B,再从 B 飞到 C,只有 A - B 和 B - C 都买到票了才有意义。

这时候的选择就没得 Try 了,直接调用航空公司的买票操作,当两个航空公司都买成功了那就直接成功了,如果某个公司买失败了,那就需要调用取消订票接口。

也就是在第一阶段直接就执行完整个业务操作了,所以要重点关注回滚操作,如果回滚失败得有提醒,要人工介入等。

这其实就是 TCC 的思想。

你们想看的分布式事务,三歪搞来了。

异步 TCC

这 TCC 还能异步?其实也是一种折中,比如某些服务很难改造,并且它又不会影响主业务决策,也就是它不那么重要,不需要及时的执行。

这时候可以引入可靠消息服务,通过消息服务来替代个别服务来进行 Try、Confirm、Cancel 。

Try 的时候只是写入消息,消息还不能被消费,Confirm 就是真正发消息的操作,Cancel 就是取消消息的发送。

这可靠消息服务其实就类似于等下要提到的事务消息,这个方案等于糅合了事务消息和 TCC。

TCC 小结

可以看到 TCC 是通过业务代码来实现事务的提交和回滚,对业务的侵入较大,它是业务层面的两阶段提交,

它的性能比 2PC 要高,因为不会有资源的阻塞,并且适用范围也大于 2PC,在实现上要注意上面提到的几个注意点。

它是业界比较常用的分布式事务实现方式,而且从变体也可以得知,还是得看业务变通的,不是说你要用 TCC 一定就得死板的让所有的服务都改造成那三个方法。

本地消息表

本地消息就是利用了本地事务,会在数据库中存放一直本地事务消息表,在进行本地事务操作中加入了本地消息的插入,即将业务的执行和将消息放入消息表中的操作放在同一个事务中提交

这样本地事务执行成功的话,消息肯定也插入成功,然后再调用其他服务,如果调用成功就修改这条本地消息的状态。

如果失败也不要紧,会有一个后台线程扫描,发现这些状态的消息,会一直调用相应的服务,一般会设置重试的次数,如果一直不行则特殊记录,待人工介入处理。

可以看到还是很简单的,也是一种最大努力通知思想。

你们想看的分布式事务,三歪搞来了。

事务消息

这个其实我写过一篇文章,专门讲事务消息,从源码层面剖析了 RocketMQ 、Kafka 的事务消息实现,以及两者之间的区别。

在这里我不再详细阐述,因为之前的文章写的很详细了,大概四五千字吧。我就附上链接了:

Seata 的实现

首先什么是 Seata ,摘抄官网的一段话。

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

可以看到提供了很多模式,我们先来看看 AT 模式。

AT模式

AT 模式就是两阶段提交,前面我们提到了两阶段提交有同步阻塞的问题,效率太低了,那 Seata 是怎么解决的呢?

AT 的一阶段直接就把事务提交了,直接释放了本地锁,这么草率直接提交的嘛?当然不是,这里和本地消息表有点类似,就是利用本地事务,执行真正的事务操作中还会插入回滚日志,然后在一个事务中提交。

这回滚日志怎么来的

通过框架代理 JDBC 的一些类,在执行 SQL 的时候解析 SQL 得到执行前的数据镜像,然后执行 SQL ,再得到执行后的数据镜像,然后把这些数据组装成回滚日志。

再伴随的这个本地事务的提交把回滚日志也插入到数据库的 UNDO_LOG 表中(所以数据库需要有一张UNDO_LOG 表)。

这波操作下来在一阶段就可以没有后顾之忧的提交事务了。

然后一阶段如果成功,那么二阶段可以异步的删除那些回滚日志,如果一阶段失败那么可以通过回滚日志来反向补偿恢复。

这时候有细心的同学想到了,万一中间有人改了这条数据怎么办?你这镜像就不对了啊?

所以说还有个全局锁的概念,在事务提交前需要拿到全局锁(可以理解为对这条数据的锁),然后才能顺利提交本地事务。

如果一直拿不到那就需要回滚本地事务了。

官网的示例很好,我就不自己编了,以下部分内容摘抄自 Seata 官网的示例

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

tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。

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

你们想看的分布式事务,三歪搞来了。

可以看到 tx2 的修改被阻塞了,之后重试拿到全局锁之后就能提交然后释放本地锁。

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

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

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

你们想看的分布式事务,三歪搞来了。

然后 AT 模式默认全局是读未提交的隔离级别,如果应用在特定场景下,必需要求全局的读已提交 ,可以通过 SELECT FOR UPDATE 语句的代理。

当然前提是你本地事务隔离级别是读已提交及以上。

AT 模式小结

可以看到通过代理来无侵入的得到数据的前后镜像,组装成回滚日志伴随本地事务一起提交,解决了两阶段的同步阻塞问题。

并且利用全局锁来实现写隔离。

为了总体性能的考虑,默认是读未提交隔离级别,只代理了 SELECT FOR UPDATE 来进行读已提交的隔离。

这其实就是两阶段提交的变体实现

TCC 模式

没什么花头,就是咱们上面分析的需要搞三个方法, 然后把自定义的分支事务纳入到全局事务的管理中

我贴一张官网的图应该挺清晰了。

你们想看的分布式事务,三歪搞来了。

Saga 模式

这个 Saga 是 Seata 提供的长事务解决方案,适用于业务流程多且长的情况下,这种情况如果要实现一般的 TCC 啥的可能得嵌套多个事务了。

并且有些系统无法提供 TCC 这三种接口,比如老项目或者别人公司的,所以就搞了个 Saga 模式,这个 Saga 是在 1987 年 Hector & Kenneth 发表的论⽂中提出的。

那 Saga 如何做呢?来看下这个图。

你们想看的分布式事务,三歪搞来了。

假设有 N 个操作,直接从 T1 开始就是直接执行提交事务,然后再执行 T2,可以看到就是无锁的直接提交,到 T3 发现执行失败了,然后就进入 Compenstaing 阶段,开始一个一个倒回补偿了。

思想就是一开始蒙着头干,别怂,出了问题咱们再一个一个改回去呗。

可以看到这种情况是不保证事务的隔离性的,并且 Saga 也有 TCC 的一样的注意点,需要空补偿,防悬挂和幂等。

而且极端情况下会因为数据被改变了导致无法回滚的情况。比如第一步给我打了 2 万块钱,我给取出来花了,这时候你回滚,我账上余额已经 0 了,你说怎么办嘛?难道给我还搞负的不成?

这种情况只能在业务流程上入手,我写代码其实一直是这样写的,就拿买皮肤的场景来说,我都是先扣钱再给皮肤。

假设先给皮肤扣钱失败了不就白给了嘛?这钱你来补啊?你觉得用户会来反馈说皮肤给了钱没扣嘛?

可能有小机灵鬼说我到时候把皮肤给改回去,嘿嘿这种事情确实发生过,啧啧,被骂的真惨。

所以正确的流程应该是先扣钱再给皮肤,钱到自己袋里先,皮肤没给成功用户自然而然会找过来,这时候再给他呗,虽说可能你写出了个 BUG ,但是还好不是个白给的 BUG。

所以说这点在编码的时候还是得注意下的。

你们想看的分布式事务,三歪搞来了。

最后

可以看到分布式事务还是会有各种问题,一般分布式事务的实现还是只能达到最终一致性。

极端情况下还是得人工介入,所以做好日志记录很关键。

还有编码的业务流程,要往利于公司的方向写,就例如先拿到用户的钱,再给用户东西这个方向,切记。

在上分布式事务之前想想,有没有必要,能不能改造一下避免分布式事务?

再极端一点,你的业务有没有必要上事务?


原创电子书

你们想看的分布式事务,三歪搞来了。

原创思维导图

你们想看的分布式事务,三歪搞来了。

扫码或微信搜 Java3y 回复「888」领取1000+原创电子书和思维导图。

你们想看的分布式事务,三歪搞来了。

你们想看的分布式事务,三歪搞来了。

以上是关于最后的分布式事务 有用的主要内容,如果未能解决你的问题,请参考以下文章

你们想看的分布式事务,三歪搞来了。

两天,我把分布式事务搞完了

花了整整三天,我把分布式事务搞完了

分布式事务管理

使用Atomikos实现JTA分布式事务

分布式柔性事务之最大努力通知事务详解