第一章 分布式事务介绍
一、什么是分布式事务
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
举个栗子:
电商系统中的订单系统与库存系统
图中包含了库存和订单两个独立的微服务,每个微服务维护了自己的数据库。
在交易系统的业务逻辑中,一个商品在下单之前需要先调用库存服务,进行扣除库存,再调用订单服务,创建订单记录。
正常情况下,两个数据库各自更新成功,两边数据维持着一致性。如果在非正常情况下,有可能库存的扣减完成了,随后的订单记录却因为某些原因插入失败。或者是订单创建成功了,但是库存扣除商品的数据量失败了,这个时候,两边数据就失去了应有的一致性。
这时候我们需要保证分布式事务的一致性,单数据源的用单机事务来保证。多数据源就需要依赖分布式事务来处理。
二、XA 的两阶段提交方案
-
什么是XA 协议
XA 协议由Oracle Tuxedo 首先提出的,并交给X/Open组织,作为资源管理器(数据库)与事务管理器的接口标准。目前,Oracle、Informix、DB2 和Sybase 等各大数据库厂家都提供对XA 的支持。XA 协议采用两阶段提交方式来管理分布式事务。XA 接口提供资源管理器与事务管理器之间进行通信的标准接口。
XA 就是X/Open DTP定义的交易中间件与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。XA 接口函数由数据库厂商提供。
X/Open 组织(即现在的Open Group)定义了分布式事务处理模型。X/Open DTP 模型(1994)包括:
应用程序(AP)、
事务管理器(TM)、
资源管理器(RM)、
通信资源管理器(CRM)。一般常见的事务管理器(TM)是交易中间件,常见的资源管理器(RM)是数据库,常见的通信资源管理器(CRM)是消息中间件。
-
XA 协议的一阶段提交
一阶段提交协议相对简单。优点也很直观,它不用再与其他的对象交互,节省了判断步骤和时间,所以在性能上是在阶段提交协议中最好的。
但缺点也很明显:数据库确认执行事务的时间较长,出问题的可能性就随之增大。如果有多个数据源,一阶段提交协议无法协调他们之间的关系。
如果在程序中开启了事务,那么在应用程序发出提交/回滚请求后,数据库执行操作,而后将成功/失败返回给 应用程序,程序继续执行。 -
XA 协议的二阶段提交
二阶段三角色: 在一阶段协议的基础上,有了二阶段协议,二阶段协议的好处是添加了一个管理者角色。
很明显,二阶段协议通过将两层变为三层,增加了中间的管理者角色,从而协调多个数据源之间的关系,二阶段提交协议分为两个阶段。
应用程序调用了事务管理器的提交方法,此后第一阶段分为两个步骤:
事务管理器通知参与该事务的各个资源管理器,通知他们开始准备事务。
资源管理器接收到消息后开始准备阶段,写好事务日志并执行事务,但不提交,
然后将是否就绪的消息返回给事务管理器(此时已经将事务的大部分事情做完,以后的内容耗时极小)。
第二阶段也分为两个步骤:
a. 事务管理器在接受各个消息后,开始分析,如果有任意其一失败,则发送回滚命令,否则发送提交命令。
b. 各个资源管理器接收到命令后,执行(耗时很少),并将提交消息返回给事务管理器。
事务管理器接受消息后,事务结束,应用程序继续执行。
为什么要分两步执行?
一是因为分两步,就有了事务管理器统一管理的机会;
二是尽可能晚地提交事务,让事务在提交前尽可能地完成所有能完成的工作,
这样,最后的提交阶段将是耗时极短,耗时极短意味着操作失败的可能性也就降低。
同时,二阶段提交协议为了保证事务的一致性,不管是事务管理器还是各个资源管理器,
每执行一步操作,都会记录日志,为出现故障后的恢复准备依据。
缺点:
-
二阶段提交协议的存在的弊端是阻塞,因为事务管理器要收集各个资源管理器的响应消息,
如果其中一个或多个一直不返回消息,则事务管理器一直等待,应用程序也被阻塞,甚至可能永久阻塞。 -
两阶段提交理论的一个广泛工业应用是XA 协议。目前几乎所有收费的商业数据库都支持XA 协议。 XA
协议已在业界成熟运行数十年,但目前它在互联网海量流量的应用场景中,吞吐量这个瓶颈变得十分致命,因此很少被用到。
三、TCC 解决方案
-
TCC 介绍
TCC 是由支付宝架构师提供的一种柔性解决分布式事务解决方案,主要包括三个步骤
Try:预留业务资源/数据效验
Confirm:确认执行业务操作
Cancel:取消执行业务操作
幂等性: 在进行事务提交时 ,将多次操作合并成为一次并提交 -
TCC 原理 TCC 方案在电商、金融领域落地较多。TCC 方案其实是两阶段提交的一种改进。
其将整个业务逻辑的每个分支显式的分成了Try、Confirm、Cancel 三个操作. Try 部分完成业务的准备工作,confirm
部分完成业务的提交,cancel 部分完成事务的回滚。 基本原理如下图所示。
事务开始时,业务应用会向事务协调器注册启动事务。 之后业务应用会调用所有服务的try 接口,完成一阶段准备。 之后事务协调器会根据try
接口返回情况,决定调用confirm接口或者cancel 接口。 如果接口调用失败,会进行重试。微服务倡导服务的轻量化、易部署,
而TCC 方案中很多事务的处理逻辑需要应用自己编码实现,复杂且开发量大 -
TCC 的关键流程如下图(以创建订单和扣减库存为例子)
-
TCC 优缺点
优点
让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。缺点
对应用的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作, 应用侵入性较强,改造成本高。实现难度较大。
需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。 为了满足一致性的要求,confirm 和cancel 接口必须实现幂等。
四、分布式事务中间件解决方案
分布式事务中间件其本身并不创建事务,而是基于对本地事务的协调从而达到事务一致性的效果。
典型代表有:阿里的GTS(https://www.aliyun.com/aliware/txc)、开源应用LCN。
其实现原理如下:
第二章 LCN分布式事务处理框架介绍
一、什么是LCN 框架
-
LCN 框架的由来
在设计框架之初的1.0 ~ 2.0 的版本时,框架设计的步骤是如下的,各取其首字母得来的LCN 命名。
LCN: 锁定事务单元(lock)、确认事务模块状态(confirm)、通知事务(notify)
它的宗旨 : LCN 并不生产事务,LCN 只是本地事务的协调工 -
LCN 框架相关资料( 学习还是得靠自己钻研呀~~~ )
tx-lcn 官方地址:https://www.codingapi.com/
tx-lcn Github 地址:https://github.com/codingapi/tx-lcn
tx-lcn 官方文档地址:https://www.codingapi.com/docs/txlcn-preface/
二、LCN 框架原理及执行步骤
-
LCN 的执行原理
在上图中,微服务A,微服务B,TxManager 事务协调器,都需要去Eureka 中注册服务。
Eureka 是用于TxManager 与其他服务之间的相互服务发现。
redis 是用于存放我们事务组的信息以及补偿的信息。
然后微服务A 与微服务B 他们都需要去配置上我们TxClient 的包架构(代码的包架构);
来支持我们的LCN 框架,以及他们的数据库。 -
LCN 执行步骤
创建事务组
事务组是指的我们在整个事务过程中把各个节点(微服务)单元的事务信息存储在一个固定单元里。
但这个信息并不是代表是事务信息,而是只是作为一个模块的标示信息。
创建事务组是指在事务发起方开始执行业务代码之前先调用TxManager 创建事务组对象,
然后拿到事务标示GroupId 的过程。添加事务组
添加事务组是指参与方在执行完业务方法以后,将该模块的事务信息添加通知给TxManager 的操作。关闭事务组
是指在发起方执行完业务代码以后,将发起方执行结果状态通知给TxManager 的动作。
当执行完关闭事务组的方法以后,TxManager 将根据事务组信息来通知相应的参与模块提交或回滚事务。 -
业务执行流程图
模式一:lcn
LCN模式是通过代理Connection的方式实现对本地事务的操作,然后在由TxManager统一协调控制事务。当本地事务提交回滚或者关闭连接时将会执行假操作,该代理的连接将由LCN连接池管理。
缺点:
- 该模式对代码的嵌入性为低。 该模式仅限于本地存在连接对象且可通过连接对象控制事务的模块。
- 该模式下的事务提交与回滚是由本地事务方控制,对于数据一致性上有较高的保障。
- 该模式缺陷在于代理的连接需要随事务发起方一共释放连接,增加了连接占用的时间。
模式二:tcc
TCC事务机制相对于传统事务机制(X/Open XA Two-Phase-Commit),其特征在于它不依赖资源管理器(RM)对XA的支持,而是通过对(由业务系统提供的)业务逻辑的调度来实现分布式事务。主要由三步操作,Try: 尝试执行业务、 Confirm:确认执行业务、 Cancel: 取消执行业务。
缺点:
- 该模式对代码的嵌入性高,要求每个业务需要写三种步骤的操作。
- 该模式对有无本地事务控制都可以支持使用面广。
- 数据一致性控制几乎完全由开发者控制,对业务开发难度要求高。
模式三:txc
TXC模式命名来源于淘宝,实现原理是在执行SQL之前,先查询SQL的影响数据,然后保存执行的SQL快走信息和创建锁。当需要回滚的时候就采用这些记录数据回滚数据库,目前锁实现依赖redis分布式锁控制。
缺点:
- 该模式同样对代码的嵌入性低。
- 该模式仅限于对支持SQL方式的模块支持。
- 该模式由于每次执行SQL之前需要先查询影响数据,因此相比LCN模式消耗资源与时间要多。
- 该模式不会占用数据库的连接资源。
三、什么是LCN 的事务协调机制
如图:
假设服务已经执行到关闭事务组的过程,那么接下来作为一个模块执行通知给TxManager,
然后告诉他本次事务已经完成。那么如图中Txmanager 下一个动作就是通过事务组的id,
获取到本次事务组的事务信息;然后查看一下对应有那几个模块参与,
如果是有A/B/C 三个模块;那么对应的对三个模块做通知、提交、回滚。
那么提交的时候是提交给谁呢?
是提交给了我们的TxClient 模块。然后TxCliient 模块下有一个连接池,就是框架自定义的一个连接池(如图DB 连接池);这个连接池其实就是在没有通知事务之前一直占有着这次事务的连接资源,就是没有释放。
但是他在切面里面执行了close 方法。在执行close的时候。
如果需要(TxManager)分布式事务框架的连接。他被叫做“假关闭”,也就是没有关闭,
只是在执行了一次关闭方法。实际的资源是没有释放的。这个资源是掌握在LCN 的连接池里的。
当TxManager 通知提交或事务回滚的时候呢?
TxManager 会通知我们的TxClient 端。然后TxClient 会去执行相应的提交或回滚。
提交或回滚之后再去关闭连接。这就是LCN 的事务协调机制。
说白了就是代理DataSource 的机制;相当于是拦截了一下连接池,控制了连接池的事务提交。
四、LCN 的事务补偿机制
-
什么是补偿事务机制?
LCN 的补偿事务原理是模拟上次失败事务的请求,然后传递给TxClient 模块然后再次执行该次请求事务。
简单的说:lcn 事务补偿是指在服务挂机和网络抖动情况下txManager
无法通知事务单元时。(通知不到也就两种原因服务挂了和网络出问题)在这种情况下TxManager 会做一个标示;然后返回给发起方。
告诉他本次事务有存在没有通知到的情况。那么如果是接收到这个信息之后,发起方就会做一个标示,
标示本次事务是需要补偿事务的。这就是事务补偿机制。 -
为什么需要事务补偿?
事务补偿是指在执行某个业务方法时,本应该执行成功的操作却因为服务器挂机或者网络抖动等问题
导致事务没有正常提交,此种场景就需要通过补偿来完成事务,从而达到事务的一致性。 -
补偿机制的触发条件?
当执行关闭事务组步骤时,若发起方接受到失败的状态后将会把该次事务识别为待补偿事务,
然后发起方将该次事务数据异步通知给TxManager。TxManager 接受到补偿事务以后
先通知补偿回调地址,然后再根据是否开启自动补偿事务状态来补偿或保存该次切面事务数据。
第三章 LCN分布式事务框架应用
一、LCN 分布式事务框架前准备
-
需求描述
有三个服务分别为:springcloud-portal、springcloud-order、springcloud-inventory。
在springcloud-portal 服务中处理创建订单的请求, 然后分别请求springcloud-order
以及springcloud-inventory 服务。令其调用对应的接口项目 在springcloud-order
中插入一条订单数据,在springcloud-inventory中对商品的数量做更新。 -
使用技术
数据库:Mysql + Redis
开发平台:SpringCloud+SpringBoot+MyBatis -
准备数据库
创建数据库:tx-manager
数据库脚本:
CREATE TABLE `t_tx_exception` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`group_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`unit_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`mod_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`transaction_state` tinyint(4) NULL DEFAULT NULL,
`registrar` tinyint(4) NULL DEFAULT NULL,
`remark` varchar(4096) NULL DEFAULT NULL,
`ex_state` tinyint(4) NULL DEFAULT NULL COMMENT '0 未解决 1已解决',
`create_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
自己的服务端的数据库就自行设计了。
启动本地的Redis,TxManager默认配置就是连接了本地Redis。TxManager是基于Redis做统一事务控制的。
二、服务端搭建
新建一个 tx-server 在 pom.xml 中 添加一个依赖,
<dependency>
<groupId>com.codingapi.txlcn</groupId>
<artifactId>txlcn-tm</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
配置 application.properties文件
注意: tx-lcn在当前版本有个bug只能使用properties文件,使用yml文件会导致配置文件无法被加载的问题
spring.application.name=t1-service
server.port=8069
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/tx-manager?serverTimezone=UTC&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
# 记录异常记录
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto=update
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.use-generated-keys=true
# TxManager Host Ip
tx-lcn.manager.host=127.0.0.1
# 后台管理页面登录密码
tx-lcn.manager.admin-key=123456
# TxClient连接请求端口
tx-lcn.manager.port=8070
# 心跳检测时间(ms)
tx-lcn.manager.heart-time=15000
# 分布式事务执行总时间
tx-lcn.manager.dtx-time=8000
#参数延迟删除时间单位ms
tx-lcn.message.netty.attr-delay-time=10000
tx-lcn.manager.concurrent-level=128
# 开启日志
tx-lcn.logger.enabled=true
logging.level.com.codingapi=debug
#redis 主机
spring.redis.host=XXXXXXXX
#redis 端口
spring.redis.port=6379
#redis 密码
spring.redis.password=XXXXXXXX
启动类
package com.dsk.txmanger;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.codingapi.txlcn.tm.config.EnableTransactionManagerServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
@EnableTransactionManagerServer
public class TxMangerApplication {
public static void main(String[] args) {
SpringApplication.run(TxMangerApplication.class, args);
}
}
下面就可以启动了出现如下图
也可以进入后台管理页面登录地址:http://127.0.0.1:8069,密码:123456,TM配置中有写,我这边配置了2个微服务,就显示2个注册的TC。
三 、客户端搭建
我这使用的是 LCN 模式 另外两种大家可以试试
pom文件加入如下依赖:
<!--lcn事务客户端开始-->
<dependency>
<groupId>com.codingapi.txlcn</groupId>
<artifactId>txlcn-tc</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>com.codingapi.txlcn</groupId>
<artifactId>txlcn-txmsg-netty</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!--lcn事务客户端结束-->
配置文件加入如下配置:
#lcn事务管理器ip端口
tx-lcn:
client:
manager-address: 127.0.0.1:5800 #tm集群的情况下可以写多个地址,逗号隔开
# ribbon:
# loadbalancer:
# dtx:
# enabled: true #是否采用轮训
springcloud-portal(发起方 | LCN模式)
启动类配置 @EnableDistributedTransaction
@SpringBootApplication
@EnableDiscoveryClient
public class PortalApplication {
public static void main(String[] args) {
SpringApplication.run(SpringDApplication.class, args);
}
}
@Service
@Slf4j
public class DemoServiceImpl implements DemoService {
private final ClientDemoMapper demoMapper;
private final DDemoClient dDemoClient;
private final EDemoClient eDemoClient;
@Autowired
public DemoServiceImpl(ClientDemoMapper demoMapper, DDemoClient dDemoClient, EDemoClient eDemoClient) {
this.demoMapper = demoMapper;
this.dDemoClient = dDemoClient;
this.eDemoClient = eDemoClient;
}
@Override
@LcnTransaction
public String execute(String value) {
// ServiceD
String dResp = dDemoClient.rpc(value);
// ServiceE
String eResp = eDemoClient.rpc(value);
// local transaction
Demo demo = new Demo();
demo.setDemoField(value);
demo.setAppName(Transactions.APPLICATION_ID_WHEN_RUNNING); // 应用名称
demo.setCreateTime(new Date());
demo.setGroupId(DTXLocalContext.getOrNew().getGroupId()); // DTXLocal
demo.setUnitId(DTXLocalContext.getOrNew().getUnitId());
demoMapper.save(demo);
// 手动异常,DTX B回滚
// int i = 1 / 0;
return dResp + " > " + eResp + " > " + "ok-client";
}
}
springcloud-order (参与方 | LCN模式)
启动类配置
@SpringBootApplication
@EnableDiscoveryClient
@EnableDistributedTransaction
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(SpringDApplication.class, args);
}
}
service 层
@Service
@Slf4j
public class DemoServiceImpl implements DemoService {
private final DDemoMapper demoMapper;
@Autowired
public DemoServiceImpl(DDemoMapper demoMapper) {
this.demoMapper = demoMapper;
}
@Override
@LcnTransaction(propagation = DTXPropagation.SUPPORTS)
@Transactional
public String rpc(String value) {
// log.info("GroupId: {}", TracingContext.tracing().groupId());
Demo demo = new Demo();
demo.setCreateTime(new Date());
demo.setDemoField(value);
demo.setAppName(Transactions.APPLICATION_ID_WHEN_RUNNING); // 应用名称
demo.setGroupId(DTXLocalContext.getOrNew().getGroupId()); // DTXLocal
demo.setUnitId(DTXLocalContext.getOrNew().getUnitId());
demoMapper.save(demo);
// moreOperateMapper.update(new Date());
// moreOperateMapper.delete();
return "ok-d";
}
}
springcloud-inventory (参与方 | LCN模式)
启动类配置
@SpringBootApplication
@EnableDiscoveryClient
@EnableDistributedTransaction
public class Inventory Application {
public static void main(String[] args) {
SpringApplication.run(SpringDApplication.class, args);
}
}
service 层
@Service
@Slf4j
public class DemoServiceImpl implements DemoService {
private final EDemoMapper demoMapper;
private ConcurrentHashMap<String, Long> ids = new ConcurrentHashMap<>();
@Autowired
public DemoServiceImpl(EDemoMapper demoMapper) {
this.demoMapper = demoMapper;
}
@Override
@LcnTransaction(propagation = DTXPropagation.SUPPORTS)
@Transactional
public String rpc(String value) {
// log.info("GroupId: {}", TracingContext.tracing().groupId());
Demo demo = new Demo();
demo.setDemoField(value);
demo.setCreateTime(new Date());
demo.setAppName(Transactions.APPLICATION_ID_WHEN_RUNNING);
demo.setGroupId(DTXLocalContext.getOrNew().getGroupId());
demo.setUnitId(DTXLocalContext.getOrNew().getUnitId());
demoMapper.save(demo);
// ids.put(DTXLocalContext.getOrNew().getGroupId(), demo.getId());
return "ok-e";
}
public void confirmRpc(String value) {
log.info("tcc-confirm-" + DTXLocalContext.getOrNew().getGroupId());
ids.remove(DTXLocalContext.getOrNew().getGroupId());
}
public void cancelRpc(String value) {
log.info("tcc-cancel-" + DTXLocalContext.getOrNew().getGroupId());
Long kid = ids.get(DTXLocalContext.getOrNew().getGroupId());
demoMapper.deleteByKId(kid);
}
}
启动SpringCloud微服务
事务发参与 springcloud-order
事务发参与 springcloud-inventory
事务发起方 springcloud-portal
启动成功,测试结果就不展示了,这样 TX-LCN就整合完成了!