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

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.

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 more comprehensive knowledge of distributed transactions, please refer to Seven classic solutions for distributed transactions

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

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

Tags: Java Go Database Microservices

Posted on Sat, 25 Sep 2021 21:15:05 -0400 by Sk~