再前不久,我写了一篇关于分布式事务中间件Fescar的解析,没过几天Fescar团队对其进行了品牌升级,取名为Seata(Simpe Extensible Autonomous Transcaction Architecture),而之前的Fescar的英文全称为Fast & EaSy Commit And Rollback。能够看见Fescar从名字上来看更加局限于Commit和Rollback,而新的品牌名字Seata旨在打造一套一站式分布式事务解决方案。更换名字以后,我对其将来的发展更有信心。java
这里先大概回忆一下Seata的整个过程模型:mysql
在以前的文章中对整个角色有个大致的介绍,在这篇文章中我将重点介绍其中的核心角色TC,也就是事务协调器。git
为何以前一直强调TC是核心呢?那由于TC这个角色就好像上帝同样,管控着云云众生的RM和TM。若是TC一旦很差使,那么RM和TM一旦出现小问题,那一定会乱的一塌糊涂。因此要想了解Seata,那么必需要了解他的TC。github
那么一个优秀的事务协调者应该具有哪些能力呢?我以为应该有如下几个:redis
下面我也将逐步阐述Seata是如何作到上面四点。sql
Seata-Server总体的模块图如上所示:数据库
首先来说讲比较基础的Discover模块,又称服务注册/发现模块。咱们将Seata-Sever启动以后,须要将本身的地址暴露给其余使用者,那么就须要咱们这个模块帮忙。缓存
这个模块有个核心接口RegistryService,如上图所示:安全
若是须要添加本身定义的服务注册/发现,那么实现这个接口便可。截止目前在社区的不断开发推进下,已经有四种服务注册/发现,分别是redis,zk,nacos,eruka。下面简单介绍下Nacos的实现:网络
step1:校验地址是否合法
step2:获取Nacos的Name实例,而后将地址注册到当前Cluster名称上面。
unregister接口相似,这里不作详解。
step1:获取当前clusterName名字
step2:判断当前cluster是否已经获取过了,若是获取过就从map中取。
step3:从Nacos拿到地址数据,将其转换成咱们所须要的。
step4:将咱们事件变更的Listener注册到Nacos
这个接口比较简单,具体分两步:
step1:将clstuer和listener添加进map中。
step2:向Nacos注册。
配置模块也是一个比较基础,比较简单的模块。咱们须要配置一些经常使用的参数好比:Netty的select线程数量,work线程数量,session容许最大为多少等等,固然这些参数再Seata中都有本身的默认设置。
一样的在Seata中也提供了一个接口Configuration,用来自定义咱们须要的获取配置的地方:
目前为止有四种方式获取Config:File(文件获取),Nacos,Apollo,ZK。再Seata中首先须要配置registry.conf,来配置conf的类型。实现conf比较简单这里就不深刻分析。
存储层的实现对于Seata是否高性能,是否可靠很是关键。 若是存储层没有实现好,那么若是发生宕机,在TC中正在进行分布式事务处理的数据将会被丢失,既然使用了分布式事务,那么其确定不能容忍丢失。若是存储层实现好了,可是其性能有很大问题,RM可能会发生频繁回滚那么其彻底没法应对高并发的场景。
在Seata中默认提供了文件方式的存储,下面咱们定义咱们存储的数据为Session,而咱们的TM创造的全局事务数据叫GloabSession,RM创造的分支事务叫BranchSession,一个GloabSession能够拥有多个BranchSession。咱们的目的就是要将这么多Session存储下来。
在FileTransactionStoreManager#writeSession代码中:
上面的代码主要分为下面几步:
咱们将数据提交到队列以后,咱们接下来须要对其进行消费,代码以下:
这里将一个WriteDataFileRunnable()提交进咱们的线程池,这个Runnable的run()方法以下:
分为下面几步:
step1: 判断是否中止,若是stopping为true则返回null。
step2:从咱们的队列中获取数据。
step3:判断future是否已经超时了,若是超时,则设置结果为false,此时咱们生产者get()方法会接触阻塞。
step4:将咱们的数据写进文件,此时数据还在pageCahce层并无刷新到磁盘,若是写成功而后根据条件判断是否进行刷盘操做。
step5:当写入数量到达必定的时候,或者写入时间到达必定的时候,须要将咱们当前的文件保存为历史文件,删除之前的历史文件,而后建立新的文件。这一步是为了防止咱们文件无限增加,大量无效数据浪费磁盘资源。
在咱们的writeDataFile中有以下代码:
step1:首先获取咱们的ByteBuffer,若是超出最大循环BufferSize就直接建立一个新的,不然就使用咱们缓存的Buffer。这一步能够很大的减小GC。
step2:而后将数据添加进入ByteBuffer。
step3:最后将ByteBuffer写入咱们的fileChannel,这里会重试三次。此时的数据还在pageCache层,受两方面的影响,OS有本身的刷新策略,可是这个业务程序不能控制,为了防止宕机等事件出现形成大量数据丢失,因此就须要业务本身控制flush。下面是flush的代码:
这里flush的条件写入必定数量或者写的时间超过必定时间,这样也会有个小问题若是是停电,那么pageCache中有可能还有数据并无被刷盘,会致使少许的数据丢失。目前还不支持同步模式,也就是每条数据都须要作刷盘操做,这样能够保证每条消息都落盘,可是性能也会受到极大的影响,固然后续会不断的演进支持。
咱们的store核心流程主要是上面几个方法,固然还有一些好比,session重建等,这些比较简单,读者能够自行阅读。
你们知道数据库实现隔离级别主要是经过锁来实现的,一样的再分布式事务框架Seata中要实现隔离级别也须要经过锁。通常在数据库中数据库的隔离级别一共有四种:读未提交,读已提交,可重复读,串行化。在Seata中能够保证写的隔离级别是已提交,而读的隔离级别通常是未提交,可是提供了达到读已提交隔离的手段。
Lock模块也就是Seata实现隔离级别的核心模块。在Lock模块中提供了一个接口用于管理咱们的锁:
其中有三个方法:
层数 | key | value |
---|---|---|
1-LOCK_MAP | resourceId(jdbcUrl) | dbLockMap |
2- dbLockMap | tableName (表名) | tableLockMap |
3- tableLockMap | PK.hashcode%Bucket (主键值的hashcode%bucket) | bucketLockMap |
4- bucketLockMap | PK | trascationId |
能够看见实际上的加锁在bucketLockMap这个map中,这里具体的加锁方法比较简单就不做详细阐述,主要是逐步的找到bucketLockMap,而后将当前trascationId塞进去,若是这个主键当前有TranscationId,那么比较是不是本身,若是不是则加锁失败。
保证Seata高性能的关键之一也是使用了Netty做为RPC框架,采用默认配置的线程模型以下图所示:
若是采用默认的基本配置那么会有一个Acceptor线程用于处理客户端的连接,会有cpu*2数量的NIO-Thread,再这个线程中不会作业务过重的事情,只会作一些速度比较快的事情,好比编解码,心跳事件,和TM注册。一些比较费时间的业务操做将会交给业务线程池,默认状况下业务线程池配置为最小线程为100,最大为500。
这里须要提一下的是Seata的心跳机制,这里是使用Netty的IdleStateHandler完成的,以下:
在Sever端对于写没有设置最大空闲时间,对于读设置了最大空闲时间,默认为15s,若是超过15s则会将连接断开,关闭资源。
step1:判断是不是读空闲的检测事件。
step2:若是是则断开连接,关闭资源。
目前官方没有公布HA-Cluster,可是经过一些其余中间件和官方的一些透露,能够将HA-Cluster用以下方式设计:
具体的流程以下:
step1:客户端发布信息的时候根据transcationId保证同一个transcation是在同一个master上,经过多个Master水平扩展,提供并发处理性能。
step2:在server端中一个master有多个slave,master中的数据近实时同步到slave上,保证当master宕机的时候,还能有其余slave顶上来能够用。
固然上述一切都是猜想,具体的设计实现还得等0.5版本以后。目前有一个Go版本的Seata-Server也捐赠给了Seata(还在流程中),其经过raft实现副本一致性,其余细节不是太清楚。
这个模块也是一个没有具体公布实现的模块,固然有可能会提供插件口,让其余第三方metric接入进来,最近Apache skywalking 正在和Seata小组商讨如何接入进来。
上面咱们讲了不少Server基础模块,想必你们对Seata的实现已经有个大概,接下来我会讲解事务协调器具体逻辑是如何实现的,让你们更加了解Seata的实现内幕。
启动方法在Server类有个main方法,定义了咱们启动流程:
step1:建立一个RpcServer,再这个里面包含了咱们网络的操做,用Netty实现了服务端。
step2:解析端口号和文件地址。
step3:初始化SessionHoler,其中最重要的重要就是重咱们dataDir这个文件夹中恢复咱们的数据,重建咱们的Session。
step4:建立一个CoorDinator,这个也是咱们事务协调器的逻辑核心代码,而后将其初始化,其内部初始化的逻辑会建立四个定时任务:
step5: 初始化UUIDGenerator这个也是咱们生成各类ID(transcationId,branchId)的基本类。
step6:将本地IP和监听端口设置到XID中,初始化rpcServer等待客户端的链接。
启动流程比较简单,下面我会介绍分布式事务框架中的常见的一些业务逻辑Seata是如何处理的。
一次分布式事务的起始点必定是开启全局事务,首先咱们看看全局事务Seata是如何实现的:
step1: 根据应用ID,事务分组,名字,超时时间建立一个GloabSession,这个再前面也提到过他和branchSession分别是什么。
step2:对其添加一个RootSessionManager用于监听一些事件,这里要说一下目前在Seata里面有四种类型的Listener(这里要说明的是全部的sessionManager都实现了SessionLifecycleListener):
step3:开启Globalsession
这一步会把状态变为Begin,记录开始时间,而且调用RootSessionManager的onBegin监听方法,将Session保存到map并写入到咱们的文件。
step4:最后返回XID,这个XID是由ip+port+transactionId组成的,很是重要,当TM申请到以后须要将这个ID传到RM中,RM经过XID来决定到底应该访问哪一台Server。
当咱们全局事务在TM开启以后,咱们RM的分支事务也须要注册到咱们的全局事务之上,这里看看是如何处理的:
step1:经过transactionId获取并校验全局事务是不是开启状态。
step2:建立一个新的分支事务,也就是咱们的BranchSession。
step3:对分支事务进行加全局锁,这里的逻辑就是使用的咱们锁模块的逻辑。
step4:添加branchSession,主要是将其添加到globalSession对象中,并写入到咱们的文件中。
step5:返回branchId,这个ID也很重要,咱们后续须要用它来回滚咱们的事务,或者对咱们分支事务状态更新。
分支事务注册以后,还须要汇报分支事务的后续状态究竟是成功仍是失败,在Server目前只是简单的作一下保存记录,汇报的目的是,就算这个分支事务失败,若是TM仍是执意要提交全局事务,那么再遍历提交分支事务的时候,这个失败的分支事务就不须要提交。
当咱们分支事务执行完成以后,就轮到咱们的TM-事务管理器来决定是提交仍是回滚,若是是提交,那么就会走到下面的逻辑:
step1:首先找到咱们的globalSession。若是他为Null证实已经被commit过了,那么直接幂等操做,返回成功。
step2:关闭咱们的GloabSession防止再次有新的branch进来。
step3:若是status是等于Begin,那么久证实尚未提交过,改变其状态为Committing也就是正在提交。
step4:判断是不是能够异步提交,目前只有AT模式能够异步提交,由于是经过Undolog的方式去作的。MT和TCC都须要走同步提交的代码。
step5:若是是异步提交,直接将其放进咱们ASYNC_COMMITTING_SESSION_MANAGER,让其再后台线程异步去作咱们的step6,若是是同步的那么直接执行咱们的step6。
step6:遍历咱们的BranchSession进行提交,若是某个分支事务失败,根据不一样的条件来判断是否进行重试,异步不须要重试,由于其自己都在manager中,只要没有成功就不会被删除会一直重试,若是是同步提交的会放进异步重试队列进行重试。
若是咱们的TM决定全局回滚,那么会走到下面的逻辑:
这个逻辑和提交流程基本一致,能够看做是他的反向,这里就不展开讲了。
最后在总结一下开始咱们提出了分布式事务的关键4点,Seata究竟是怎么解决的:
最后但愿你们能从这篇文章能了解Seata-Server的核心设计原理,固然你也能够想象若是你本身去实现一个分布式事务的Server应该怎样去设计?
seata github地址:https://github.com/seata/seata。
最后这篇文章被我收录于JGrowing-分布式事务篇,一个全面,优秀,由社区一块儿共建的Java学习路线,若是您想参与开源项目的维护,能够一块儿共建,github地址为:https://github.com/javagrowing/JGrowing 麻烦给个小星星哟。
若是你们以为这篇文章对你有帮助,你的关注和转发是对我最大的支持,O(∩_∩)O: