Easily complete a distributed transaction TCC with Java, and automatically handle null compensation, suspension, idempotent, etc

Posted by tlawless on Mon, 29 Nov 2021 03:46:04 +0100

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

TCC composition

TCC is divided into three stages

  • Try phase: try to execute, complete all business checks (consistency), and reserve necessary business resources (quasi isolation)
  • Confirm phase: if the Try of all branches is successful, go to the confirm phase. Confirm really executes the business without any business check, and only uses the business resources reserved in the Try phase
  • Cancel phase: if one Try of all branches fails, go to the cancel phase. Cancel releases the business resources reserved in the Try phase.

There are three roles in TCC distributed transactions, which are the same as the classic XA distributed transactions:

  • The AP / application initiates a global transaction and defines which transaction branches are included in the global transaction
  • RM / resource manager is responsible for the management of various resources in branch transactions
  • TM / transaction manager is 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 inter-bank transfer, the transfer out and transfer in are in different micro services respectively. The typical sequence diagram of a successfully completed TCC transaction is as follows:

TCC network exception

During the whole process of global transactions, various network exceptions may occur in TCC, such as null rollback, idempotent and suspension. Because the exceptions of TCC are similar to SAGA, reliable message and other transaction modes, we put all the solutions to exceptions in this article Are you still troubled by the network anomaly of distributed transactions? A function call helps you handle it Explain

TCC practice

For the previous inter-bank transfer operation, the simplest way is to adjust the balance in the Try stage, reverse the balance in the Cancel stage, and leave it blank in the Confirm stage. The problem with this is that if A deducts successfully, the amount transferred to B fails, and finally rolls back to adjust 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 stage, Confirm the actual deduction, and Cancel the unfreezing of funds, so that the data users see at any stage is clear.

Let's develop a TCC transaction

Our example uses the Java language and the distributed transaction framework is https://github.com/yedf/dtm , its support for distributed transactions is elegant. Let's explain the composition of TCC in detail

First, we create two tables, one is the user balance table and the other is the frozen fund table. The statement for creating 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 funds, and check the constraint balance + trading_ Balance > = 0. If the constraint is not tenable, 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("Transaction amount updated successfully");
        } else {
            throw new FailureException("Transaction failed");
        }
    } 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("Balance updated successfully");
        }
    } 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("user: +" + transReq.getUserId() + ",Transfer out" + Math.abs(transReq.getAmount()) + "Yuan preparation");
        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("user: +" + transReq.getUserId() + ",Transfer out" + Math.abs(transReq.getAmount()) + "Meta submission");
        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("user: +" + transReq.getUserId() + ",Transfer out" + Math.abs(transReq.getAmount()) + "Meta rollback");
        this.adjustTrading(connection, transReq);
    });
    connection.close();
    return TransResponse.buildTransResponse(Constant.SUCCESS_RESULT);
}

// The TransIn correlation function is similar to TransOut and is omitted here

At this point, the processing functions of each sub transaction are OK, and then start the TCC transaction for branch calls

@RequestMapping("tccBarrier")
public String tccBarrier() {
    // Create dmt client
    DtmClient dtmClient = new DtmClient(ipPort);
    //Create tcc transaction
    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 {
    // User 1 transferred out 30 yuan
    Response outResponse = tcc
            .callBranch(new TransReq(1, -30), svc + "/barrierTransOutTry", svc + "/barrierTransOutConfirm",
                    svc + "/barrierTransOutCancel");
    log.info("outResponse:{}", outResponse);

    // User 2 transferred 30 yuan
    Response inResponse = tcc
            .callBranch(new TransReq(2, 30), svc + "/barrierTransInTry", svc + "/barrierTransInConfirm",
                    svc + "/barrierTransInCancel");
    log.info("inResponse:{}", inResponse);
}

So far, a complete TCC distributed transaction is written.

If you want to run a successful example in its entirety, follow dtmcli-java-sample After setting up the environment and starting up, run the following command to run the example of tcc

curl http://localhost:8081/tccBarrier

Rollback of TCC

What if the bank finds that the account of user 2 is abnormal and fails to return when transferring the amount out of user 2? In our example, the balance of available users is 10000, and initiating a transfer of 100000 will trigger an exception and fail:
curl http://localhost:8081/tccBarrier
This is a sequence diagram of transaction failure interactions

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

Summary

In this article, we introduce the theoretical knowledge of TCC, and give a complete process of writing a TCC transaction through an example, including normal successful completion and successful rollback. I believe readers have a deep understanding of TCC through this article.

For idempotent, dangling and null compensation to be handled in distributed transactions, please refer to another article: Distributed transaction you can not know the pit, a function call to help you handle it

For more comprehensive knowledge of distributed transactions, please refer to Seven classic solutions for distributed transactions

The examples used in this paper are selected from yedf/dtmcli-java-sample It supports multiple transaction modes: TCC, SAGA, XA, cross language support for transaction messages, and supports clients in golang, python, PHP, nodejs and other languages. It provides sub transaction barrier function to gracefully solve idempotent, suspension, null compensation and other problems.

After reading this article, welcome to visit https://github.com/yedf/dtm Project, give star support!

Topics: Java Database MySQL PostgreSQL Back-end