1 TCC原理
TCC(Try-Confirm-Cancel)将一个事务分成两阶段:
- Try阶段:尝试锁定资源
- Confirm阶段:如果Try阶段所有资源均锁定成功,那么执行Confirm阶段,真正的扣除资源。
- Cancel阶段:如果Try阶段有部分资源锁定失败,那么执行Cancel阶段,回滚Try阶段锁定的资源。
注意:除了Try阶段为主动触发外,Confirm/Cancel均有框架从自动发起。
TCC系统模型如下所示:
从微服务的调用过程上看TCC系统模型如下所示:
TCC有多种实现方式,本文仅介绍Seata的TCC模式,github上还有其他的TCC实现,例如:tcc-transaction
TCC模式和AT模式有很多相同之处,建议先了解AT模式,本文介绍原理时重点介绍其差异部分
2 TCC模式示例
TCC模式使用非常简单,这里仅摘取和实现原理相关的部分代码,并结合这些代码进行原理分析。这里的示例摘自github上seata-sample
2.1 TM系统配置
<bean class="io.seata.spring.annotation.GlobalTransactionScanner">
<constructor-arg value="tcc-sample"/>
<constructor-arg value="my_test_tx_group"/>
</bean>
GlobalTransactionScanner将会自动扫描@GlobalTransactional,并为其开启分布式事务。
使用时:只需要在需要开启全局事务的地方加上@GlobalTransactional注解即可,如下所示:
事务开启方:
@GlobalTransactional
public String doTransactionCommit(){
boolean result = tccActionOne.prepare(null, 1);
if(!result){
throw new RuntimeException("TccActionOne failed.");
}
List list = new ArrayList();
result = tccActionTwo.prepare(null, "two", list);
if(!result){
throw new RuntimeException("TccActionTwo failed.");
}
return RootContext.getXID();
}
2.2 RM提供TCC接口:
TccActionOne
public interface TccActionOne {
@TwoPhaseBusinessAction(name = "DubboTccActionOne" , commitMethod = "commit", rollbackMethod = "rollback")
public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "a") int a);
public boolean commit(BusinessActionContext actionContext);
public boolean rollback(BusinessActionContext actionContext);
}
TccActionTwo
public interface TccActionTwo {
@TwoPhaseBusinessAction(name = "DubboTccActionTwo" , commitMethod = "commit", rollbackMethod = "rollback")
public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "b") String b, @BusinessActionContextParameter(paramName = "c",index = 1) List list);
public boolean commit(BusinessActionContext actionContext);
public boolean rollback(BusinessActionContext actionContext);
}
2.3 启动seata-server
seata-server承担的是「seata-分布式事务方案」中TC的职责,即作为事务协调者,管理全局事务的生命周期。
3 Seata-TCC方案简介
3.1 Seata-TCC架构图
注:此图来自seata官网
3.2 分布式事务流程
在流程上,TCC模式在整个流程上与AT模式基本相同,如下所示:
在流程中,AT、TCC模式的主要区别在于:
- AT模式是在DB数据源层做了代理,通过拦截SQL执行,在SQL执行前向TC中register branch;TC模式是通过@TwoPhaseBusinessAction来实现的,即框架层面扫描@TwoPhaseBusinessAction注解,通过拦截器向TC中register branch。
- 在branch commit/rollback阶段,AT模式基于undo log执行数据回滚操作;TCC模式需要自己制定commit/rollback方法。
3.3 Seata-TCC原理分析
3.3.1 时序图
TCC模式的时序图和AT模式的时序图非常类似,从整体上看,仅图中用虚线框起来的部分和AT模式有差异。
3.3.2 Begin阶段
3.3.2.1 TM请求TC开启一个全局事务
此步骤和AT模式完全相同。注册成功后将xid绑定到上下文中,并传递到各个微服务中去。
3.3.2.2 TC的收到开启全局事务请求时的处理流程。
此步骤和AT模式完全相同。执行完毕后,TC中将会创建一个全局事务记录,如下所示:
[ { "xid":"192.168.1.5:8091:2612211982926405633", "transaction_id":2612211982926400000, "status":1, "application_id":"tcc-sample", "transaction_service_group":"my_test_tx_group", "transaction_name":"doTransactionCommit()", "timeout":60000, "begin_time":1618757292292, "application_data":"NULL", "gmt_create":44304.9501388889, "gmt_modified":44304.9501388889 }]
3.3.3 TM执行业务逻辑
在GlobalTransactionalInterceptor拦截器中完成begin阶段以后,就会正式执行TM中的业务逻辑代码,即示例中的:
@GlobalTransactional
public String doTransactionCommit(){
boolean result = tccActionOne.prepare(null, 1);
if(!result){
throw new RuntimeException("TccActionOne failed.");
}
List list = new ArrayList();
result = tccActionTwo.prepare(null, "two", list);
if(!result){
throw new RuntimeException("TccActionTwo failed.");
}
return RootContext.getXID();
}
这里执行业务逻辑的时候,会RPC调用其他微服务(微服务B、微服务C)的接口,RM在执行业务逻辑的过程中将进入Register Branch阶段。
3.3.4 Register Branch阶段
这个阶段和AT模式有较大的差别,AT模式是基于DataSourceProxy,在业务sql执行前注册分支事务,TCC模式是基于@TwoPhaseBusinessAction来做到注册分支事务。
3.3.4.1 RM注册向TC分支事务
系统启动的时候,会为使用@TwoPhaseBusinessAction注解的类生成代理,即TccActionInterceptor负责处理。这个拦截器很简单,主要做了这几件事情:
- 绑定分支类型:BranchType.TCC
- 通过注解获取TCC方法的上下文,以方便在Confirm/Cancel阶段直接找到对应的方法。
- 向TC注册分支事务。
如下代码所示。
TccActionInterceptor#invoke
public Object invoke(final MethodInvocation invocation) throws Throwable {
if (!RootContext.inGlobalTransaction() || disable || RootContext.inSagaBranch()) {
//not in transaction
return invocation.proceed();
}
Method method = getActionInterfaceMethod(invocation);
TwoPhaseBusinessAction businessAction = method.getAnnotation(TwoPhaseBusinessAction.class);
//try method
if (businessAction != null) {
//save the xid
String xid = RootContext.getXID();
//save the previous branchType
BranchType previousBranchType = RootContext.getBranchType();
//if not TCC, bind TCC branchType
if (BranchType.TCC != previousBranchType) {
RootContext.bindBranchType(BranchType.TCC);
}
try {
Object[] methodArgs = invocation.getArguments();
//Handler the TCC Aspect
Map<String, Object> ret = actionInterceptorHandler.proceed(method, methodArgs, xid, businessAction, invocation::proceed);
//return the final result
return ret.get(Constants.TCC_METHOD_RESULT);
}
finally {
//if not TCC, unbind branchType
if (BranchType.TCC != previousBranchType) {
RootContext.unbindBranchType();
}
//MDC remove branchId
MDC.remove(RootContext.MDC_KEY_BRANCH_ID);
}
}
return invocation.proceed();
}
ActionInterceptorHandler#proceed
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
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;
}
protected String doTccActionLogStore(Method method, Object[] arguments, TwoPhaseBusinessAction businessAction, BusinessActionContext actionContext) {
String actionName = actionContext.getActionName();
String xid = actionContext.getXid();
//
Map<String, Object> context = fetchActionRequestContext(method, arguments);
context.put(Constants.ACTION_START_TIME, System.currentTimeMillis());
//init business context
initBusinessContext(context, method, businessAction);
//Init running environment context
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
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);
}
}
protected void initBusinessContext(Map<String, Object> context, Method method,
TwoPhaseBusinessAction businessAction) {
if (method != null) {
//the phase one method name
context.put(Constants.PREPARE_METHOD, method.getName());
}
if (businessAction != null) {
//the phase two method name
context.put(Constants.COMMIT_METHOD, businessAction.commitMethod());
context.put(Constants.ROLLBACK_METHOD, businessAction.rollbackMethod());
context.put(Constants.ACTION_NAME, businessAction.name());
}
}
这个过程需要注意:TCC在此阶段会将commitMethod、rollbackMethod、请求上下文信息保存下来。
3.3.4.2 TC受理RM的Register Branch流程
这个过程和AT模式基本相同,暂不累述。分支事务注册成功时,TC中branch_table中将多出一条分支事务记录。
[ { "branch_id":2612211982926400000, "xid":"192.168.1.5:8091:2612211982926405633", "transaction_id":2612211982926400000, "resource_group_id":"NULL", "resource_id":"DubboTccActionOne", "branch_type":"TCC", "status":0, "client_id":"tcc-sample:127.0.0.1:58584", "application_data":{ "actionContext":{ "a":1, "action-start-time":1618757292549, "sys::prepare":"prepare", "sys::rollback":"rollback", "sys::commit":"commit", "host-name":"192.168.1.5", "actionName":"DubboTccActionOne" } }, "gmt_create":"2021-04-18 22:48:12.705895", "gmt_modified":"2021-04-18 22:48:12.705895" }, { "branch_id":2612211982926400000, "xid":"192.168.1.5:8091:2612211982926405633", "transaction_id":2612211982926400000, "resource_group_id":"NULL", "resource_id":"DubboTccActionTwo", "branch_type":"TCC", "status":0, "client_id":"tcc-sample:127.0.0.1:58584", "application_data":{ "actionContext":{ "b":"two", "action-start-time":1618757293189, "c":"c2", "sys::prepare":"prepare", "sys::rollback":"rollback", "sys::commit":"commit", "host-name":"192.168.1.5", "actionName":"DubboTccActionTwo" } }, "gmt_create":"2021-04-18 22:48:12.7058959", "gmt_modified":"2021-04-18 22:48:12.705895" }]
3.3.5 Global Commit/RollBack阶段
这个过程和AT模式基本相同,暂不累述。
3.3.5.1 TM通知TC进行Global Commit/Rollback阶段
这个过程和AT模式基本相同,暂不累述。
3.3.5.2 TC进行Global Commit/Rollback阶段
这个阶段的前半部分和AT模式基本一致,
3.3.6 RM进行Branch Commit/Rollback阶段
RM收到Branch Commit/Rollback请求以后,会交给AbstractRMHandler处理,接着AbstractRMHandler将请求委派给TCCResourceManager处理。
Branch Commit/Rollback阶段主要包括以下内容:
- 从缓存中获取TCCResource
- 从TCCResource获取commitMethod/rollbackMethod
- 获取请求执行的上下文
- 执行commit/rollback方法
branchCommit代码如下,rollbackMethod流程基本类似。
TCCResourceManager#branchCommit
public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId, String applicationData) throws TransactionException {
TCCResource tccResource = (TCCResource)tccResourceCache.get(resourceId);
if (tccResource == null) {
throw new ShouldNeverHappenException(String.format("TCC resource is not exist, resourceId: %s", resourceId));
}
Object targetTCCBean = tccResource.getTargetBean();
Method commitMethod = tccResource.getCommitMethod();
if (targetTCCBean == null || commitMethod == null) {
throw new ShouldNeverHappenException(String.format("TCC resource is not available, resourceId: %s", resourceId));
}
try {
//BusinessActionContext
BusinessActionContext businessActionContext = getBusinessActionContext(xid, branchId, resourceId,
applicationData);
Object ret = commitMethod.invoke(targetTCCBean, businessActionContext);
LOGGER.info("TCC resource commit result : {}, xid: {}, branchId: {}, resourceId: {}", ret, xid, branchId, resourceId);
boolean result;
if (ret != null) {
if (ret instanceof TwoPhaseResult) {
result = ((TwoPhaseResult)ret).isSuccess();
} else {
result = (boolean)ret;
}
} else {
result = true;
}
return result ? BranchStatus.PhaseTwo_Committed : BranchStatus.PhaseTwo_CommitFailed_Retryable;
} catch (Throwable t) {
String msg = String.format("commit TCC resource error, resourceId: %s, xid: %s.", resourceId, xid);
LOGGER.error(msg, t);
return BranchStatus.PhaseTwo_CommitFailed_Retryable;
}
}
4 TCC实践经验
此部分针对所有TCC方案。
4.1 防悬挂控制
原因分析:Try阶段超时(拥堵),分布式事务回滚触发Cancel,Cancel可能比Try先到达。
解决方案:允许空回滚,且要拒绝回滚后的Try操作。
此图来自网络,比较懒不想自己画
4.2 幂等控制
分布式开发中,任何写操作都需要进行幂等控制,TCC的分布式事务也是。
此图来自网络,比较懒不想自己画