3

What is TCC? TCC is the abbreviation of Try, Confirm and Cancel. It was first proposed by Pat Helland in a paper entitled "Life beyond Distributed Transactions: an Apostate's Opinion" published in 2007.

TCC composition

TCC is divided into 3 stages

  • Try stage: try to execute, complete all business checks (consistency), reserve necessary business resources (quasi-isolation)
  • Confirm phase: If the Try of all branches are successful, go to the Confirm phase. Confirm actually executes the business without any business checks, and only uses the business resources reserved during the Try phase
  • Cancel phase: If one of the Try of all branches fails, go to the Cancel phase. Cancel releases the business resources reserved in the Try phase.

In TCC distributed transaction, there are 3 roles, the same as the classic XA distributed transaction:

  • AP/application, initiate a global transaction, define which transaction branches the global transaction contains
  • RM/resource manager, responsible for the management of various resources of branch affairs
  • TM/transaction manager, responsible for coordinating the correct execution of global transactions, including the execution of Confirm and Cancel, and handling network exceptions

If we want to conduct a business similar to bank inter-bank transfer, the transfer out (TransOut) and transfer in (TransIn) are in different microservices. A typical sequence diagram of a successfully completed TCC transaction is as follows:
image.png

TCC network abnormal

In the process of TCC's entire global transaction, various network abnormalities may occur, such as empty rollback, idempotence, and suspension. The example introduced below uses the sub-transaction barrier SDK of dtm to gracefully handle these abnormal situations. In each open source project I know, developers currently (2021-12-01) require developers to handle these exceptions manually, and our project's system automatically handles these exceptions, which is the first of its kind, which greatly reduces the difficulty of developing distributed transactions.

Here is an article distributed transactions. Let’s take a look at the correct posture . It explains this type of network abnormal problems in detail, the current technical status and problems, and our solutions.

TCC practice

For the previous inter-bank transfer operation, the easiest way is to adjust the balance in the Try phase, reverse the balance in the Cancel phase, and do nothing in the Confirm phase. The problem with this is that if the deduction of A is successful, the transfer of the amount to B fails, and finally rolls back, adjusting the balance of A to the initial value. In this process, if A finds that his balance has been deducted, but the payee B has not received the balance for a long time, it will cause trouble to A.

A better approach is to freeze the amount transferred by A in the Try phase, Confirm to deduct the actual amount, and Cancel to unfreeze the funds, so that the user can see the data clearly at any stage.

Below we carry out the specific development of a TCC transaction

Our example uses the Java language, and the distributed transaction framework used is https://github.com/yedf/dtm , which supports distributed transactions very elegantly. Let's explain the composition of TCC in detail below

We first create a user balance table, the statement to build the table is as follows:

create table if not exists dtm_busi.user_account(
  id int(11) PRIMARY KEY AUTO_INCREMENT,
  user_id int(11) UNIQUE,
  balance DECIMAL(10, 2) not null default '0',
  trading_balance DECIMAL(10, 2) not null default '0',
  create_time datetime DEFAULT now(),
  update_time datetime DEFAULT now(),
  key(create_time),
  key(update_time)
);

In the table, trading_balance records the amount being traded.

We first write the core code to freeze/unfreeze the funds operation, and check the constraint balance+trading_balance >= 0. If the constraint is not established, the execution fails

public void adjustTrading(Connection connection, TransReq transReq) throws Exception {
    String sql = "update dtm_busi.user_account set trading_balance=trading_balance+?"
            + " where user_id=? and trading_balance + ? + balance >= 0";
    PreparedStatement preparedStatement = null;
    try {
        preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setInt(1, transReq.getAmount());
        preparedStatement.setInt(2, transReq.getUserId());
        preparedStatement.setInt(3, transReq.getAmount());
        if (preparedStatement.executeUpdate() > 0) {
            System.out.println("交易金额更新成功");
        } else {
            throw new FailureException("交易失败");
        }
    } finally {
        if (null != preparedStatement) {
            preparedStatement.close();
        }
    }
    
}

Then adjust the balance

public void adjustBalance(Connection connection, TransReq transReq) throws SQLException {
    PreparedStatement preparedStatement = null;
    try {
        String sql = "update dtm_busi.user_account set trading_balance=trading_balance-?,balance=balance+? where user_id=?";
        preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setInt(1, transReq.getAmount());
        preparedStatement.setInt(2, transReq.getAmount());
        preparedStatement.setInt(3, transReq.getUserId());
        if (preparedStatement.executeUpdate() > 0) {
            System.out.println("余额更新成功");
        }
    } finally {
        if (null != preparedStatement) {
            preparedStatement.close();
        }
    }
}

Let's write a specific Try/Confirm/Cancel processing function

@RequestMapping("barrierTransOutTry")
public Object TransOutTry(HttpServletRequest request) throws Exception {

    BranchBarrier branchBarrier = new BranchBarrier(request.getParameterMap());
    logger.info("barrierTransOutTry branchBarrier:{}", branchBarrier);

    TransReq transReq = extracted(request);
    Connection connection = dataSourceUtil.getConnecion();
    branchBarrier.call(connection, (barrier) -> {
        System.out.println("用户: +" + transReq.getUserId() + ",转出" + Math.abs(transReq.getAmount()) + "元准备");
        this.adjustTrading(connection, transReq);
    });
    connection.close();
    return TransResponse.buildTransResponse(Constant.SUCCESS_RESULT);
}

