前言
在前面的博客中我们已经学会了如何在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
对象并不是同一个对象,但是它们的元数据是一致的,所以不会影响使用效果;