Seata中的三个角色
TC
Transaction Coordinator: 事务协调者。维护全局和分支事务的状态,驱动全局事务提交或回滚。简单来说,就是seata-server端,是一个独立的服务,业务方往往并不关注。
TM
Transaction Manager: 定义全局事务的范围(开始、提交、回滚),启动时与TC建立连接
RM
Resource Manage: 管理分支事务处理的资源,启动时与TC建立连接。那资源是什么?在不同的模式中,资源有不同的含义,比如在AT模式下,资源就是一个个数据源连接;在TCC模式下,资源可以用过注解定义
当我们引入Seata客户端之后,会同时启动TM和RM
交互流程
下面以AT模式为例,介绍Seata的交互流程
假如现在有一个简单场景:有两个应用 A、B,调用链路如下: 前端 http-> A dubbo-> B ,伪代码如下:
A{
log.info("start")
B.do
log.info("end")
}
B{
DB.insert
}
从这个请求里链路中,可以看出后端的入口在A,所以此时在这个全局事务中,A是TM,即全局事务的发起者;B应用中引入了seata客户端, 对应的是全局事务中的一个分支事务,A B 之间,通过XID关联起来
- TM(A)先创建一个全局事务
GlobalTransaction - TM(A)开启全局事务: TM通过同步的方式,向TC发送一个开启全局事务的请求
GlobalBeginRequest - TC记录全局事务: TC收到TM发送过来的
GlobalBeginRequest请求,向global_table表插入一条记录(可以认为global_table表中的每条记录对应一条全局事务,在事务结束后会删除),然后向TM返回该全局事务的唯一标识XID - TM(A)执行业务逻辑: TM收到TC返回的XID,将该XID绑定到上下文中,然后执行业务逻辑,即调用DUBBO服务B。调用B的时候会将XID透传下去
- RM(B)接收请求: B接收到请求,然后执行SQL,因为B中用到了seata提供的数据源代理,所以在执行SQL之前,会先进行分支事务注册(向
branch_table表插入一条记录),除此之外,在执行业务SQL之前,会解析SQL,生成before快照和after快照,将其当作undo_log,然后将业务SQL和undo_log放在一个本地事务中提交。这里面还涉及到全局锁,这个后面再介绍,先把整体流程介绍好 - TC记录分支事务:TC收到RM(B)的分支事务请求,会向
branch_table表插入一条记录 - TM(B)发起二阶段提交: RM(B)正常执行返回后,A继续执行,A的业务逻辑执行完成之后,然后发起二阶段提交,即向TC发一个全局提交
GlobalCommitResponse请求 - TC异步提交: TC收到TM的全局提交请求,发现该全局事务下,涉及到的都是分支事务,于是直接返回返回成功,具体的提交逻辑交给后台的定时任务来处理
- 事务完成: TM收到TC的响应,发现全局事务提交完成,至此整个事务结束
上面介绍的是不报错时的逻辑,如果再中途有异常,会发生什么?
- 开启全局事务报错
这个很好处理,直接报错,根本不涉及到事务
- 在向RM发送请求之前,TM的业务逻辑报错
TM业务报错之后,TM会向TC发一个回滚请求。TC收到回滚请求,会执行回滚逻辑:根据XID去
branch_table表查找相关的分支事务,但因为此时都还没有涉及到RM的调用,所以此从branch_table表中找到的分支事务列表为空,即不需要执行什么回滚操作,此时可以任务和事务没啥关系,只是多了一次向TC发送回滚请求
- RM报错
RM报错后,会将异常反馈到TM,TM收到异常,会向TC发一个回滚请求。TC收到回滚请求,根据XID去
branch_table表查找相关的分支事务,然后向分支事务对应的RM发送回滚请求。RM收到回滚请求,执行对应的回滚逻辑,然后将反馈给TC,TC再反馈给TM,直到回滚完成
- TC单点问题
TC在启动的时候,会将IP注册到注册中心注册。客户端在启动的时候,会向每个TC节点都建立连接。在发送请求的时候,通过负载均衡算法选择性一个TC。因为全局事务和分支事务的状态都保存在DB,所以当某个TC宕机的时候,通过重试机制也将请求发送到其他的TC节点,对事务来说并没有什么影响
- TC全挂了
这种情况应该比较少出现吧。假如第一个分支事务提交成功,第二个分支事务在提交的时候,TC宕机了,如何处理?在seata中,每个全局事务都由一个失效时间,默认是一分钟,在TC中有一个定时任务负责对那些过期的事务进行回滚。所以在TC下次重启成功后,会对第一个分支事务进行回滚
- 回滚时发现脏数据
接着第5点,在TC重启成功后,对第一个分支事务进行回滚,该RM在回滚时候,发现undo_log中的
after快照和表里的值不一样了,如何处理?这时候已经产生脏数据了,没办法,人工处理
核心实现
直接介绍通过引入seata-spring-boot-starter依赖的使用方式,seata-all原理也类似,只不过在使用时,需要手动手动注入相关Bean
SeataAutoConfiguration
自动装配相关,核心代码如下:
@Bean
@DependsOn({BEAN_NAME_SPRING_APPLICATION_CONTEXT_PROVIDER, BEAN_NAME_FAILURE_HANDLER})
@ConditionalOnMissingBean(GlobalTransactionScanner.class)
public GlobalTransactionScanner globalTransactionScanner(SeataProperties seataProperties, FailureHandler failureHandler) {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Automatically configure Seata GlobalTransactionScanner");
}
return new GlobalTransactionScanner(seataProperties.getApplicationId(), seataProperties.getTxServiceGroup(), failureHandler);
}
-
BEAN_NAME_SPRING_APPLICATION_CONTEXT_PROVIDER: 这个对应的Bean是SpringApplicationContextProvider,干嘛用的呢?我们知道seata有两个关键文件,registry.conf和file.conf,其实file.conf并不是关键的,仅当回滚日志通过文件的方式存储的时候才需要用到。最关键的是registry.conf文件,该文件中定义了一些关键的配置,比如注册中心的连接信息、日志存储方式、配置中心的连接信息。也就是说,使用seata必须要有这么个文件。SpringApplicationContextProvider的作用就是让我们在springboot应用中不需要写这个配置文件,有了它之后,只需要将相关的配置定义在application.yam中或该springboot应用对应的配置中心就好了,使用起来更方便 -
BEAN_NAME_FAILURE_HANDLER: 这个对应的Bean是DefaultFailureHandlerImpl,里面有几个关键方法:onBeginFailureonCommitFailureonRollbackFailure作用看名字就知道了
3、GlobalTransactionScanner: 可以将它理解成客户端的入口类,在里面设计到 RM TM 的初始化,为关键注解 GlobalTransactional 创建代理对象
GlobalTransactionScanner
可以将它理解成客户端的入口类,在里面设计到 RM TM 的初始化,为关键注解 GlobalTransactional 创建代理对象
1.TM和RM初始化: 在afterPropertiesSet方法中,主要是是设置好NettyClient相关的配置,然后开启一个定时任务,定时和Server进行连接,核心代码如下
private void initClient() {
//init TM
TMClient.init(applicationId, txServiceGroup);
//init RM
RMClient.init(applicationId, txServiceGroup);
// 消息处理
registerSpringShutdownHook();
}
2.AbstractAutoProxyCreator: 这个类的本质是一个BeanPostProcessor,AbstractAutoProxyCreator中实现了代理创建的逻辑,核心方法就是wrapIfNecessary. wrapIfNecessary方法在生命周期函数postProcessAfterInitialization中被调用,postProcessAfterInitialization方法在Bean初始化之后被调用
3.解析GlobalTransactional注解: GlobalTransactional注解在seata中表示一个全局事务的入口。GlobalTransactionScanner用于解析GlobalTransactional注解并为其创建一个切面。GlobalTransactionScanner继承自AbstractAutoProxyCreator抽象类并重写了wrapIfNecessary方法。在该方法中,会解析GlobalTransactional注解TCC相关的注解(这里不多说明)。找到GlobalTransactional注解,然后为带有该注解的Bean创建一个Advisor,即 GlobalTransactionalInterceptor。
4.GlobalTransactionalInterceptor: 一个针对GlobalTransactional注解的拦截器。在其invoke方法中,会判断当前执行的方法是否存在GlobalTransactional注解,如果存在,则执行它的handleGlobalTransaction方法,可以把这里当作是事务发起的地方
TransactionalTemplate
在GlobalTransactionalInterceptor#handleGlobalTransaction方法,将事务相关的逻辑委托给TransactionalTemplate处理吗,大概逻辑如下:
public Object execute(TransactionalExecutor business) throws Throwable {
// 1 get transactionInfo
TransactionInfo txInfo = business.getTransactionInfo();
// 1.1 get or create a transaction
GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
// 1.2 Handle the Transaction propatation and the branchType
Propagation propagation = txInfo.getPropagation();
SuspendedResourcesHolder suspendedResourcesHolder = null;
try {
// 2. begin transaction
beginTransaction(txInfo, tx);
Object rs = null;
try {
// 3. Do Your Business
rs = business.execute();
} catch (Throwable ex) {
// 4. the needed business exception to rollback.
completeTransactionAfterThrowing(txInfo, tx, ex);
throw ex;
}
// 5. everything is fine, commit.
commitTransaction(tx);
return rs;
} finally {
//5. clear
triggerAfterCompletion();
cleanUp();
}
}
- 创建一个
DefaultGlobalTransaction: 如果当前线程上下文中存在XID,则在创建DefaultGlobalTransaction的时候绑定该XID;否则创建一个XID为空的DefaultGlobalTransaction - 开启全局事务: 向
TC发送一个GlobalBeginRequest请求,TC收到该请求,会在global_table表中插入一条记录标致该全局事务,默认失效时间60秒,然后返回一个是否开启成功的状态 - 执行业务逻辑: 这里分几种情况讨论:
普通业务逻辑(比如单纯的计算)、SQL操作、RPC调用- 普通逻辑: 和事务不相关,你单纯点执行就好了,出错了就抛异常,然后全局事务回滚
- SQL操作: 如果这时应用中使用了
AT或XA模式,则涉及到一个分支事务,这其实进入到了RM的概念。以AT模式为例,此时会解析业务SQL,然后生成before快照和after快照作为undo_log,RM然后向TC发送一个BranchRegisterRequest请求,TC收到该请求会在branch_table表中插入一条记录,代表一个分支事务。RM收到TC的相应后,会将undo_log和业务SQL作为一个本地事务提交,然后返回到TM继续往下执行 - RPC调用: 以Dubbo调用为例
A -> B。A发起RPC调用的时候,将XID透传下去,如果B中使用了AT或XA模式,则接下来的逻辑和上面类似
- 全局回滚: 执行到步骤4说明抛出异常了,这时候需要全局回滚。向
TC发送一个GlobalRollbackRequest请求,TC收到该请求后,根据XID从branch_table表中找到相关的分支事务,然后一一对这些分支事务进行回滚。如何回滚?TC向分支事务对应的RM发送一个BranchRollbackRequest请求,RM收到请求后,执行对应的分支事务回滚逻辑,然后返回给TC(这部分逻辑在AbstractRMHandler#doBranchRollback方法中,想详细了解的可以看看)。TC对所有分支事务回滚完成之后,然后返回给TM。至此,该全局事务结束 - 全局提交: 流程和
全局回滚类似,只不过全局提交在TC端是异步的。首先向向TC发送一个GlobalCommitRequest请求,TC收到该请求收,直接返回成功,然后在TC端有一个定时任务去执分支事务的二阶段提交。至此,该全局事务结束
其实和客户端相关的流程主要就这些,介绍的比较粗糙,具体的可以自己看一看,比如 数据源代理、SQL解析相关。
RegistryService
服务发现相关。这里引入服务发现主要是为了TC的高可用。目前TC的各个节点之间是没有交互的,所以为了实现TC的高可用,事务的状态信息不能保存在本地文件中,需要一个其它的组件来保存事务信息,目前提供的支持有DB和Redis。从下面可以看到,支持的注册中心类型很多
Server在启动的时候向注册中心注册自己的IPClient在启动的时候,从注册中心获取到Server列表,然后与其一一建立TCP连接Client发送请求时,根据负载均衡算法从Server列表选择一个进行发送
Configuration
配置中心相关。seata中有非常多个配置,有些作用于Client,有些作用于Server,还有一些Client和Server都能用。seata支持的配置中心类型也很多
seata版本:1.3.0
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
service.vgroupMapping.my_test_tx_group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
store.mode=file
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true
store.db.user=username
store.db.password=password
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.database=0
store.redis.password=null
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
其实这里我对配置中心的使用有点疑惑,为什么要这样设计呢?Server可以是一个独立的应用,它完全可以有自己的配置中心,和Server相关的配置,直接使用应用本身的配置中心就可以了。Client的配置一般是通用的,这时我们一般会将这些相关的配置放到一个公共的namespace里面,对于应用本身,每个应用本身就有一个自己的配置namespace,这时候只需要将Client相关的namespace关联上就可以使用了。这样客户端接入非常方便,公共配置的维护交给中间件维护就好了。
ChannelManager
连接管理相关。连接主要包括两方面: TM和TC的连接、RM和TC的连接。
1.IDENTIFIED_CHANNELS
ConcurrentMap<Channel, RpcContext> IDENTIFIED_CHANNELS = new ConcurrentHashMap<>();
不管是RM还是TM,与TC建立连接时候,都会在TC端将这份关系维护在IDENTIFIED_CHANNELS中。一个Channel对应一个RpcContext。在registerRMChannel方法中,首先基于appId和resourceIds创建一个RpcContext,然后将该RpcContext和Channel维护到IDENTIFIED_CHANNELS中
2.RM_CHANNELS
// resourceId -> applicationId -> ip -> port -> RpcContext
ConcurrentMap<String, ConcurrentMap<String, ConcurrentMap<String, ConcurrentMap<Integer, RpcContext>>>> RM_CHANNELS= new ConcurrentHashMap<>();
一个资源可能涉及到多个应用,比如数据源连接; 一个应用可能涉及到多个节点(IP); 但是一个应用节点可能涉及到多个Channel?不知道什么情况下会产生这种情况
3.TM_CHANNELS
// appId + ip -> port
ConcurrentMap<String, ConcurrentMap<Integer, RpcContext>> TM_CHANNELS = new ConcurrentHashMap<String, ConcurrentMap<>();
在RM_CHANNELS和TM_CHANNELS中,不是很清楚,为什么要维护一层 port -> RpcContext 的关系
TM与TC建立来连接
核心逻辑在ChannelManager#registerTMChannel方法中
public static void registerTMChannel(RegisterTMRequest request, Channel channel) throws IncompatibleVersionException {
// 校验TM seata版本
Version.checkVersion(request.getVersion());
// 基于 appId 创建一个 RpcContext
RpcContext rpcContext = buildChannelHolder(NettyPoolKey.TransactionRole.TMROLE, request.getVersion(),
request.getApplicationId(),
request.getTransactionServiceGroup(),
null, channel);
// 维护到 IDENTIFIED_CHANNELS 中
rpcContext.holdInIdentifiedChannels(IDENTIFIED_CHANNELS);
String clientIdentified = rpcContext.getApplicationId() + Constants.CLIENT_ID_SPLIT_CHAR
+ ChannelUtil.getClientIpFromChannel(channel);
// 维护到 TM_CHANNELS 中
TM_CHANNELS.putIfAbsent(clientIdentified, new ConcurrentHashMap<Integer, RpcContext>());
ConcurrentMap<Integer, RpcContext> clientIdentifiedMap = TM_CHANNELS.get(clientIdentified);
// 将 port -> RpcContext 的映射,设置到该RpcContext实例中,
rpcContext.holdInClientChannels(clientIdentifiedMap);
}
- 校验TM seata版本
- 基于appId创建一个 RpcContext
- 将步骤2创建的RpcContext和当前Channel维护到IDENTIFIED_CHANNELS中
- 将 port -> RpcContext 的映射,设置到该 RpcContext 实例中,在 RpcContext 中有一个属性
ConcurrentMap<Integer, RpcContext> clientTMHolderMap
RM与TC建立来连接
核心逻辑在ChannelManager#registerTMChannel方法中
public static void registerRMChannel(RegisterRMRequest resourceManagerRequest, Channel channel) throws IncompatibleVersionException {
// 校验RM seata版本
Version.checkVersion(resourceManagerRequest.getVersion());
Set<String> dbkeySet = dbKeytoSet(resourceManagerRequest.getResourceIds());
RpcContext rpcContext;
// 如果在IDENTIFIED_CHANNELS中不存在,就添加到IDENTIFIED_CHANNELS中; 如果存在,就更新该Channel对应RpcContext的资源列表
if (!IDENTIFIED_CHANNELS.containsKey(channel)) {
rpcContext = buildChannelHolder(NettyPoolKey.TransactionRole.RMROLE, resourceManagerRequest.getVersion(),
resourceManagerRequest.getApplicationId(), resourceManagerRequest.getTransactionServiceGroup(),
resourceManagerRequest.getResourceIds(), channel);
rpcContext.holdInIdentifiedChannels(IDENTIFIED_CHANNELS);
} else {
rpcContext = IDENTIFIED_CHANNELS.get(channel);
rpcContext.addResources(dbkeySet);
}
if (dbkeySet == null || dbkeySet.isEmpty()) { return; }
// 更新 RM_CHANNELS
for (String resourceId : dbkeySet) {
String clientIp;
ConcurrentMap<Integer, RpcContext> portMap = RM_CHANNELS.computeIfAbsent(resourceId, resourceIdKey -> new ConcurrentHashMap<>())
.computeIfAbsent(resourceManagerRequest.getApplicationId(), applicationId -> new ConcurrentHashMap<>())
.computeIfAbsent(clientIp = ChannelUtil.getClientIpFromChannel(channel), clientIpKey -> new ConcurrentHashMap<>());
rpcContext.holdInResourceManagerChannels(resourceId, portMap);
// 更新RpcContext中的clientRMHolderMap
updateChannelsResource(resourceId, clientIp, resourceManagerRequest.getApplicationId());
}
}
- 校验RM seata版本
- 如果该Channel在IDENTIFIED_CHANNELS中不存在,就添加到IDENTIFIED_CHANNELS中;否则就更新该Channel对应RpcContext的资源列表
- 更新RM_CHANNELS,涉及到更新RpcContext中的clientRMHolderMap
感觉这关系看起来有点乱?