@RequestMapping("barrierTransOutConfirm")
public Object TransOutConfirm(HttpServletRequest request) throws Exception {
    BranchBarrier branchBarrier = new BranchBarrier(request.getParameterMap());
    logger.info("barrierTransOutConfirm branchBarrier:{}", branchBarrier);
    Connection connection = dataSourceUtil.getConnecion();
    TransReq transReq = extracted(request);
    branchBarrier.call(connection, (barrier) -> {
        System.out.println("用户: +" + transReq.getUserId() + ",转出" + Math.abs(transReq.getAmount()) + "元提交");
        adjustBalance(connection, transReq);
    });
    connection.close();
    return TransResponse.buildTransResponse(Constant.SUCCESS_RESULT);
}

@RequestMapping("barrierTransOutCancel")
public Object TransOutCancel(HttpServletRequest request) throws Exception {
    BranchBarrier branchBarrier = new BranchBarrier(request.getParameterMap());
    logger.info("barrierTransOutCancel branchBarrier:{}", branchBarrier);
    TransReq transReq = extracted(request);
    Connection connection = dataSourceUtil.getConnecion();
    branchBarrier.call(connection, (barrier) -> {
        System.out.println("用户: +" + transReq.getUserId() + ",转出" + Math.abs(transReq.getAmount()) + "元回滚");
        this.adjustTrading(connection, transReq);
    });
    connection.close();
    return TransResponse.buildTransResponse(Constant.SUCCESS_RESULT);
}

// TransIn相关函数与TransOut类似,这里省略

At this point, the processing functions of each sub-transaction have been OK. In the above code, the following lines are related to the sub-transaction barrier code. As long as you call your business logic in this way, the sub-transaction barrier guarantees repeated requests, suspension, and emptiness. When a compensation situation occurs, your business logic will not be invoked, ensuring the correct conduct of normal business

BranchBarrier branchBarrier = new BranchBarrier(request.getParameterMap());
branchBarrier.call(connection, (barrier)-> {
...
 });

Then start the TCC transaction and make a branch call

@RequestMapping("tccBarrier")
public String tccBarrier() {
    // 创建dmt client
    DtmClient dtmClient = new DtmClient(ipPort);
    //创建tcc事务
    try {
        dtmClient.tccGlobalTransaction(dtmClient.genGid(), TccTestController::tccBarrierTrans);
    } catch (Exception e) {
        log.error("tccGlobalTransaction error", e);
        return "fail";
    }
    return "success";
}

public static void tccBarrierTrans(Tcc tcc) throws Exception {
    // 用户1 转出30元
    Response outResponse = tcc
            .callBranch(new TransReq(1, -30), svc + "/barrierTransOutTry", svc + "/barrierTransOutConfirm",
                    svc + "/barrierTransOutCancel");
    log.info("outResponse:{}", outResponse);

    // 用户2 转入30元
    Response inResponse = tcc
            .callBranch(new TransReq(2, 30), svc + "/barrierTransInTry", svc + "/barrierTransInConfirm",
                    svc + "/barrierTransInCancel");
    log.info("inResponse:{}", inResponse);
}

At this point, a complete TCC distributed transaction has been written.

If you want to run a successful example completely, then follow the dtmcli-java-sample project. After the environment is started, run the following command to run the tcc example.

curl http://localhost:8081/tccBarrier

TCC rollback

What happens if the bank finds that the account of user 2 is abnormal when the bank is about to transfer the amount out of user 2 and the return fails? In our example, the user's balance is 10,000, and a 100,000 transfer will trigger an exception and fail:
curl http://localhost:8081/tccBarrierError
This is the sequence diagram of transaction failure interaction
image.png

The difference between this and a successful TCC is that when a sub-transaction returns to failure, the global transaction is subsequently rolled back, and the Cancel operation of each sub-transaction is called to ensure that the global transaction is all rolled back.

The above describes a rollback situation that is very in line with expectations. There will be many rollbacks for the actual running business. For example, the TransIn request is abnormal before it starts processing, or the processing ends and the transaction has been submitted abnormally.

With the help of the sub-transaction barrier, the user does not need to care about these different abnormal situations, it will automatically handle them. You can try to throw an exception in TransIn processing after the transaction is committed. You can see that in this case, the last transaction is correctly rolled back, and the final balances of the two users are the same as before the transfer.

summary

In this article, we introduced the theoretical knowledge of TCC, and through an example, gave a complete process of writing a TCC transaction, covering the normal successful completion and successful rollback. I believe that readers have an in-depth understanding of TCC through this article.

Regarding the idempotence, suspension, and null compensation that need to be processed in distributed transactions, please refer to another article: These common uses of distributed transactions have pits, let’s take a look at the correct posture

For more comprehensive knowledge of most classic seven solutions for distributed transactions

The examples used in this article are selected from yedf/dtmcli-java-sample . The distributed transaction manager used is https://github.com/yedf/dtm , which supports multiple transaction modes: TCC, SAGA, XA, transaction message cross-language support, and already supports golang, python, PHP, nodejs, etc. Language client. Provide sub-transaction barrier function, elegantly solve the problems of idempotence, suspension, null compensation and so on.

After reading this dry goods, welcome everyone to visit the https://github.com/yedf/dtm project, give stars to support!


叶东富
1.1k 声望6.1k 粉丝