@[TOC] Still the same old saying, on the Internet about distributed transaction explanation theory more, less cases, recently Songko would like to pass a few cases, and everyone common distributed transaction solution over again, before I shared with you AT model in Seata, today we look AT TCC model.

The TCC mode has a lot in common with the AT mode that Songo demonstrated earlier, but also a lot of differences.

  • That’s where distributed transaction TCC comes from!

Interested partners can also have a look first.

Today, we will first come to the whole case, after the case analysis, you will basically understand how TCC is, and also understand the difference between TCC and AT.

1. In the code

It is still the official Seata warehouse, which contains TCC cases. However, due to the large number of cases in this warehouse and the large number of dependencies that need to be downloaded, it is easy to fail to import all the cases. The following is a case organized by Songko (removing unnecessary projects), which can be imported directly. You can download this case by replying to seata-Demo.

The TCC case given by the authorities is a classic case of transfer. Many small partners learn the case of transfer when they contact with the business for the first time, so this business is easy to understand for everyone.

1.1 Business Process

I’ll talk about the business logic of this case first, and then we’ll look at the code, which looks like this:

  1. The project has two parts, Provider and Consumer (if there were only one project, there would be no distributed transaction problem).
  2. Providers provide two transfer interfaces, one for deducting account balances and the other for adding money to accounts. In this case, the two projects are provided by one provider. In practice, you can use two providers to provide the interfaces respectively.
  3. The interfaces provided by the provider are exposed through Dubbo, and the consumer refers to these exposed interfaces through Dubbo.
  4. The transfer takes place in two steps: first, call FirstTccAction to subtract the amount from an account; Then call SecondTccAction to add money to an account. Both operations either succeed or fail at the same time.

Some people might say, are all the interfaces provided by the provider, is also a distributed transaction? Calculate! Of course! Although the two interfaces mentioned above are provided by the provider, they are still distributed transactions because there are two databases and different interfaces operate on different databases.

That’s basically what this project is going to do.

1.2 Case Configuration

The official case uses H2 database, which is not convenient to see the effect. Therefore, we do a little configuration here and change the database to MySQL, so that we can easily see the effect of transfer.

The configuration procedure is as follows:

  1. MySQL > create two databases from local MySQL:

Create two empty libraries instead of creating tables, which are automatically initialized when the project starts.

  • Transfer_from_db: library for transferring out accounts.
  • Transfer_to_db: transfer account library.
  1. Modify the database connection pool version of the project.

There is a slight problem with the official case, and an error will be reported if the case is directly started. The reason is that the version of DBCP used in the case conflicts with that of MyBatis, so we need to change the version number of DBCP to 1.4 in PEM. XML first, as follows:

<properties>
    <curator.version>4.2.0</curator.version>
    <commons-dbcp.version>1.4</commons-dbcp.version>
    <h2.version>1.4.181</h2.version>
    <mybatis.version>3.5.6</mybatis.version>
    <mybatis.spring.version>1.3.1</mybatis.spring.version>
</properties>
Copy the code

Add MySQL driver as follows:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.25</version>
</dependency>
Copy the code

Although some things in the case are a bit old-fashioned, in line with the principle of simplicity, I will not modify, as long as the project runs and can help us understand TCC.

In addition, the Dubbo version referenced by this project also has a problem, we manually add the version number to it (the default 3.0.1 version has a problem, songo test 2.7.3 is available) :

<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework</groupId>
            <artifactId>spring</artifactId>
        </exclusion>
    </exclusions>
    <version>2.7.3</version>
</dependency>
Copy the code
  1. Modify database configurations.

SRC /main/resources/db-bean: SRC /main/resources/db-bean: SRC /main/resources/db-bean

From-datasourced-bean.xml:

<bean id="fromAccountDataSource"  class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName">
        <value>com.mysql.cj.jdbc.Driver</value>
    </property>
    <property name="url">
        <value>jdbc:mysql:///transfer_from_db? serverTimezone=Asia/Shanghai</value>
    </property>
    <property name="username">
        <value>root</value>
    </property>
    <property name="password">
        <value>123</value>
    </property>
</bean>
Copy the code

Change four things: database driver, database connection address, database user name, database password.

To datasource-bean.xml

<bean id="toAccountDataSource"  class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName">
        <value>com.mysql.cj.jdbc.Driver</value>
    </property>
    <property name="url">
        <value>jdbc:mysql:///transfer_to_db? serverTimezone=Asia/Shanghai</value>
    </property>
    <property name="username">
        <value>root</value>
    </property>
    <property name="password">
        <value>123</value>
    </property>
</bean>
Copy the code

The two configurations differ mainly in the database they connect to.

OK, after that, our configuration is complete.

1.3 Case Operation

The case run is divided into two parts.

1.3.1 launch Provider

Find the SRC/main/Java/IO/seata/samples/TCC/transfer/starter/TransferProviderStarter. Java, execute the main method, that can be executed directly, If the console sees the following information, the project is successfully started and the table structure and table data are successfully initialized:

There may be a null pointer exception during startup, but it does not affect usage, so you can ignore it.

After the project is successfully started, we can look at the two databases we just created, each containing three tables:

First look at the roll-out library:

The account table has two records:

In this table, there are two accounts A and B, each with 100 yuan, and their freezed_amount is 0.

Business_action and business_activity are both empty tables.

Let’s look at the imported library:

Transfer_from_db = transfer_from_db = transfer_from_db = transfer_from_db

1.3.2 Enabling the Transfer logic

Find the SRC/main/Java/IO/seata/samples/TCC/transfer/starter/TransferApplication. Java, the inside of the main method of two testing methods, If doTransferSuccess succeeds, doTransferFailed fails.

For these two methods, we first comment out doTransferFailed and run the doTransferSuccess method. The console output log is as follows:

This means the transfer has been successful.

Now check the database, account A is 10 yuan less, account C is 10 yuan more:

Then we comment out doTransferSuccess and run the doTransferFailed method. The result is as follows:

You can see that the transfer failed, then check the database, and find that the data in the two databases has not changed, indicating that the data has been rolled back.

Ok, so this is a typical transfer case that the authorities have given us. So how does this transfer case work? Now let’s analyze the code. After analyzing the code, we will understand what TCC is!

2. Code analysis

Here on Dubbo call logic, Songo will not say, I believe you can, let’s focus on distributed transaction related code.

First, two interfaces are provided in this project:

  • FirstTccAction
  • SecondTccAction

These two interfaces represent two steps in transferring money:

  • FirstTccAction: This interface is used to handle the transfer of account balance (minus money). The data source used in this interface is transfer_from_db.
  • SecondTccAction: This interface is used to handle transfers to accounts (add money). The data source used in this interface is transfer_to_DB.

The definitions of the two interfaces are actually very similar, and once we understand one, the other is easy to understand.

2.1 FirstTccAction

This is the interface to transfer money out. Let’s look at the definition of the interface:

public interface FirstTccAction {
	
	/** ** one stage method **@param businessActionContext
     * @param accountNo
     * @param amount
     */
    @TwoPhaseBusinessAction(name = "firstTccAction", commitMethod = "commit", rollbackMethod = "rollback")
    public boolean prepareMinus(BusinessActionContext businessActionContext,
                                @BusinessActionContextParameter(paramName = "accountNo") String accountNo,
                                @BusinessActionContextParameter(paramName = "amount") double amount);

