
最常應用的模式
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事務失效的原因一樣哈)。