持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第19天,点击查看活动详情
前言
在前面的博客中我们已经学会了如何在Spring Cloud项目中集成Seata TCC模式,在这个项目中,我们创建的TCC Action需要传入一个BusinessActionContext对象参数,但是我们在Service调用TCC Action时,传入的是一个null,那么我们的BusinessActionContext对象是怎么创建出来的,在Seata中又是如何传递的呢?
创建BusinessActionContext对象
因为我们的TCC Action是通过拦截器原理进行了一层包装,如果被Service调用,最终会进入ActionInterceptorHandler.proceed():
public Object proceed(Method method, Object[] arguments, String xid, TwoPhaseBusinessAction businessAction,
Callback<Object> targetCallback) throws Throwable {
// 从参数中获取BusinessActionContext,如果没有就创建一个BusinessActionContext对象
BusinessActionContext actionContext = getOrCreateActionContextAndResetToArguments(method.getParameterTypes(), arguments);
......
}
所以,最关键的代码在getOrCreateActionContextAndResetToArguments()方法中:
protected BusinessActionContext getOrCreateActionContextAndResetToArguments(Class<?>[] parameterTypes, Object[] arguments) {
BusinessActionContext actionContext = null;
// 从参数中获取BusinessActionContext对象
int argIndex = 0;
for (Class<?> parameterType : parameterTypes) {
if (BusinessActionContext.class.isAssignableFrom(parameterType)) {
actionContext = (BusinessActionContext)arguments[argIndex];
if (actionContext == null) {
// 如果从参数中拿到的是一个BusinessActionContext类型的参数,但是是一个null,那么就创建一个BusinessActionContext对象
actionContext = new BusinessActionContext();
arguments[argIndex] = actionContext;
} else {
// 重置updated的值,避免不必要的上报,updated=true会触发分支上报
actionContext.setUpdated(null);
}
break;
}
argIndex++;
}
// 如果参数列表中没有BusinessActionContext类型参数,那么Seata自己创建一个BusinessActionContext对象
if (actionContext == null) {
actionContext = new BusinessActionContext();
}
return actionContext;
}
1.Seata会遍历所有的参数类型,如果没有找到
BusinessActionContext类型参数,Seata会自己创建一个BusinessActionContext对象;2.如果在参数列表中找到了
BusinessActionContext类型参数,但是该参数值为null,那么Seata会给该参数创建一个BusinessActionContext对象;3.如果找到的
BusinessActionContext类型参数值不为null,那么Seata会重置其中的updated参数值为null,以避免不必要的上报;
也就是说,我们在资源预留的方法中,其实是可以不写BusinessActionContext类型的参数的,这样在Service中也就不会很奇怪地传递null了;另外,如果有需要的话,我们自己也可以通过new BusinessActionContext()方式自己创建BusinessActionContext对象;
所以,通过上述源码解析后可得出以下结论:
- 如果在Service调用
TCC Action预留资源的时候不需要使用到BusinessActionContext对象,那么我们就可以不在参数列表中写上BusinessActionContext类型参数; - 如果我们需要使用到
BusinessActionContext参数,那么在Service传递参数时,可以通过new BusinessActionContext()方式创建,或者直接传递null;
BusinessActionContext如何保存
在Seata TCC模式中,如果所有的一阶段全部成功后,就需要执行二阶段的提交逻辑,在提交逻辑中,可以拿到一个BusinessActionContext对象;同样的,在二阶段的回滚逻辑中,也可以拿到一个BusinessActionContext对象;因为一阶段和二阶段并不是同一个线程连续执行的,那么这个BusinessActionContext对象是如何保存的呢?
我们可以查看ActionInterceptorHandler.doTccActionLogStore()方法:
protected String doTccActionLogStore(Method method, Object[] arguments, TwoPhaseBusinessAction businessAction,
BusinessActionContext actionContext) {
......
......
// 从BusinessActionContext中取出ActionContext
Map<String, Object> originContext = actionContext.getActionContext();
if (CollectionUtils.isNotEmpty(originContext)) {
//如果有数据,就全部放到context中
originContext.putAll(context);
context = originContext;
} else {
actionContext.setActionContext(context);
}
......
// 包装context
Map<String, Object> applicationContext = Collections.singletonMap(Constants.TCC_ACTION_CONTEXT, context);
// 转换成json字符串
String applicationContextStr = JSON.toJSONString(applicationContext);
try {
// 注册分支,把json字符串一起提交给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);
}
}
1.Seata会把
BusinessActionContext对象中的数据全部转换成Map键值对;2.为了能够将Map键值对传递给TC服务,中间会进行json转换,最终变成json字符串,这样就可以在分支事务注册的时候,把
BusinessActionContext对象的数据给到TC服务保存;
结论:Seata会通过分支事务注册的时候,把BusinessActionContext对象数据通过json字符串的方式保存在TC服务中;
提交和回滚逻辑中的BusinessActionContext对象从哪儿来
BusinessActionContext对象的保存疑问解决了,那么在调用提交或回滚逻辑时,我们拿到的BusinessActionContext对象又是从哪儿来的呢?
我们通过前几篇文章的源码分析知道,提交或回滚逻辑的调用是通过TC服务发出来的指令实现的,那么我们查看一下提交或回滚逻辑的入口:
@Override
public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId,
String applicationData) throws TransactionException {
}
@Override
public BranchStatus branchRollback(BranchType branchType, String xid, long branchId, String resourceId,
String applicationData) throws TransactionException {
}
我们可以观察到,TC服务除了最重要的xid和branchId传递过来了,另外还传递了applicationData数据,这就是我们在分支注册的时候保存的BusinessActionContext对象里面的数据;我们继续沿着调用栈查看源码,最终可以找到TCCResourceManager.getBusinessActionContext()方法:
protected BusinessActionContext getBusinessActionContext(String xid, long branchId, String resourceId,
String applicationData) {
Map actionContextMap = null;
// 如果applicationData不为空
if (StringUtils.isNotBlank(applicationData)) {
// 把applicationData转换成Map对象
Map tccContext = JSON.parseObject(applicationData, Map.class);
// 取出其中Constants.TCC_ACTION_CONTEXT对应的值
actionContextMap = (Map)tccContext.get(Constants.TCC_ACTION_CONTEXT);
}
// 如果没有数据,那么创建一个空的HashMap对象
if (actionContextMap == null) {
actionContextMap = new HashMap<>(2);
}
// 通过Map对象创建BusinessActionContext对象
BusinessActionContext businessActionContext = new BusinessActionContext(
xid, String.valueOf(branchId), actionContextMap);
businessActionContext.setActionName(resourceId);
// 返回BusinessActionContext对象
return businessActionContext;
}
也就是说,Seata最终还是通过TC服务传递回来的applicationData重新创建了一个新的BusinessActionContext对象,这样的话,我们的提交或回滚逻辑中,就可以顺其自然地使用到之前在资源预留逻辑中的BusinessActionContext对象了,虽然这两个对象并不是同一个对象,但是里面保存的元数据是一致的,所以不影响我们使用;
小结
通过以上源码分析,我们可以归纳出以下几点:
1.资源预留方法中,如果不需要使用
BusinessActionContext对象,可以不添加BusinessActionContext类型参数,Seata会自己创建;2.Service在调用资源预留方法是,参数列表中有
BusinessActionContext类型参数时,既可以传null,也可以new BusinessActionContext()传递进去;3.
BusinessActionContext参数的保存是以json字符串的形式保存在TC服务中的;4.我们在提交或回滚逻辑中使用到的
BusinessActionContext对象和资源预留中使用到的BusinessActionContext对象并不是同一个对象,但是它们的元数据是一致的,所以不会影响使用效果;