    /** * Phase 2 commit *@param businessActionContext
     * @return* /
    public boolean commit(BusinessActionContext businessActionContext);

    /** * Phase rollback *@param businessActionContext
     * @return* /
    public boolean rollback(BusinessActionContext businessActionContext);
}
Copy the code

As you can see, there are three methods in the interface:

  1. prepareMinus
  2. commit
  3. rollback

FirstTccActionImpl (FirstTccActionImpl); FirstTccActionImpl (FirstTccActionImpl);

  1. PrepareMinus: This method is prepareMinus. PrepareMinus: This method is prepareMinus. Checking account existence, freezing transfer funds and so on can be done in this method. Take the above case as an example (account A transfers 10 yuan to account C), specifically, inFirstTccActionImpl#prepareMinusMethod:
@Override
public boolean prepareMinus(BusinessActionContext businessActionContext, final String accountNo, final double amount) {
    // Distributed transaction ID
    final String xid = businessActionContext.getXid();
    return fromDsTransactionTemplate.execute(new TransactionCallback<Boolean>(){
        @Override
        public Boolean doInTransaction(TransactionStatus status) {
            try {
                // Check the account balance
                Account account = fromAccountDAO.getAccountForUpdate(accountNo);
                if(account == null) {throw new RuntimeException("Account does not exist");
                }
                if (account.getAmount() - amount < 0) {
                    throw new RuntimeException("Insufficient balance");
                }
                // Freeze the transfer amount
                double freezedAmount = account.getFreezedAmount() + amount;
                account.setFreezedAmount(freezedAmount);
                fromAccountDAO.updateFreezedAmount(account);
                System.out.println(String.format("prepareMinus account[%s] amount[%f], dtx transaction id: %s.", accountNo, amount, xid));
                return true;
            } catch (Throwable t) {
                t.printStackTrace();
                status.setRollbackOnly();
                return false; }}}); }Copy the code

This method does three things: 1. Check whether account A exists and throw exception if it does not; 2. 2. Check whether the balance of account A is less than 10 yuan, if so, throw the exception (there is not enough money to transfer); 3. Modify the database records of account A to mark the frozen funds (the freezed_amount field of account A will be changed to 10).

  1. Everything the prepareMinus method does is a one-phase thing.
  2. The prepareMinus method has a @TwophaseBusinessAction annotation to mark the transaction. CommitMethod annotation indicates the method for committing the transaction, and rollbackMethod indicates the method for rolling back the transaction. Both methods are defined in the transaction.
  3. The prepareMinus method is called by the developer itself, so custom parameters can be passed in, while the COMMIT and ROLLBACK methods are called by the framework (if phase 1 fails, phase 2 automatically rolls back; The first phase is fine, and the second phase is automatically committed), but we may still need some business-specific parameters when the framework calls, so in prepareMinus, We can use @ BusinessActionContextParameter annotations to the parameters is needed in the commit and rollback is bound to the BusinessActionContext, These parameters will be available in the commit and ROLLBACK methods.
  4. The COMMIT method is a two-phase commit method. If the first-phase work is completed successfully, the two-phase commit is performed. Concrete implementation inFirstTccActionImpl#commitMethod:
@Override
public boolean commit(BusinessActionContext businessActionContext) {
    // Distributed transaction ID
    final String xid = businessActionContext.getXid();
    / / account ID
    final String accountNo = String.valueOf(businessActionContext.getActionContext("accountNo"));
    // Transfer amount
    final double amount = Double.valueOf(String.valueOf(businessActionContext.getActionContext("amount")));
    return fromDsTransactionTemplate.execute(new TransactionCallback<Boolean>() {
        @Override
        public Boolean doInTransaction(TransactionStatus status) {
            try{
                Account account = fromAccountDAO.getAccountForUpdate(accountNo);
                // Deduct the account balance
                double newAmount = account.getAmount() - amount;
                if (newAmount < 0) {
                    throw new RuntimeException("Insufficient balance");
                }
                account.setAmount(newAmount);
                // Release the frozen amount of the account
                account.setFreezedAmount(account.getFreezedAmount()  - amount);
                fromAccountDAO.updateAmount(account);
                System.out.println(String.format("minus account[%s] amount[%f], dtx transaction id: %s.", accountNo, amount, xid));
                return true;
            }catch (Throwable t){
                t.printStackTrace();
                status.setRollbackOnly();
                return false; }}}); }Copy the code

Take a look at the execution logic of this method:

  • First pull the parameters in prepareMinus out of the BusinessActionContext object.
  • Then determine if the account balance is sufficient (enough to transfer money).
  • Update account balance and frozen amount (balance is transferred normally, frozen amount is returned to zero).

This is what the commit method does.

  1. The rollback method is a two-phase rollback method. If the one-phase method is faulty, the two-phase rollback method needs to be rolled back. The rollback method needs to do the reverse compensation operationFirstTccActionImpl#rollbackMethod:
@Override
public boolean rollback(BusinessActionContext businessActionContext) {
    // Distributed transaction ID
    final String xid = businessActionContext.getXid();
    / / account ID
    final String accountNo = String.valueOf(businessActionContext.getActionContext("accountNo"));
    // Transfer amount
    final double amount = Double.valueOf(String.valueOf(businessActionContext.getActionContext("amount")));
    return fromDsTransactionTemplate.execute(new TransactionCallback<Boolean>() {
        @Override
        public Boolean doInTransaction(TransactionStatus status) {
            try{
                Account account = fromAccountDAO.getAccountForUpdate(accountNo);
                if(account == null) {// Account does not exist, rollback does nothing
                    return true;
                }
                // Release the frozen amount
                account.setFreezedAmount(account.getFreezedAmount()  - amount);
                fromAccountDAO.updateFreezedAmount(account);
                System.out.println(String.format("Undo prepareMinus account[%s] amount[%f], dtx transaction id: %s.", accountNo, amount, xid));
                return true;
            }catch (Throwable t){
                t.printStackTrace();
                status.setRollbackOnly();
                return false; }}}); }Copy the code

As you can see, the reverse compensation of rollback is actually very simple, first look at whether the account exists, if the account exists, cancel the frozen funds frozen on the line.

That’s the whole process of getting the money out.

2.2 SecondTccAction

This is the interface for transferring money in.


public interface SecondTccAction {

	 /** ** one stage method **@param businessActionContext
     * @param accountNo
     * @param amount
     */
    @TwoPhaseBusinessAction(name = "secondTccAction", commitMethod = "commit", rollbackMethod = "rollback")
    public boolean prepareAdd(BusinessActionContext businessActionContext,
                              @BusinessActionContextParameter(paramName = "accountNo") String accountNo,
                              @BusinessActionContextParameter(paramName = "amount") double amount);

    /** * Phase 2 commit *@param businessActionContext
     * @return* /
    public boolean commit(BusinessActionContext businessActionContext);

    /** * Phase rollback *@param businessActionContext
     * @return* /
    public boolean rollback(BusinessActionContext businessActionContext);

}
Copy the code

Interface implementation class:

public class SecondTccActionImpl implements SecondTccAction {

    /** * add money to account DAP */
    private AccountDAO toAccountDAO;

    private TransactionTemplate toDsTransactionTemplate;

    /** * Stage 1 preparation, transfer to capital preparation *@param businessActionContext
     * @param accountNo
     * @param amount
     * @return* /
    @Override
    public boolean prepareAdd(final BusinessActionContext businessActionContext, final String accountNo, final double amount) {
        // Distributed transaction ID
        final String xid = businessActionContext.getXid();

        return toDsTransactionTemplate.execute(new TransactionCallback<Boolean>(){

            @Override
            public Boolean doInTransaction(TransactionStatus status) {
                try {
                    // Verify the account
                    Account account = toAccountDAO.getAccountForUpdate(accountNo);
                    if(account == null){
                        System.out.println("[prepareAdd: account"+accountNo+"] txId: does not exist + businessActionContext.getXid());
                        return false;
                    }
                    // Funds to be transferred as unusable amount
                    double freezedAmount = account.getFreezedAmount() + amount;
                    account.setFreezedAmount(freezedAmount);
                    toAccountDAO.updateFreezedAmount(account);
                    System.out.println(String.format("prepareAdd account[%s] amount[%f], dtx transaction id: %s.", accountNo, amount, xid));
                    return true;
                } catch (Throwable t) {
                    t.printStackTrace();
                    status.setRollbackOnly();
                    return false; }}}); }/** * Phase 2 commit *@param businessActionContext
     * @return* /
    @Override
    public boolean commit(BusinessActionContext businessActionContext) {
        // Distributed transaction ID
        final String xid = businessActionContext.getXid();
        / / account ID
        final String accountNo = String.valueOf(businessActionContext.getActionContext("accountNo"));
        // Transfer amount
        final double amount = Double.valueOf(String.valueOf(businessActionContext.getActionContext("amount")));
        return toDsTransactionTemplate.execute(new TransactionCallback<Boolean>() {

            @Override
            public Boolean doInTransaction(TransactionStatus status) {
                try{
                    Account account = toAccountDAO.getAccountForUpdate(accountNo);
                    / / add money
                    double newAmount = account.getAmount() + amount;
                    account.setAmount(newAmount);
                    // Freeze amount cleared
                    account.setFreezedAmount(account.getFreezedAmount()  - amount);
                    toAccountDAO.updateAmount(account);

                    System.out.println(String.format("add account[%s] amount[%f], dtx transaction id: %s.", accountNo, amount, xid));
                    return true;
                }catch (Throwable t){
                    t.printStackTrace();
                    status.setRollbackOnly();
                    return false; }}}); }/** * Phase rollback *@param businessActionContext
     * @return* /
    @Override
    public boolean rollback(BusinessActionContext businessActionContext) {
        // Distributed transaction ID
        final String xid = businessActionContext.getXid();
        / / account ID
        final String accountNo = String.valueOf(businessActionContext.getActionContext("accountNo"));
        // Transfer amount
        final double amount = Double.valueOf(String.valueOf(businessActionContext.getActionContext("amount")));
        return toDsTransactionTemplate.execute(new TransactionCallback<Boolean>() {

            @Override
            public Boolean doInTransaction(TransactionStatus status) {
                try{
                    Account account = toAccountDAO.getAccountForUpdate(accountNo);
                    if(account == null) {// Account does not exist, no need to scroll back
                        return true;
                    }
                    // Freeze amount cleared
                    account.setFreezedAmount(account.getFreezedAmount()  - amount);
                    toAccountDAO.updateFreezedAmount(account);

                    System.out.println(String.format("Undo prepareAdd account[%s] amount[%f], dtx transaction id: %s.", accountNo, amount, xid));
                    return true;
                }catch (Throwable t){
                    t.printStackTrace();
                    status.setRollbackOnly();
                    return false; }}}); }}Copy the code

FirstTccActionImpl SecondTccActionImpl FirstTccActionImpl SecondTccActionImpl

  1. In the prepareAdd method, you determine whether the transferred account exists, and if so, deposit the transferred funds in the frozen field first (not directly to the account balance).
  2. In the COMMIT method, when a transaction commits, the frozen funds are added to the account balance and the frozen funds are cleared.
  3. In the ROLLBACK method, when the transaction rolls back, reverse compensation clears the frozen funds.

That’s how you get the money in.

2.3 TransferServiceImpl

Transfer (FirstTccAction); Transfer (SecondTccAction); transfer (SecondTccAction); transfer (FirstTccAction); transfer (SecondTccAction); transfer (SecondTccAction);

public class TransferServiceImpl implements TransferService {

    private FirstTccAction firstTccAction;

    private SecondTccAction secondTccAction;

    /** * transfer operation *@paramFrom withholding account *@paramTo add money to account *@paramAmount Transfer amount *@return* /
    @Override
    @GlobalTransactional
    public boolean transfer(final String from, final String to, final double amount) {
        // Deduct money participant, one stage execution
        boolean ret = firstTccAction.prepareMinus(null, from, amount);

        if(! ret){// Fail in the first stage; Roll back local and distributed transactions
            throw new RuntimeException("Account: ["+from+"] Withholding failed");
        }

        // Add money participant, one stage execution
        ret = secondTccAction.prepareAdd(null, to, amount);

        if(! ret){throw new RuntimeException("Account: ["+to+"] Advance payment failed");
        }

        System.out.println(String.format("transfer amount[%s] from [%s] to [%s] finish.", String.valueOf(amount), from, to));
        return true;
    }

    public void setFirstTccAction(FirstTccAction firstTccAction) {
        this.firstTccAction = firstTccAction;
    }

    public void setSecondTccAction(SecondTccAction secondTccAction) {
        this.secondTccAction = secondTccAction; }}Copy the code

Let’s look at the transfer logic:

  1. First, inject FirstTccAction and SecondTccAction, and if this is a microservice project, add their Feign here.
  2. The transfer method, annotated @GlobalTransactional, performs the specific transfer logic. PrepareXXX is called to complete the work of phase 1. If phase 1 fails, an exception will be thrown and the transaction will be rolled back. The rollback automatically calls FirstTccAction and SecondTccAction’s respective ROLLBACK methods (reverse compensation); If the first phase is ok, the second phase calls the commit methods of FirstTccAction and SecondTccAction to complete the commit.

That’s the general logic of the transfer.

3. TCC Vs AT

After the above analysis, I believe that you have some feelings about TCC.

So what is TCC?

TCC is short for try-confirm-cancel.

In TCC mode, a transaction is implemented through do-commit /Rollback. The developer needs to provide a try-confirm /Cancel interface for each interservice call. This interface is similar to our above prepareXXX/commit/rollback interface.

To take another simplified e-commerce case, when the user completes the payment, the order service is processed first, and then the goods service is called to reduce the inventory. Both operations succeed or fail simultaneously, which involves a distributed transaction: in the TCC mode, we need three interfaces. First is the Try interface for inventory reduction. Here, we need to check the status of business data and check whether the inventory is enough. Then, we need to reserve resources, that is, set the reserved state in a field. Reset the previously reserved field (the reserved state is actually similar to the frozen funds field freezed_amount in the previous example).

Why go to so much trouble? The three steps have the advantage of being able to perform a smooth database reset (reverse compensation) in the event of an error, and retry even if confirm fails, as long as the logic in prepare is correct.

Let’s look at the next picture:

According to the behavior modes of the two phases, we divided the Branch transactions into Automatic (Branch) Transaction Mode and TCC (Branch) Transaction Mode.

The AT schema is based on a relational database that supports local ACID transactions:

  • Phase 1 Prepare behavior: The local transaction submits the service data update and the corresponding rollback log.
  • Phase-2 Commit behavior: Automatically and asynchronously clears rollback logs in batches after the logs are successfully completed immediately.
  • Two-phase rollback: the system automatically generates compensation operations to rollback data by rolling back logs.

As for AT, if you are not familiar with it, you can refer to songo’s previous article:

  • Experience distributed transactions in five minutes! So easy!

Accordingly, TCC mode, independent of transaction support for underlying data resources:

  • One-stage Prepare behavior: The customized prepare logic is invoked.
  • Two-stage COMMIT behavior: Custom commit logic is invoked.
  • Two-stage ROLLBACK behavior: Invoke custom ROLLBACK logic.

The TCC pattern supports the integration of custom branch transactions into the management of global transactions.

In TCC, the logic in prepare, COMMIT, and ROLLBACK is written by ourselves. Therefore, TCC does not rely on the transaction support of the underlying data resources.

Compared to AT mode, TCC requires us to implement the prepare, COMMIT, and ROLLBACK logic by ourselves. In AT mode, WE do not need to do the COMMIT and rollback. Seata does it for us automatically.

4. Summary

In this article, Songo will share with you the TCC model in Seata. We suggest that you must run the case in the article first, and then read the analysis, it is easy to understand ~

Other solutions for distributed transactions will be discussed later

Public number jiangnan little rain background reply seata-demo, you can download this case.