What is TCC? TCC is short for Try, Confirm, Cancel. It was first proposed by Pat Helland in a paper titled Life Beyond Distributed Transactions: An Apostate’s Opinion published in 2007.

Of TCC

TCC is divided into three phases

  • Try phase: Attempts to execute, completes all service checks (consistency), and reserves required service resources (quasi-isolation).
  • Confirm phase: If all branch tries are successful, the Confirm phase is entered. Confirm Indicates that services are actually executed without any service check. Service resources reserved in the Try phase are used
  • Cancel phase: If one of the tries for all branches fails, the Cancel phase is entered. Cancel Releases service resources reserved in the Try phase.

In TCC distributed transactions, there are three roles, as in classic XA distributed transactions:

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

If we were to perform a business similar to an inter-bank transfer, with a TransOut and a TransIn in different microservices, the typical sequence diagram for a successfully completed TCC transaction is as follows:

The TCC network is abnormal

During the whole global transaction process of TCC, various kinds of network exceptions may occur, typical of which are empty rollback, idempotentality and suspension. Due to the exceptions of TCC, they are similar to transaction modes such as SAGA and reliable message. That’s why we’ve put all the exception solutions in this article, “Still bothered by network exceptions for distributed transactions? One function call will fix it for you.

TCC practice

For the previous inter-bank transfer, the simplest approach is to adjust the balance during the Try phase, reverse adjust the balance during the Cancel phase, and leave the Confirm phase empty. The problem with this is that if A succeeds in the deduction and the amount is transferred to B, it will be rolled back to adjust A’s balance to the original value. In this process, if A finds that her balance has been deducted, but the payee B has not received the balance, it will cause trouble to A.

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

Let’s proceed to the specific development of a TCC transaction

The main open source frameworks available for TCC are the Java language, represented by SEATA. Our example uses the GO language and the distributed transaction framework DTM, which has elegant support for distributed transactions. Let’s look at the TCC in more detail

Create two tables, one for user balance and one for frozen funds.

CREATE TABLE dtm_busi.`user_account` ( `id` int(11) AUTO_INCREMENT PRIMARY KEY, `user_id` int(11) not NULL UNIQUE , 'balance' decimal(10,2) NOT NULL DEFAULT '0.00', 'create_time' datetime DEFAULT now(), `update_time` datetime DEFAULT now() ); CREATE TABLE dtm_busi.`user_account_trading` ( `id` int(11) AUTO_INCREMENT PRIMARY KEY, 'user_id' int(11) not NULL UNIQUE, 'trading_balance' decimal(10,2) not NULL DEFAULT '0.00', `create_time` datetime DEFAULT now(), `update_time` datetime DEFAULT now() );Copy the code

In the trading table, trading_balance records the amount of money being traded.

If the constraint balance+trading_balance >= 0 is not valid, the execution will fail

func adjustTrading(uid int, amount int) (interface{}, DBR := sdb.exec ("update dtM_busi. user_account_trading t join dtm_busi.user_account a on t.user_id=a.user_id and t.user_id=? set t.trading_balance=t.trading_balance + ? where a.balance + t.trading_balance + ? >= 0", uid, amount, amount) if dcr. Error == nil && dcr. RowsAffected == 0 {// If the balance is not enough, Return nil, fmt.Errorf("update error, balance not enough")}Copy the code

Then you adjust the balance

Func adjustBalance(uid int, amount int) (RET interface{}, rerr error) DBR := db.exec ("update dtM_busi. user_account_trading t join dtM_busi. user_account a on t.user_id=a.user_id and t.user_id=? set t.trading_balance=t.trading_balance + ?" , uid, DBR = db.exec ("update dtm_busi.user_account set ") if DBR.Error == nil && DBR.RowsAffected == 1 {DBR = db.exec ("update dtm_busi.user_account set" balance=balance+? where user_id=?" , amount, uid)} Check and handle other cases}Copy the code

Let’s write a specific Try/Confirm/Cancel handler

RegisterPost(app, "/api/TransInTry", func (c *gin.Context) (interface{}, error) {
  return adjustTrading(1, reqFrom(c).Amount)
})
RegisterPost(app, "/api/TransInConfirm", func TransInConfirm(c *gin.Context) (interface{}, error) {
  return adjustBalance(1, reqFrom(c).Amount)
})
RegisterPost(app, "/api/TransInCancel", func TransInCancel(c *gin.Context) (interface{}, error) {
  return adjustTrading(1, -reqFrom(c).Amount)
})

RegisterPost(app, "/api/TransOutTry", func TransOutTry(c *gin.Context) (interface{}, error) {
  return adjustTrading(2, -reqFrom(c).Amount)
})
RegisterPost(app, "/api/TransOutConfirm", func TransInConfirm(c *gin.Context) (interface{}, error) {
  return adjustBalance(2, -reqFrom(c).Amount)
})
RegisterPost(app, "/api/TransOutCancel", func TransInCancel(c *gin.Context) (interface{}, error) {
  return adjustTrading(2, reqFrom(c).Amount)
})

Copy the code

At this point, the handling function of each subtransaction is OK, then the TCC transaction is opened and the branch call is made

/ / TccGlobalTransaction opens a global transaction _, err: = dtmcli. TccGlobalTransaction (DtmServer, Func (TCC * dtmcli.tcc) (rerr error) {// CallBranch registers Confirm/Cancel for the global transaction, Rerr := tcc.callbranch (&transreq {Amount: 30}, host+"/ API /TransOutTry", host+"/ API /TransOutConfirm", host+"/ API /TransOutRevert" Res2, rerr := tcc.callBranch (&transreq {Amount: 30}, host+"/ API /TransInTry", host+"/ API /TransInConfirm", host+"/ API /TransInConfirm") After the function returns normally, the global transaction is committed and TM calls Confirm on each transaction branch to complete the entire transaction})Copy the code

At this point, a complete TCC distributed transaction is written.

If you want to run a complete successful example, after setting up the environment as described in the DTM project, run the following command to run the TCC example

go run app/main.go tcc_barrier

TCC rolled back

What if when the bank is preparing to transfer the amount to user 2, it finds that the account of user 2 is abnormal and fails to return? We present a sequence diagram of a transaction failure interaction

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

summary

In this article, we introduce the theory of TCC and, with an example, complete the process of writing a TCC transaction, covering both normal successful completion and successful rollback. I believe that readers have a deep understanding of TCC through this article.

For a more comprehensive knowledge of distributed transactions, please refer to the seven classic solutions for distributed transactions

After reading this article, welcome to visit the DTM project, give a star support!