seata源码阅读笔记
- 本文没有seata的使用方法,怎么使用seata可以参考官方示例,详细的很。
- 本文基于v0.8.0版本,本文没贴代码。
- seata中的三个重要部分:
- TC:事务协调器,维护全局事务和分支事务的状态,驱动全局提交或回滚,就是seata的服务端。
- TM:事务管理器,开始全局事务,提交或回滚全局事务。
- RM:资源管理器,管理正在处理的分支事务的资源,向TC注册并报告分支事务的状态,并驱动分支事务的提交或回滚。
seata的初始化
- TC启动
- RM发送jdbcUrl、applicationId和transactionServiceGroup三个参数向TC发起注册,建立连接。入口是在
DataSourceProxy
实例化的时候 - TM发送applicationId和transactionServiceGroup两个参数向TC发起注册,建立连接。
TM-处理全局事务
全局事务发起者,就是我们加@GlobalTransactional
注解的方法,seata会代理我们的方法,通过以下步骤来完成全局事务。
- 发送全局事务开始请求到TC,TC返回xid(全局事务唯一id),将xid绑定到当前线程
- 执行我们的业务逻辑
- 业务逻辑成功,发送全局事务commit请求到TC,如果失败则重试
- 业务逻辑异常(可配置回滚规则,即哪些异常回滚),发送全局事务rollback请求到TC,如果失败则重试
- 清理资源,事务结束
源码阅读入口:io.seata.spring.annotation.GlobalTransactionalInterceptor#invoke
RM-处理分支事务
rm需要代理我们项目中的数据源,这一步需要我们修改自己的代码,如下:
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource() {
return new DruidDataSource();
}
/**
* 需要将DataSourceProxy 设置为主数据源,否则事务无法回滚
*/
@Primary
@Bean("dataSource")
public DataSource dataSource(DataSource druidDataSource) {
return new io.seata.rm.datasource.DataSourceProxy(druidDataSource);
}
代理数据源的主要目的是代理数据库连接,这样就可以控制分支事务的commit、rollback。
seate的rm-datasoure模块中有这几个代理类DataSourceProxy、ConnectionProxy、StatementProxy,通过代理jdbc中的这几个类,让我们的sql通过ExecuteTemplate来进行执行,这个类就是我们了解seata怎么控制分支事务的入口。下面看分支事务的处理步骤:
- 将xid绑定到当前数据库连接(xid怎么来的?在后面)
- 拿到sql执行前数据库表中的数据记录beforeImage(只是受影响的数据及字段,通过解析我们的sql完成)
- 执行我们的业务sql
- 拿到sql执行后数据库表中的数据记录afterImage
- 用变动数据的主键值作为lockKey,并保存到当前数据库连接中
- 将beforeImage和afterImage转换成undo log,并保存到当前数据库连接中
- 向TC发送全局锁请求(带着xid和lockKey),TC返回branchId,表示拿到了全局锁,若不成功则重试(默认30次,每次间隔10ms);这个全局锁的是为了防止多个用户同时对同一数据进行修改,造成后面通过undo_log进行回滚操作时数据出错;这个锁在PhaseTwo完成后,TC会进行释放。
- 将undoLog保存到数据库的undo_log表中
- 提交数据库事务
- 向TC报告当前分支事务commit的结果(失败或成功),若报告不成功则重试(默认5次)
- commit成功后,清理当前数据库连接绑定的xid
- commit失败后,向上抛出异常,以便让tm知道,进行全局事务的回滚
这是PhaseOne的整个过程,下面看PhaseTwo:
- tc收到tm的全局事务提交或回滚请求后,会对这个全局事务中的所有分支事务进行通知,rm在收到通知后,进行回滚或者提交的操作
- 回滚:取出数据库undo_log进行数据补偿还原,成功后删除undo_log;提交:直接删除undo_log
- 返回tc处理结果
源码阅读入口: io.seata.rm.datasource.StatementProxy#execute
XID在服务链路中的传递
dubbo
用dubbo的filter实现的,源码:io.seata.integration.dubbo.TransactionPropagationFilter
原理就是:上游在filter中把xid放到RpcContext中,下游再从RpcContext拿到xid。
RestTemplate和Feign
对RestTemplate和Feign的支持不在seata-all中,而是在spring-cloud-alibaba-seata中
源码入口:
com.alibaba.cloud.seata.rest.SeataRestTemplateAutoConfiguration
com.alibaba.cloud.seata.feign.SeataFeignClientAutoConfiguration
com.alibaba.cloud.seata.web.SeataHandlerInterceptorConfiguration
原理就是:上游通过拦截器将xid放到请求的header中,下游通过拦截器从header中拿到xid。
seata还支持很多RPC框架,如sofa-rpc、motan等。我们也可以通过类似的方法使seata支持我们自己的rpc框架。
GlobalLock注解
如果是用 GlobalLock 修饰的本地业务方法,虽然该方法并非某个全局事务下的分支事务,但是它对数据资源的操作也需要先查询全局锁,如果存在其他 Seata 全局事务正在修改,则该方法也需等待。所以,如果想要Seata 全局事务执行期间,数据库不会被其他事务修改,则该方法需要强制添加 GlobalLock 注解,来将其纳入 Seata 分布式事务的管理范围。
功能有点类似于 Spring 的 @Transactional 注解,如果你希望开启事务,那么必须添加该注解,如果你没有添加那么事务功能自然不生效,业务可能出 BUG;Seata 也一样,如果你希望某个不在全局事务下的 SQL 操作不影响 AT 分布式事务,那么必须添加 GlobalLock 注解。
TC的高可用设计
seata服务端支持zk、nacos、eureka等作为服务发现,通过数据库实现数据共享,全局事务Session信息、分支事务Session信息,全局锁信息都是放在数据库中
TCC模式
TCC和AT的不同主要体现在RM这边,TC和TM都是一样的。
TCC的RM不会代理我们的数据源了,而是由我们自己指定rollback和commit逻辑,在二阶段中,TM在发起全局事务提交回滚时,RM只需要执行我们指定的rollback和commit方法就行了。
这种模式就是我们业务代码的变动要多些,效率是要比AT模式高些。
RM一阶段源码入口:io.seata.spring.tcc.TccActionInterceptor#invoke
RM二阶段源码入口:io.seata.rm.tcc.TCCResourceManager#branchCommit#branchRollback
参考: