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

posted @ 2022-07-25 10:39  姚春辉  阅读(155)  评论(0编辑  收藏  举报