fescar(Seata)详解

https://zhuanlan.zhihu.com/p/56699895

前言

在SOA、微服务架构流行的年代,许多复杂业务上需要支持多资源占用场景,而在分布式系统中因为某个资源不足而导致其它资源占用回滚的系统设计一直是个难点。我所在的团队也遇到了这个问题,为解决这个问题上,团队采用的是阿里开源的分布式中间件Fescar的解决方案,并详细了解了Fescar内部的工作原理,解决在使用Fescar中间件过程中的一些疑虑的地方,也为后续团队在继续使用该中间件奠定理论基础。

目前分布式事务解决方案基本是围绕两阶段提交模式来设计的,按对业务是有侵入分为:对业务无侵入的基于XA协议的方案,但需要数据库支持XA协议并且性能较低;对业务有侵入的方案包括:TCC等。Fescar就是基于两阶段提交模式设计的,以高效且对业务零侵入的方式,解决微服务场景下面临的分布式事务问题。Fescar设计上将整体分成三个大模块,即TM、RM、TC,具体解释如下:

  1. TM(Transaction Manager):全局事务管理器,控制全局事务边界,负责全局事务开启、全局提交、全局回滚。
  2. RM(Resource Manager):资源管理器,控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
  3. TC(Transaction Coordinator):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。

 

本文将深入到Fescar的RM模块源码去介绍Fescar是如何在完成分支提交和回滚的基础上又做到零侵入,进而极大方便业务方进行业务系统开发。

一、从配置开始解读

 


上图是Fescar源码examples模块dubbo-order-service.xml内的配置,数据源采用druid的DruidDataSource,但实际jdbcTemplate执行时并不是用该数据源,而用的是Fescar对DruidDataSource的代理DataSourceProxy,所以,与RM相关的代码逻辑基本上都是从DataSourceProxy这个代理数据源开始的。

Fescar采用2PC来完成分支事务的提交与回滚,具体怎么做到的呢,下面就分别介绍Phase1、Phase2具体做了些什么。

二、Phase1—分支(本地)事务执行

Fescar将一个本地事务做为一个分布式事务分支,所以若干个分布在不同微服务中的本地事务共同组成了一个全局事务,结构如下。

 

那么,一个本地事务中SQL是如何执行呢?在Spring中,本质上都是从jdbcTemplate开始的,比如下面的SQL语句:

jdbcTemplate.update("update storage_tbl set count = count - ? where commodity_code = ?", new Object[] {count, commodityCode});

一般JdbcTemplate执行流程如下图所示:

 

由于在配置中,JdbcTemplate数据源被配置成了Fescar实现DataSourceProxy,进而控制了后续的数据库连接使用的是Fescar提供的ConnectionProxy,Statment使用的是Fescar实现的StatmentProxy,最终Fescar就顺理成章地实现了在本地事务执行前后增加所需要的逻辑,比如:完成分支事务的快照记录和分支事务执行状态的上报等等。

DataSourceProxy获取ConnectionProxy:

 

ConnectionProxy获取StatmentProxy:

 

在获取到StatmentProxy后,可以调用excute方法执行sql了

 

而真正excute实现逻辑如下:

 

  1. 首先会检查当前本地事务是否处于全局事务中,如果不处于,直接使用默认的Statment执行,避免因引入Fescar导致非全局事务中的SQL执行性能下降。
  2. 解析Sql,有缓存机制,因为有些sql解析会比较耗时,可能会导致在应用启动后刚开始的那段时间里处理全局事务中的sql执行效率降低。
  3. 对于INSERT、UPDATE、DELETE、SELECT..FOR UPDATE这四种类型的sql会专门实现的SQL执行器进行处理,其它SQL直接是默认的Statment执行。
  4. 返回执行结果,如有异常则直接抛给上层业务代码进行处理。

再来看一下关键的INSERT、UPDATE、DELETE、SELECT..FOR UPDATE这四种类型的sql如何执行的,先看一下具体类图结构:

 

为结省篇幅,选择UpdateExecutor实现源码看一下,先看入口BaseTransactionalExecutor.execute,该方法将ConnectionProxy与Xid(事务ID)进行绑定,这样后续判断当前本地事务是否处理全局事务中只需要看ConnectionProxy中Xid是否为空。

 

然后,执行AbstractDMLBaseExecutor中实现的doExecute方法

 

基本逻辑如下:

  1. 先判断是否为Auto-Commit模式
  2. 如果非Auto-Commit模式,则先查询Update前对应行记录的快照beforeImage,再执行Update语句,完成后再查询Update后对应行记录的快照afterImage,最后将beforeImage、afterImage生成UndoLog追加到Connection上下文ConnectionContext中。(注:获取beforeImage、afterImage方法在UpdateExecutor类下,一般是构造一条Select...For Update语句获取执行前后的行记录,同时会检查是否有全局锁冲突,具体可参考源码)
  3. 如果是Auto-Commit模式,先将提交模式设置成非自动Commit,再执行2中的逻辑,再执行connectionProxy.commit()方法,由于执行2过程和commit时都可能会出现全局锁冲突问题,增加了一个循环等待重试逻辑,最后将connection的模式设置成Auto-Commit模式

如果本地事务执行过程中发生异常,业务上层会接收到该异常,至于是给TM模块返回成功还是失败,由业务上层实现决定,如果返回失败,则TM裁决对全局事务进行回滚;如果本地事务执行过程未发生异常,不管是非Auto-Commit还是Auto-Commit模式,最后都会调用connectionProxy.commit()对本地事务进行提交,在这里会创建分支事务、上报分支事务的状态以及将UndoLog持久化到undo_log表中,具体代码如下图:


基本逻辑:

  1. 判断当前本地事务是否处于全局事务中(也就判断ConnectionContext中的xid是否为空)。
  2. 如果不处于全局事务中,则调用targetConnection对本地事务进行commit。
  3. 如果处于全局事务中,首先创建分支事务,再将ConnectionContext中的UndoLog写入到undo_log表中,然后调用targetConnection对本地事务进行commit,将UndoLog与业务SQL一起提交,最后上报分支事务的状态(成功 or 失败),并将ConnectionContext上下文重置。

综上所述,RM模块通过对JDBC数据源进行代理,干预业务SQL执行过程,加入了很多流程,比如业务SQL解析、业务SQL执行前后的数据快照查询并组织成UndoLog、全局锁检查、分支事务注册、UndoLog写入并随本地事务一起Commit、分支事务状态上报等。通过这种方式,Fescar真正做到了对业务代码无侵入,只需要通过简单的配置,业务方就可以轻松享受Fescar所带来的功能。Phase1整体流程引用Fescar官方图总结如下:

 

 

三、Phase2-分支事务提交或回滚

阶段2完成的是全局事物的最终提交或回滚,当全局事务中所有分支事务全部完成并且都执行成功,这时TM会发起全局事务提交,TC收到全全局事务提交消息后,会通知各分支事务进行提交;同理,当全局事务中所有分支事务全部完成并且某个分支事务失败了,TM会通知TC协调全局事务回滚,进而TC通知各分支事务进行回滚。

在业务应用启动过程中,由于引入了Fescar客户端,RmRpcClient会随应用一起启动,该RmRpcClient采用Netty实现,可以接收TC消息和向TC发送消息,因此RmRpcClient是与TC收发消息的关键模块。

public class RMClientAT {

    public static void init(String applicationId, String transactionServiceGroup) {
        RmRpcClient rmRpcClient = RmRpcClient.getInstance(applicationId, transactionServiceGroup);
        AsyncWorker asyncWorker = new AsyncWorker();
        asyncWorker.init();
        DataSourceManager.init(asyncWorker);
        rmRpcClient.setResourceManager(DataSourceManager.get());
        rmRpcClient.setClientMessageListener(new RmMessageListener(new RMHandlerAT()));
        rmRpcClient.init();
    }
}

上述代码展示是的RmRpcClient初始化过程,有三个关键类RMHandlerAT、AsyncWorker和DataSourceManager。RMHandlerAT具有了分支提交和回滚两个方法,分支提交或回滚的逻辑可以从这里开始看;AsyncWorker是一个异步Worker,主要是完成分支事务异步提交的功能,具有失败重试功能;DataSourceManager对数据源管理和维护。

下面分成两部分来讲:分支事务提交、分去事务回滚。

3.1、分支事务提交

在接收到TC发起的全局提交消息后,经RmRpcClient对通信协议的处理,再交由RMHandlerAT来完成对分支事务的提交,分支事务提交从RMHandlerAT.doBranchCommit()开始,但最后由AsyncWorker异步Worker完成,直接看AsyncWorker中的代码实现:

 

分支事务提交关键逻辑在doBranchCommits方法中:

 

该方法主要是批量删除UndoLog日志,但并未使用ConnectionProxy去执行删除SQL,可能原因是:1、完全没必要 2、考虑效率优先

同样,对于分支事务提交也引用Fescar官方一张图来结尾:

 

3.2、分支事务回滚

同样,分支事务回滚是从RMHandlerAT.doBranchRollback开始的,然后到了dataSourceManager.branchRollback,最后完成分支事务回滚逻辑的是UndoLogManager.undo方法。

@Override
    protected void RMHandlerAT:doBranchRollback(BranchRollbackRequest request, BranchRollbackResponse response) throws TransactionException {
        String xid = request.getXid();
        long branchId = request.getBranchId();
        String resourceId = request.getResourceId();
        String applicationData = request.getApplicationData();
        LOGGER.info("AT Branch rolling back: " + xid + " " + branchId + " " + resourceId);
        BranchStatus status = dataSourceManager.branchRollback(xid, branchId, resourceId, applicationData);
        response.setBranchStatus(status);
        LOGGER.info("AT Branch rollback result: " + status);
    }
    
     @Override
    public BranchStatus DataSourceManager:branchRollback(String xid, long branchId, String resourceId, String applicationData) throws TransactionException {
        DataSourceProxy dataSourceProxy = get(resourceId);
        if (dataSourceProxy == null) {
            throw new ShouldNeverHappenException();
        }
        try {
            UndoLogManager.undo(dataSourceProxy, xid, branchId);
        } catch (TransactionException te) {
            if (te.getCode() == TransactionExceptionCode.BranchRollbackFailed_Unretriable) {
                return BranchStatus.PhaseTwo_RollbackFailed_Unretryable;
            } else {
                return BranchStatus.PhaseTwo_RollbackFailed_Retryable;
            }
        }
        return BranchStatus.PhaseTwo_Rollbacked;
    }

UndoLogManager.undo方法源码如下:


从上图可以看出,整个回滚到全局事务之前状态的代码逻辑集中在如下代码中:

AbstractUndoExecutor undoExecutor = UndoExecutorFactory.getUndoExecutor(dataSourceProxy.getDbType(), sqlUndoLog);
undoExecutor.executeOn(conn);

首先通过UndoExecutorFactory获取到对应的UndoExecutor,然后再执行UndoExecutor的executeOn方法完成回滚操作。目前三种类型的UndoExecutor结构如下:

 

undoExecutor.executeOn源码如下:

 

至此,整个分支事务回滚就结束了,分支事务回滚整体时序图如下:

 

 

引入Fescar官方对分支事务回滚原理介绍图作为结尾:

 

 

综合上述,Fescar在Phase2通过UndoLog自动完成分支事务提交与回滚,在这个过程中不需要业务方做任何处理,业务方无感知,因些在该阶段对业务代码也是无侵入的。

四、总结

