分布式事务中间件 Fescar - 全局写排它锁解读

本文涉及的产品
云原生网关 MSE Higress,422元/月
Serverless 应用引擎免费试用套餐包,4320000 CU,有效期3个月
任务调度 XXL-JOB 版免费试用,400 元额度,开发版规格
简介: 前言一般,数据库事务的隔离级别会被设置成 读已提交,已满足业务需求,这样对应在Fescar中的分支(本地)事务的隔离级别就是 读已提交,那么Fescar中对于全局事务的隔离级别又是什么呢?如果认真阅读了 分布式事务中间件Txc/Fescar-RM模块源码解读 的同学应该能推断出来:Fescar将全局事务的默认隔离定义成读未提交。

前言

一般,数据库事务的隔离级别会被设置成 读已提交,已满足业务需求,这样对应在Fescar中的分支(本地)事务的隔离级别就是 读已提交,那么Fescar中对于全局事务的隔离级别又是什么呢?如果认真阅读了 分布式事务中间件Txc/Fescar-RM模块源码解读 的同学应该能推断出来:Fescar将全局事务的默认隔离定义成读未提交。对于读未提交隔离级别对业务的影响,想必大家都比较清楚,会读到脏数据,经典的就是银行转账例子,出现数据不一致的问题。而对于Fescar,如果没有采取任何其它技术手段,那会出现很严重的问题,比如:
问题1
如上图所示,问最终全局事务A对资源R1应该回滚到哪种状态?很明显,如果再根据UndoLog去做回滚,就会发生严重问题:覆盖了全局事务B对资源R1的变更。那Fescar是如何解决这个问题呢?答案就是 Fescar的全局写排它锁解决方案,在全局事务A执行过程中全局事务B会因为获取不到全局锁而处于等待状态。
对于Fescar的隔离级别,引用官方的一段话来作说明:

全局事务的隔离性是建立在分支事务的本地隔离级别基础之上的。
在数据库本地隔离级别 读已提交 或以上的前提下,Fescar 设计了由事务协调器维护的 全局写排他锁,来保证事务间的 写隔离,将全局事务默认定义在 读未提交 的隔离级别上。
我们对隔离级别的共识是:绝大部分应用在 读已提交 的隔离级别下工作是没有问题的。而实际上,这当中又有绝大多数的应用场景,实际上工作在 读未提交 的隔离级别下同样没有问题。
在极端场景下,应用如果需要达到全局的 读已提交,Fescar 也提供了相应的机制来达到目的。默认,Fescar 是工作在 读未提交 的隔离级别下,保证绝大多数场景的高效性。

下面,本文将深入到源码层面对Fescar全局写排它锁实现方案进行解读。Fescar全局写排它锁实现方案在TC(Transaction Coordinator)模块维护,RM(Resource Manager)模块会在需要锁获取全局锁的地方请求TC模块以保证事务间的写隔离,下面就分成两个部分介绍:TC-全局写排它锁实现方案、RM-全局写排它锁使用

一、TC—全局写排它锁实现方案

首先看一下TC模块与外部交互的入口,下图是TC模块的main函数:
b2417761ce2022b0.png

上图中看出RpcServer处理通信协议相关逻辑,而对于TC模块真实处理器是DefaultCoordiantor,里面包含了所有TC对外暴露的功能,比如doGlobalBegin(全局事务创建)、doGlobalCommit(全局事务提交)、doGlobalRollback(全局事务回滚)、doBranchReport(分支事务状态上报)、doBranchRegister(分支事务注册)、doLockCheck(全局写排它锁校验)等,其中doBranchRegister、doLockCheck、doGlobalCommit就是全局写排它锁实现方案的入口。

/**
* 分支事务注册,在注册过程中会获取分支事务的全局锁资源
*/
@Override
protected void doBranchRegister(BranchRegisterRequest request, BranchRegisterResponse response,
                                RpcContext rpcContext) throws TransactionException {
    response.setTransactionId(request.getTransactionId());
    response.setBranchId(core.branchRegister(request.getBranchType(), request.getResourceId(), rpcContext.getClientId(),
            XID.generateXID(request.getTransactionId()), request.getLockKey()));
}
/**
* 校验全局锁能否被获取到
*/
@Override
protected void doLockCheck(GlobalLockQueryRequest request, GlobalLockQueryResponse response, RpcContext rpcContext)
    throws TransactionException {
    response.setLockable(core.lockQuery(request.getBranchType(), request.getResourceId(),
        XID.generateXID(request.getTransactionId()), request.getLockKey()));
}
/**
* 全局事务提交,会将全局事务下的所有分支事务的锁占用记录释放
*/
@Override
protected void doGlobalCommit(GlobalCommitRequest request, GlobalCommitResponse response, RpcContext rpcContext)
throws TransactionException {
   response.setGlobalStatus(core.commit(XID.generateXID(request.getTransactionId())));
}

上述代码逻辑最后会被代理到DefualtCore去做执行
b3367eafb3042ad8.png
如上图,不管是获取锁还是校验锁状态逻辑,最终都会被LockManger所接管,而LockManager的逻辑由DefaultLockManagerImpl实现,所有与全局写排它锁的设计都在DefaultLockManagerImpl中维护。
首先,就先来看一下全局写排它锁的结构:

private static final ConcurrentHashMap<String, ConcurrentHashMap<String, ConcurrentHashMap<Integer, Map<String, Long>>>> LOCK_MAP = new ConcurrentHashMap<~>();

d092b0d809aa40a9.png

整体上,锁结构采用Map进行设计,前半段采用ConcurrentHashMap,后半段采用HashMap,最终其实就是做一个锁占用标记:在某个ResourceId(数据库源ID)上某个Tabel中的某个主键对应的行记录的全局写排它锁被哪个全局事务占用。下面,我们来看一下具体获取锁的源码:
6ad82a22d9d0061a.png

如上图注释,整个acquireLock逻辑还是很清晰的,对于分支事务需要的锁资源,要么是一次性全部成功获取,要么全部失败,不存在部分成功部分失败的情况。通过上面的解释,可能会有两个疑问:

  1. 为什么锁结构前半部分采用ConcurrentHashMap,后半部分采用HashMap?

前半部分采用ConcurrentHashMap好理解:为了支持更好的并发处理;疑问的是后半部分为什么不直接采用ConcurrentHashMap,而采用HashMap呢?可能原因是因为后半部分需要去判断当前全局事务有没有占用PK对应的锁资源,是一个复合操作,即使采用ConcurrentHashMap还是避免不了要使用Synchronized加锁进行判断,还不如直接使用更轻量级的HashMap。

  1. 为什么BranchSession要存储持有的锁资源

这个比较简单,在整个锁的结构中未体现分支事务占用了哪些锁记录,这样如果全局事务提交时,分支事务怎么去释放所占用的锁资源呢?所以在BranchSession保存了分支事务占用的锁资源。

下图展示校验全局锁资源能否被获取逻辑:
7f55b599d3d47400.png

下图展示分支事务释放全局锁资源逻辑
05c55ade6f1a56c9.png

以上就是TC模块中全局写排它锁的实现原理:在分支事务注册时,RM会将当前分支事务所需要的锁资源一并传递过来,TC获取负责全局锁资源的获取(要么一次性全部成功,要么全部失败,不存在部分成功部分失败);在全局事务提交时,TC模块自动将全局事务下的所有分支事务持有的锁资源进行释放;同时,为减少全局写排它锁获取失败概率,TC模块对外暴露了校验锁资源能否被获取接口,RM模块可以在在适当位置加以校验,以减少分支事务注册时失败概率。

二、RM-全局写排它锁使用

在RM模块中,主要使用了TC模块全局锁的两个功能,一个是校验全局锁能否被获取,一个是分支事务注册去占用全局锁,全局锁释放跟RM无关,由TC模块在全局事务提交时自动释放。分支事务注册前,都会去做全局锁状态校验逻辑,以保证分支注册不会发生锁冲突。
在执行Update、Insert、Delete语句时,都会在sql执行前后生成数据快照以组织成UndoLog,而生成快照的方式基本上都是采用Select...For Update形式,RM尝试校验全局锁能否被获取的逻辑就在执行该语句的执行器中:SelectForUpdateExecutor,具体如下图:
776d52daf50a9682.png
1c32167fa57a6ae5.png

基本逻辑如下:

  1. 执行Select ... For update语句,这样本地事务就占用了数据库对应行锁,其它本地事务由于无法抢占本地数据库行锁,进而也不会去抢占全局锁。
  2. 循环掌握校验全局锁能否被获取,由于全局锁可能会被先于当前的全局事务获取,因此需要等之前的全局事务释放全局锁资源;如果这里校验能获取到全局锁,那么由于步骤1的原因,在当前本地事务结束前,其它本地事务是不会去获取全局锁的,进而保证了在当前本地事务提交前的分支事务注册不会因为全局锁冲突而失败。

