Easily complete a distributed transaction TCC with Java, a real nanny tutorial

Keywords: Java Back-end

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 practice

Let's develop a TCC transaction

The distributed transaction framework used in our example is dtm, which supports distributed transactions very gracefully. Let's explain the composition of TCC in detail

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

@RequestMapping("TransOutTry")
    public Map<String, String> TransOutTry() {
        logger.info("TransOutTry");
        Map<String, String> result = new HashMap<>();
        result.put("dtm_result", "SUCCESS");
        return result;
    }

    @RequestMapping("TransOutConfirm")
    public Map<String, String> TransOutConfirm(HttpServerResponse response) {
        logger.info("TransOutConfirm");
        Map<String, String> result = new HashMap<>();
        result.put("dtm_result", "SUCCESS");
        return result;
    }

    @RequestMapping("TransOutCancel")
    public Map<String, String> TransOutCancel() {
        logger.info("TransOutCancel");
        Map<String, String> result = new HashMap<>();
        result.put("dtm_result", "SUCCESS");
        return result;
    }

    @RequestMapping("TransInTry")
    public Map<String, String> TransInTry() {
        logger.info("TransInTry");
        Map<String, String> result = new HashMap<>();
        result.put("dtm_result", "SUCCESS");
        return result;
    }

    @RequestMapping("TransInConfirm")
    public Map<String, String> TransInConfirm() {
        logger.info("TransInConfirm");
        Map<String, String> result = new HashMap<>();
        result.put("dtm_result", "SUCCESS");
        return result;
    }

    @RequestMapping("TransInCancel")
    public Map<String, String> TransInCancel() {
        logger.info("TransInCancel");
        Map<String, String> result = new HashMap<>();
        result.put("dtm_result", "SUCCESS");
        return result;
    }

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

@RequestMapping("fireTcc")
    public String fireTcc() {
        Function<Tcc, Boolean> function = TccController::tccTrans;
        return tcc.tccGlobalTransaction(function);
    }

    public static Boolean tccTrans(Tcc tcc) {
        try {
            boolean a = tcc.callBranch("", svc + "/TransOutTry", svc + "/TransOutConfirm", svc + "/TransOutCancel");
            boolean b = tcc.callBranch("", svc + "/TransInTry", svc + "/TransInConfirm", svc + "/TransInCancel");
            return a && b;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

So far, a complete TCC distributed transaction is written.

If you want to run a successful example completely, refer to the example yedf / dtmcli Java sample, which is very simple to run

# Deploy start dtm
# docker version 18 or above is required
git clone https://github.com/yedf/dtm
cd dtm
docker-compose up

# Start another command line
git clone https://github.com/yedf/dtmcli-java-sample.git
cd dtmcli-java-sample
# Compile and run the example main/src/main/java/com/github/viticis/dtmclijavaexamples/DtmcliJavaSampleApplication

Rollback of TCC

What if the bank finds that the account of user 2 is abnormal and fails to return when it is ready to transfer the amount to user 2? We can simulate this by having TransIn return a failure

@RequestMapping("TransInTry")
    public Map<String, String> TransInTry() {
        logger.info("TransInTry");
        Map<String, String> result = new HashMap<>();
        result.put("dtm_result", "FAILURE");
        return result;
    }

We give the sequence diagram of transaction failure interaction

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.

In the TCC transaction mode, many readers will ask, what happens if Confirm/Cancel fails? This is a good question, which means that you are thinking deeply about the TCC transaction mode. The first case is temporary failure, such as network failure, application or database downtime. Retry such errors and finally return success; The other case is business failure. According to the TCC agreement, resources are locked in the first stage to ensure that sufficient resources can be used for Confirm/Cancel execution. In other words, in program logic, Confirm/Cancel is not allowed to return business failure. If a business failure occurs, it is a bug and needs to be manually repaired by the developer.

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.

More comprehensive knowledge of distributed transactions

The examples used in this article are excerpted from yedf/dtm. It supports a variety of transaction modes: TCC, SAGA, XA and cross language support for transaction messages. It has supported clients in golang, python, Java, PHP, nodejs and other languages. Refer to the SDK s of various languages. It provides sub transaction barrier function to gracefully solve idempotent, suspension, null compensation and other problems.

 

Posted by [UW] Jake on Thu, 18 Nov 2021 08:55:33 -0800