本文主要介绍了RM模块的相关代码,将RM模块按2PC模式分成Phase1和Phase2分别进行介绍,从Fescar源码上看,整个源码结构清晰,有利于研发人员快速学习Fescar的原理。在使用方面,只需进行简单的配置,就可以享受Fescar带来的便捷功能,对业务做到了无侵入;同时在性能方面,Fescar在分支事务提交过程中采用异步模式,减少了全局锁的占用时间,进而提升了整体性能。后续,将继续学习Fescar的其它模块(TM、TC)与全局锁的实现逻辑,并做相关总结介绍。

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Seata的高可用模式是通过TC使用db模式共享全局事务会话信息,使用非file的seata支持的第三方注册中心和配置中心来共享全局配置的方式来实现的。 Seata支持的第三方注册中心有nacos 、eureka、redis、zk、consul、etcd3、sofa、custom,支持的第三方配置中心有nacos 、apollo、zk、consul、etcd3、custom。seata官方主推的应该是nacos(毕竟是一家的),但是由于本人平常使用的注册中心一直是zk,因此首先考虑通过zk作为注册中心来实现高可用模式。 环境准备 zk环境准备 本地已安装zk的可以忽略,如果本地未安装,先在本地安装zk,具体安装自行百度。 PS: 此处如果使用的是远程zk服务器,则本地可以只下载,不安装。 数据库环境准备 1、创建数据库seata 2、执行源码(version1.2.0)script -> server -> db 下面的mysql.sql文件,建立global_table,branch_table,lock_table表。 配置信息导入zk 1、打开本地seata源码(版本1.2.0) 2、编辑script -> config-center -> config.txt文件,修改store.mode=db,修改store.db相关的数据库连接信息,其它默认即可 3、进入script -> config-center ->zk,执行 sh zk-config.sh -h 127.0.0.1 -p 2181 -z /usr/local/zookeeper-3.4.14(-z 后面的参数为你本地zk目录) 上面命令会将config.txt中的配置信息写入zk的/seata节点下。 启动tc-server 1、编辑conf下面的registry.conf文件,修改registry.type=zk,修改config.type=zk,修改registry.zk及config.zk信息,如下: 注意:config的zk配置没有cluster属性。 2、启动server,在本地seata安装目录bin目录下执行./seata-server.sh -m db (此处也可以直接编译本地源码然后启动Server模块下的Server类)。 不出意外的话,启动会报错,错误信息是从zk读取配置的时候反序列化出问题。 错误原因:序列化问题,由于使用seata自带的zk-config.sh脚本向zk写入配置信息的时候,采用的序列化方式相当于String.getBytes(),而框架读取配置的时候使用的是org.101tec包中的Zkclient客户端,反序列化使用的是该包下面的SerializableSerializer序列化类,使用的ObjectOutputStream进行反序列化,和序列化方式不一致。 该问题在1.3.0版本中解决,解决方式是seata支持序列化方式配置,支持自定义序列化方式,同时提供默认序列化实现类DefaultZkSerializer,反序列化实现为new String()。 到此处,1.2.0版本无法进行下去,由于目前1.3.0正式版本还未出,只能拉取最新的开发分支源码,本地编译打包1.3.0-SNAPSHOT版本。 后续版本切为1.3.0-SNAPSHOT(20200701),删除原zk配置信息重新导入1.3版本的config.txt文件信息。 本地源码编译后直接Idea启动Server类。启动成功。 PS:启动日志里面会有一些getConfig失败的报错,这些不用管,这些新的配置参数是1.3版本新增的,由于当前是SNAPSHOT版本,还不完善。 PS: 如果遇到getServerCharset 空指针异常,这个主要是MySQL和MySQL驱动版本不一致引起的,看https://blog.csdn.net/zcs20082015/article/details/107087589 服务启动 配置修改 简单处理,这里不再建新的模块,直接使用zhengcs-seata-storage模块作为演示。 1、修改POM,引入zkclient,修改seata版本 2、修改application.yml,将注册和配置类型改为zk 另外需要注意的是seata.tx-service-group配置参数要和zk导入的配置信息相关参数保持一致,否则会找不到server集群 启动服务 1、引入全局事务 2、启动 测试 基本功能测试 单元测试用例: 手动插入异常 执行用例: 基本功能是没问题的,更详细全面的测试这里就不介绍了,大家自行尝试。 高可用测试 上面的单机版肯定无法满足高可用,tc-server一旦宕掉,整个全局事务会无法进行回滚,同时会在seata库下面的事务表里面留下事务记录(正常处理成功后会被删除)。 seata的高可用是通过多个tc-server实例组成的集群来实现的。 启动多个tc-server实例: 通过-p参数修改启动接口,同时勾选Allow parallel run,开启多个实例。 然后启动客服端服务: 从启动日志可以看出,客户端会同时向所有几点注册TM和RM。 执行测试用例: 那,如果在数据已提交,异常退出之前把对应的tc-server节点停掉,会怎么样呢?答案是:全局事务回滚。大家自行尝试一下。 还有一种情况,如果客户端在执行过程中中断了,会怎么样? 如果客户端是单节点部署,那么: 首先,seata库下面的事务处理表里面有遗留事务处理记录,然后你会发现tc-server端日志里面会持续刷上述日志,tc-server会持续的尝试回滚该事务。最遗憾的是:哪怕客户端服务重启,也不会回滚该事务!!! 不过还好的是,这种情况毕竟是特例,如果客户端服务是集群部署,那么seata是可以正常完成事务回滚的。 结语 从上面的情况来看,起码seata对于简单的分布式事务场景的高可用支撑是没问题的,但是seata毕竟还是一个新框架,在实际的复杂的业务场景下会否出现什么问题,其实应该说出现什么问题都是可能的,这个需要实践和时间才能出真知了。 另外,seata目前没有提供控制台,没有服务集群配套的HA机制,这个不知道什么时候会出,拭目以待,seata应该会是一个很不错的分布式事务解决方案。   参考资料 https://seata.io/zh-cn/docs/ https://github.com/seata/seata ———————————————— 版权声明:本文为CSDN博主「zhengcs已被占用」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/zcs20082015/article/details/107092936
Seata 是阿里巴巴开源的一款分布式事务解决方案,它提供了高性能、易扩展、易集成的分布式事务解决方案,能够帮助我们解决分布式事务问题。 在 Spring Boot 和 Spring Cloud Alibaba 中集成 Seata 需要做以下几步操作: 1. 引入 Seata 的依赖 在 pom.xml 文件中引入 seata-all 的依赖: ```xml <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>1.4.2</version> </dependency> ``` 2. 配置 Seata 的注册中心 在 application.properties 或 application.yml 文件中配置 Seata 的注册中心地址: ```yaml seata: registry: type: nacos nacos: server-addr: localhost:8848 ``` 其中,type 表示注册中心的类型,这里设置为 nacos;nacos 表示 Nacos 注册中心的配置信息。 3. 配置 Seata 的事务组名称 在 application.properties 或 application.yml 文件中配置 Seata 的事务组名称: ```yaml seata: tx-service-group: my_tx_group ``` 其中,tx-service-group 表示事务组名称,可以任意设置。 4. 配置 Seata 的数据源代理 在 application.properties 或 application.yml 文件中配置 Seata 的数据源代理: ```yaml spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: 123456 seata: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: 123456 enable-auto-data-source-proxy: true ``` 其中,datasource 表示业务数据源的配置信息;seata.datasource 表示 Seata 数据源的配置信息;enable-auto-data-source-proxy 表示是否启用数据源代理。 5. 配置 Seata 的代理数据源 在 application.properties 或 application.yml 文件中配置 Seata 的代理数据源: ```yaml spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: 123456 seata: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: 123456 enable-auto-data-source-proxy: true ``` 其中,type 表示数据源的类型,这里使用的是阿里巴巴的 Druid 数据源。 6. 配置 Seata 的 AT 模式 在 application.properties 或 application.yml 文件中配置 Seata 的 AT 模式: ```yaml seata: service: vgroup-mapping: my_tx_group: default group-mapping: default: fescar-seata-example default-transaction-timeout: 300000 tx-service-group: my_tx_group mode: "AT" config: # AT 模式下的数据源类型 client.db-type: "mysql" server: undo: log serialization: "json" log table: "undo_log" datasource: datasource: "seataDataSource" db-type: "mysql" ``` 其中,service.vgroup-mapping 表示事务组映射关系;service.group-mapping 表示事务组和注册中心的映射关系;default-transaction-timeout 表示默认的事务超时时间;tx-service-group 表示事务组名称;mode 表示 Seata 的模式,这里使用的是 AT 模式;config.client.db-type 表示 AT 模式下的数据源类型;config.server.undo.log serialization 表示序列化方式;config.server.undo.log table 表示 undo log 表名;config.server.undo.datasource.datasource 表示 undo log 数据源名称;config.server.undo.datasource.db-type 表示 undo log 数据源类型。 7. 配置 Seata 的 TC 模式 在 application.properties 或 application.yml 文件中配置 Seata 的 TC 模式: ```yaml seata: service: vgroup-mapping: my_tx_group: default group-mapping: default: fescar-seata-example default-transaction-timeout: 300000 tx-service-group: my_tx_group mode: "TC" config: server: port: 8091 max-commit-retry-timeout: 120000 max-rollback-retry-timeout: 120000 ``` 其中,server.port 表示 TC 模式下的端口号;config.server.max-commit-retry-timeout 表示最大提交重试时间;config.server.max-rollback-retry-timeout 表示最大回滚重试时间。 以上就是在 Spring Boot 和 Spring Cloud Alibaba 中集成 Seata 的详细配置步骤。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值