注:细心的同学可能会发现,对于Update、Delete语句对应的UpdateExecutor、DeleteExecutor中会因获取beforeImage而执行Select..For Update语句,进而会去校验全局锁资源状态,而对于Insert语句对应的InsertExecutor却没有相关全局锁校验逻辑,原因可能是:因为是Insert,那么对应插入行PK是新增的,全局锁资源必定未被占用,进而在本地事务提交前的分支事务注册时对应的全局锁资源肯定是能够获取得到的。

接下来我们再来看看分支事务如何提交,对于分支事务中需要占用的全局锁资源如何生成和保存的。首先,在执行SQL完业务SQL后,会根据beforeImage和afterImage生成UndoLog,与此同时,当前本地事务所需要占用的全局锁资源标识也会一同生成,保存在ContentoionProxy的ConnectionContext中,如下图所示。
74b1f1f6b4290ec4.png
03ba58a752f42194.png

在ContentoionProxy.commit中,分支事务注册时会将ConnectionProxy中的context内保存的需要占用的全局锁标识一同传递给TC进行全局锁的获取。
51cfeed09a51e841.png

以上,就是RM模块中对全局写排它锁的使用逻辑,因在真正执行获取全局锁资源前会去循环校验全局锁资源状态,保证在实际获取锁资源时不会因为锁冲突而失败,但这样其实坏处也很明显:在锁冲突比较严重时,会增加本地事务数据库锁占用时长,进而给业务接口带来一定的性能损耗。

三、总结

本文详细介绍了Fescar为在 读未提交 隔离级别下做到 写隔离 而实现的全局写排它锁,包括TC模块内的全局写排它锁的实现原理以及RM模块内如何对全局写排它锁的使用逻辑。在了解源码过程中,笔者也遗留了两个问题:

  1. 全局写排它锁数据结构保存在内存中,如果服务器重启/宕机了怎么办,即TC模块的高可用方案是什么呢?
  2. 一个Fescar管理的全局事务和一个非Fescar管理的本地事务之间发生锁冲突怎么办?具体问题如下图,问题是:全局事务A如何回滚?
    a400aab787e614b8.png

对于问题1有待继续研究;对于问题2目前已有答案,但Fescar目前暂未实现,具体就是全局事务A回滚时会报错,全局事务A内的分支事务A1回滚时会校验afterImage与当前表中对应行数据是否一致,如果一致才允许回滚,不一致则回滚失败并报警通知对应业务方,由业务方自行处理。

参考

  1. Fescar官方介绍
  2. fescar锁设计和隔离级别的理解
  3. 姊妹篇:分布式事务中间件TXC/Fescar—RM模块源码解读
相关文章
|
4月前
|
消息中间件 存储 Kafka
分布式消息中间件设计与实现
本文深入探讨了消息中间件的核心功能实现与高并发、高可用设计。在生产者设计中,涵盖消息构造、序列化、路由策略及可靠性保障(如ACK机制)。消费者部分分析了拉取/推送模式、分区分配与消息确认机制。同时,Broker作为核心组件,负责消息路由、存储和投递,并通过索引技术实现快速检索。 高并发设计方面,重点讨论了文件存储(顺序写入、分段存储)、日志结构存储及负载均衡策略(如哈希分区、轮询分区)。为确保高可用性,文章详细解析了主从复制、故障转移机制以及同城/异地多活容灾方案。
|
4月前
|
NoSQL 调度 Redis
分布式锁—3.Redisson的公平锁
Redisson公平锁(RedissonFairLock)是一种基于Redis实现的分布式锁,确保多个线程按申请顺序获取锁,从而实现公平性。其核心机制是通过队列和有序集合管理线程的排队顺序。加锁时,线程会进入队列并等待,锁释放后,队列中的第一个线程优先获取锁。RedissonFairLock支持可重入加锁,即同一线程多次加锁不会阻塞。新旧版本在排队机制上有所不同,新版本在5分钟后才会重排队列,而旧版本在5秒后就会重排。释放锁时,Redisson会移除队列中等待超时的线程,并通知下一个排队的线程获取锁。通过这种机制,RedissonFairLock确保了锁的公平性和顺序性。
|
15天前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
1月前
|
NoSQL Redis
分布式锁设计吗,你是如何实现锁类型切换、锁策略切换基于限流的?
本方案基于自定义注解与AOP实现分布式锁,支持锁类型(如可重入锁、公平锁等)与加锁策略(如重试、抛异常等)的灵活切换,并结合Redisson实现可重入、自动续期等功能,通过LUA脚本保障原子性,兼顾扩展性与实用性。
42 0
|
2月前
|
缓存 NoSQL Java
【📕分布式锁通关指南 11】源码剖析redisson之读写锁的实现
Redisson 的 `RedissonReadWriteLock` 提供了高效的分布式读写锁实现,适用于读多写少的场景。通过 Redis 与 Lua 脚本结合,确保读锁并行、写锁互斥,以及读写之间的互斥,保障了分布式环境下的数据一致性。它支持可重入、自动过期和锁释放机制,提升了系统并发性能与资源控制能力。
63 0
|
4月前
|
NoSQL 调度 Redis
分布式锁—5.Redisson的读写锁
Redisson读写锁(RedissonReadWriteLock)是Redisson提供的一种分布式锁机制,支持读锁和写锁的互斥与并发控制。读锁允许多个线程同时获取,适用于读多写少的场景,而写锁则是独占锁,确保写操作的互斥性。Redisson通过Lua脚本实现锁的获取、释放和重入逻辑,并利用WatchDog机制自动续期锁的过期时间,防止锁因超时被误释放。 读锁的获取逻辑通过Lua脚本实现,支持读读不互斥,即多个线程可以同时获取读锁。写锁的获取逻辑则确保写写互斥和读写互斥,即同一时间只能有一个线程获取写锁,
288 17
|
4月前
|
消息中间件 存储 中间件
分布式消息中间件基础
消息中间件是一种基于异步消息传递的分布式系统通信工具,核心功能包括消息传输、存储、路由与投递,能够实现系统解耦、异步处理和流量削峰。其主要组件包括生产者、消费者、Broker、主题/队列等,支持点对点和发布-订阅两种消息模型。主流中间件如Kafka(高吞吐)、RabbitMQ(灵活路由)、RocketMQ(事务支持)各有特色,适用于不同场景。此外,中间件还涉及多种协议(AMQP、MQTT等)、可靠性传输机制(持久化、确认机制)、顺序性与重复性问题解决以及事务支持(两阶段提交、本地消息表等)。选择中间件需根据业务需求权衡性能、功能和运维成本。
|
4月前
|
监控 NoSQL Java
分布式锁—2.Redisson的可重入锁
本文主要介绍了Redisson可重入锁RedissonLock概述、可重入锁源码之创建RedissonClient实例、可重入锁源码之lua脚本加锁逻辑、可重入锁源码之WatchDog维持加锁逻辑、可重入锁源码之可重入加锁逻辑、可重入锁源码之锁的互斥阻塞逻辑、可重入锁源码之释放锁逻辑、可重入锁源码之获取锁超时与锁超时自动释放逻辑、可重入锁源码总结。
|
6月前
|
安全
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
Redisson 的看门狗机制是解决分布式锁续期问题的核心功能。当通过 `lock()` 方法加锁且未指定租约时间时,默认启用 30 秒的看门狗超时时间。其原理是在获取锁后创建一个定时任务,每隔 1/3 超时时间(默认 10 秒)通过 Lua 脚本检查锁状态并延长过期时间。续期操作异步执行,确保业务线程不被阻塞,同时仅当前持有锁的线程可成功续期。锁释放时自动清理看门狗任务,避免资源浪费。学习源码后需注意:避免使用带超时参数的加锁方法、控制业务执行时间、及时释放锁以优化性能。相比手动循环续期,Redisson 的定时任务方式更高效且安全。
381 24
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
|
6月前
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
本文深入剖析了Redisson中可重入锁的释放锁Lua脚本实现及其获取锁的两种方式(阻塞与非阻塞)。释放锁流程包括前置检查、重入计数处理、锁删除及消息发布等步骤。非阻塞获取锁(tryLock)通过有限时间等待返回布尔值,适合需快速反馈的场景;阻塞获取锁(lock)则无限等待直至成功,适用于必须获取锁的场景。两者在等待策略、返回值和中断处理上存在显著差异。本文为理解分布式锁实现提供了详实参考。
249 11
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取

热门文章

最新文章