【SEATA源码分析】 rm-datasource 模块源码解析

一 . 导读

前几篇模块分析的时候说到,spring模块中的 GlobalTransactionScanner 实现了InitializingBean 这个接口类,重载 postProcessAfterInitialization() 方法,在这个方法中,判断被扫描的bean是否是 DataSource 类型,如果是,用 DataSourceProxy 替换。

而 @GlobalTransaction 注解的方法,在项目启动的时候,也会被 spring 模块的 GlobalTransactionScanner 扫描,并织入 interceptor ,进而被tm模块的代理。
但是 tm 模块,负责处理事务逻辑和与 TransactionCoordinate 交互,并没有和数据库打交道。beforeimage、afterimage 的生成,和业务数据的落库操作,都是由 rm-datasource 模块代理完成。

rm-datasource模块代理了 datasource、connection、pripareStatement 等 jdbc 组件,在数据库连接的获取释放和数据库语句执行等步骤前后,插入 seata 实现分布式事务所需要的处理。

二 .jdk 中 jdbc 分析

jdbc应该是java基础了,但是因为太基础,所以很少用到,现在来回忆一下。

数据库的操作主要有一下五个步骤:
1.获取数据库连接
2.拼接 prepareStatement
3.执行语句
4.解析结果
5.释放连接

一些常用的数据库连接池:DBCP、druid等,都会对数据库操作进行代理,然后支持一些特性。比如阿里的druid,就可以对连接进行LRU方式的重用,支持 PreparedStatementCache ,还可以对操作语句进行监控,相比于手写jdbc,性能会有很大提升。所以现在除了框架,业务操作基本告别了手写jdbc。

rm 模块也没有完全的代理数据库的操作,只是在项目原有执行操作的基础上,增加了分布式事务需要的操作。

三 .数据库代理分析

spring 模块,在 客户端启动时,扫描到 bean 是 DataSource 类型,会对其进行代理,生成 DataSourceProxy。DataSourceProxy 会持有被代理类 DataSource,后续数据库有关操作最终还是会执行被代理类 DataSource 中提供的,代理类只进行增强,没有减少原有数据库相关操作。下面几个 proxy 类似,都会持有一个被代理类。

DataSource类中方法,比如 getConnection()被重写,返回的是 ConnectionProxy .代理连接类。

Connection 类中方法,比如 prepareStatement(sql) 被重写,返回的是 PreparedStatementProxy 代理类。Connection 的 commit() 方法也被重载,实现了自己的逻辑。

PrepareStatementProxy 的 execute() 方法被重载,调用 ExecuteTemplate 实现事务相关操作数据库逻辑,这是一个模板方法模式,很经典。

四 .事务相关操作分析

ExecuteTemplate 中 execute() 方法封装了事务有关操作,首先判断 RootContext 中是否存在 xid,如果不存在,则执行业务操作,否则,进入下面的逻辑:
在这里插入图片描述
这里通过 sqlRecognizer 解析 sql 类型,找到对应的 executor 进行操作。

在这里插入图片描述
这是 AbstractDMLBaseExecutor 中方法,调用子类重载的 beforeImage() 和 afterImage(beforeImage) 来获取对应 sql 类型i的 table 记录,然后在执行 statementCallback.execute(),即业务类操作数据库的方法。
最后执行 prepareUndoLog(beforeImage,afterImage),把业务类数据库操作执行前后的快照做成 sqlUndoLog 缓存在 ConnectionProxy 的属性 ConnectionContext 中,等到连接提交前,进行入库操作。除了 sqlUndoLog 外还缓存了lockKey。

在这里插入图片描述
这个方法外面套了一层失败重试逻辑就不展开说了。如果是 @GlobalLock 注解的,是另一个处理方法,与这个类似。

register() 方法:

Long branchId = DefaultResourceManager.get().branchRegister(BranchType.AT, getDataSourceProxy().getResourceId(),null, context.getXid(), null, context.buildLockKeys());

这里的 LockKeys 由业务中数据库操作所影响的行的 primaryKey 拼接而成。

先注册分支记录,再判断 context 中是否有 undoLog 记录,如果有的话,插入到业务操作所在的数据库中的Undo表。提交连接,这里的 targetConnection,即原 DataSource 中 getConnection() 获取到的,seata 只是加入了一些逻辑,并没有修改原有逻辑。

连接提交后,执行 report(true) 上报本次事务的状态 ,即:BranchStatus.PhaseOne_Done,一阶段完成。否则上报 :BranchStatus.PhaseOne_Failed ,一阶段失败。

这里有一个问题,为什么 seata 在 connection 提交的时候进行 undo 日志的入库操作,而不是在执行 prepareStatement 的时候?
因为业务操作,可不只会操作一下数据库,所以在数据库语句执行的时候,只是缓存undo日志,而不进行提交操作。在业务所有的数据库操作方法执行结束,执行业务的线程释放数据库连接的时候,执行入库操作,这样只需入库一次。

一阶段的操作,至此执行完毕。二阶段,由 TransactionCoodinator 发起的 commit 和 rollback 决议。
roolback 模式下 rm 调用类:
AbstractRmHandler --> DefaultResourceManager --> DataSourceManager --> AbstractUndoLogManager :

void undo(DataSourceProxy dataSourceProxy, String xid, long branchId)

只要执行不抛异常,就返回 rollback 成功的状态。undo() 就是根据undolog回滚资源。

二阶段的 commit 操作就很简单了,就是给 DataSourceManger 的 init() 方法中实例化的 AsyncWorker 类的 LinkedBlockingQueue<> 中,增加一个任务 Phase2Context ,AsyncWorker 会定时定量的清楚需要删除的 undolog。

五 .总结

seata 的 rm 模块,主要的操作就是增强 jdbc 操作,使其执行分布式事务需要的逻辑。

对外表现如下:
1.向 TransactionCoodinator 注册分支记录。
2.向业务所在数据库中的 undo 表插入 undolog。
3.负责执行二阶段的的 rollback 和 commit 操作。

更多好文请关注微信公众号:我手一杯
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值