前言

一般,数据库事务的隔离级别会被设置成 读已提交,已满足业务需求,这样对应在Fescar中的分支(本地)事务的隔离级别就是 读已提交,那么Fescar中对于全局事务的隔离级别又是什么呢?如果认真阅读了 分布式事务中间件Txc/Fescar-RM模块源码解读 的同学应该能推断出来:Fescar将全局事务的默认隔离定义成读未提交。对于读未提交隔离级别对业务的影响,想必大家都比较清楚,会读到脏数据,经典的就是银行转账例子,出现数据不一致的问题。而对于Fescar,如果没有采取任何其它技术手段,那会出现很严重的问题,比如:

如上图所示,问最终全局事务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函数:

上图中看出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去做执行

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

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

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

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

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

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

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

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

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

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

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

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

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

基本逻辑如下:

  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中,如下图所示。

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

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

三、总结

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

1. 全局写排它锁数据结构保存在内存中,如果服务器重启/宕机了怎么办,即TC模块的高可用方案是什么呢?

2. 一个Fescar管理的全局事务和一个非Fescar管理的本地事务之间发生锁冲突怎么办?具体问题如下图,问题是:全局事务A如何回滚?

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

参考

  1. Fescar官方介绍
  2. fescar锁设计和隔离级别的理解
  3. 姊妹篇:分布式事务中间件TXC/Fescar—RM模块源码解读


本文作者:中间件小哥

阅读原文

本文为云栖社区原创内容,未经允许不得转载。

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

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

    2019独角兽企业重金招聘Python工程师标准>>> 一般,数据库事务的隔离级别会被设置成 读已提交,已满足业务需求,这样对应在Fescar中的分支(本地)事务的隔离级别就是 读已 ...

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

    前言 一般,数据库事务的隔离级别会被设置成 读已提交,已满足业务需求,这样对应在Fescar中的分支(本地)事务的隔离级别就是 读已提交,那么Fescar中对于全局事务的隔离级别又是什么呢?如果认真阅 ...

  3. 关于开源分布式事务中间件Fescar,我们总结了开发者关心的13个问题

    开源分布式事务中间件 Fescar 自1月10日上线v0.1版本以来,受到了开发者们的极大关注(watch249,star3005,fork649,社区讨论的issue58,数据统计于1月17日14: ...

  4. 分布式事务中间件 Fescar—RM 模块源码解读

    2019独角兽企业重金招聘Python工程师标准>>> 前言 在SOA.微服务架构流行的年代,许多复杂业务上需要支持多资源占用场景,而在分布式系统中因为某个资源不足而导致其它资源占用 ...

  5. Seata阿里分布式事务中间件(一):Seata的基本介绍

    Fescar 是 阿里巴巴 开源的 分布式事务中间件,以 高效 并且对业务 0 侵入 的方式,解决 微服务 场景下面临的分布式事务问题. 什么是微服务化带来的分布式事务问题? 首先,设想一个传统的单体 ...

  6. 京东数科首次公开:强一致、高性能分布式事务中间件JDTX

    点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! 作者 | 张亮 来源 | http://1t.click/aJPE 在分布式数据库.云原生数 ...

  7. 首次公开:京东数科强一致、高性能分布式事务中间件 JDTX

    来源:https://www.infoq.cn/article/BAXzcfjRTcgmKisa7JHm 在分布式数据库.云原生数据库.NewSQL 等名词在数据库领域层出不穷的当今,变革--在这个相 ...

  8. 分布式事务中间件你知道哪些?

    在分布式数据库.云原生数据库.NewSQL 等名词在数据库领域层出不穷的当今,变革--在这个相对稳定的领域已愈加不可避免.相比于完全革新,渐进式增强的方案在拥有厚重沉淀的行业则更受青睐. 同所有分布式 ...

  9. 分布式事务中间件Seata的安装

    本文来说下分布式事务中间件seata 文章目录 seata下载 修改file.conf的配置 修改registry.conf的配置 启动Seata seata下载 Seata是一个分布式事务中间件,使 ...

最新文章

  1. 【AMAD】import-string -- 通过字符串来import一个对象
  2. NLP-基础知识-001
  3. 深度学习应用实战案例-员工流失预测模型(Python源代码)
  4. 如何共享自己的mysql
  5. PHP Curl多线程原理实例详解
  6. 文本处理三剑客之 awk
  7. 【转】路由转发过程的IP及MAC地址变化
  8. Java番外篇1——正则表达式
  9. 有哪些开源的 Python 库让你相见恨晚?
  10. 半解TextBox灵异事件背后神秘的深度灵异事件
  11. 如何在Mac上禁用iCloud驱动器?
  12. Core Animation基础 1
  13. win10杜比全景声评测_Win10安装杜比全景声音效教程
  14. cpu性能测试软件 国际象棋,国际象棋测试
  15. html中画分割线的代码,各种分割线Html代码
  16. 数据结构——背包问题
  17. 京东图书详情页定价获取
  18. 搜狗输入法低版本导致谷歌浏览器上传下载时崩溃
  19. abp 部署到ubuntu_centos和ubuntu系列总结 - 白色的番茄
  20. java如何将mp4写入光盘_iOS - 读取/写入mp4视频的XMP元数据

热门文章

  1. mysql raiserror_RAISERROR在SQL Server数据库中的用法
  2. java 多项式拟合最多的项数_牛顿插值法、曲线拟合、多项式拟合
  3. python大神的成长之路_Python大神成长之路: 第二次学习记录
  4. python 队列 一次取多个_Queue 队列模块-Python成为专业人士笔记
  5. matlab常用代码总结
  6. for循环里radio多选_Max里的for循环
  7. 一些java面试高频题
  8. jsoup 标准化html代码,Jsoup从元素抽取属性,文本和HTML
  9. kaggle账号_机器学习竞赛入门--kaggle篇
  10. android多功能计算器 源码,Android计算器源码