seata源码解析:seata是如何支持TCC模式的?
最常应用的模式
TCC模式应该是企业应用最广的一种模式,主要分为2个阶段
prepare,锁定相关的资源,保证事务的隔离性
commit/rollback,根据全局事务的执行状态来执行分支事务的提交和回滚
TCC模式不需要进行数据源代理,因为提交和回滚操作在业务层面都已经定义好了,不需要通过数据源代理生成对应的回滚操作
当然事务的执行状态还是会通过seata server记录在global_table和branch_table表中
通过TccActionInterceptor对方法进行增强
当使用TCC模式时,我们需要在prepare方法上@TwoPhaseBusinessAction,表明这是一个分支事务,并通过@TwoPhaseBusinessAction的commitMethod属性和rollbackMethod属性指明这个分支事务对应的提交操作和回滚操作。
因此当执行被@TwoPhaseBusinessAction标注的方法时,会执行到TccActionInterceptor#invoke方法,增强逻辑交给ActionInterceptorHandler#proceed来处理
// ActionInterceptorHandler
public Map<String, Object> proceed(Method method, Object[] arguments, String xid, TwoPhaseBusinessAction businessAction,
Callback<Object> targetCallback) throws Throwable {
Map<String, Object> ret = new HashMap<>(4);
//TCC name
String actionName = businessAction.name();
BusinessActionContext actionContext = new BusinessActionContext();
actionContext.setXid(xid);
//set action name
actionContext.setActionName(actionName);
//Creating Branch Record
// 注册分支事务
String branchId = doTccActionLogStore(method, arguments, businessAction, actionContext);
actionContext.setBranchId(branchId);
//MDC put branchId
MDC.put(RootContext.MDC_KEY_BRANCH_ID, branchId);
//set the parameter whose type is BusinessActionContext
// 如果被代理的方法中有BusinessActionContext类型,则把actionContext设置进去
// 这样方法执行的时候就能拿到actionContext了
Class<?>[] types = method.getParameterTypes();
int argIndex = 0;
for (Class<?> cls : types) {
if (cls.getName().equals(BusinessActionContext.class.getName())) {
arguments[argIndex] = actionContext;
break;
}
argIndex++;
}
//the final parameters of the try method
ret.put(Constants.TCC_METHOD_ARGUMENTS, arguments);
//the final result
// 执行被代理方法,并设置结果
ret.put(Constants.TCC_METHOD_RESULT, targetCallback.execute());
return ret;
}
proceed方法的主要逻辑为
构建BusinessActionContext,并注册分支事务
如果prepare阶段的方法入参有BusinessActionContext,则把对应的值设置进去(这就是我们在调用prepare阶段的方法时,传入的BusinessActionContext为null,但实际执行时并不为null的原因)
protected String doTccActionLogStore(Method method, Object[] arguments, TwoPhaseBusinessAction businessAction,
BusinessActionContext actionContext) {
String actionName = actionContext.getActionName();
String xid = actionContext.getXid();
// 将方法中用@BusinessActionContextParameter修饰的入参名字即值,转成map返回
Map<String, Object> context = fetchActionRequestContext(method, arguments);
// 设置开始时间
context.put(Constants.ACTION_START_TIME, System.currentTimeMillis());
//init business context
// 往context设置commit和rollback的方法名
initBusinessContext(context, method, businessAction);
//Init running environment context
// 往context设置本机ip地址
initFrameworkContext(context);
actionContext.setActionContext(context);
//init applicationData
Map<String, Object> applicationContext = new HashMap<>(4);
applicationContext.put(Constants.TCC_ACTION_CONTEXT, context);
String applicationContextStr = JSON.toJSONString(applicationContext);
try {
//registry branch record
// 向tc注册分支事务
Long branchId = DefaultResourceManager.get().branchRegister(BranchType.TCC, actionName, null, xid,
applicationContextStr, null);
return String.valueOf(branchId);
} catch (Throwable t) {
String msg = String.format("TCC branch Register error, xid: %s", xid);
LOGGER.error(msg, t);
throw new FrameworkException(t, msg);
}
}
actionContext主要用来存储动作上下文的一些参数(我们在二阶段回滚或者提交的时候,用来构建方法中的BusinessActionContext参数用),以我们之前的例子为例,最终构建的actionContext如下,将actionContext存储在branch_table表中application_data字段
"{""actionContext"":{""action-start-time"":1633261303972,""money"":200,""sys::prepare"":""prepare"",""fromUserId"":""1001"",""sys::rollback"":""cancel"",""sys::commit"":""commit"",""host-name"":""192.168.97.57"",""toUserId"":""1002"",""actionName"":""prepare""}}"
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
在TCC模式下,分支事务的注册功能是由代理对象完成的,因此不能通过自调用的方式来调用prepare方法,不然会造成事务失效(和spring事务失效的原因一样哈)。
二阶段的主要逻辑我们之前已经分析过了哈,主要就是TM端的TransactionManager向TC端的TransactionManager发送相应的请求(全局事务提交/回滚),然后TC端的TransactionManager向RM端发送相应的请求(分支事务提交/回滚),RM的ResourceManager来执行分支事务的提交和回滚操作
参考博客
[1]https://blog.csdn.net/zjj2006/article/details/108959939
[2]https://zhuanlan.zhihu.com/p/271735569