What is TCC? TCC stands for Try, Confirm and 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: Attempt execution, complete all business checks (conformance), reserve necessary business resources (quasi-isolation)
  • Confirm stage: If the Try for all branches succeeds, proceed to Confirm stage. Confirm actually performs the business, without any business checks, using only the business resources reserved in the Try phase
  • Cancel phase: If one of all the branch tries fails, proceed to the Cancel phase. Cancel frees the business resource reserved in the Try phase.

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

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

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

TCC network exception

During the whole process of TCC global transaction, various kinds of network anomalies may occur, typically including null rollback, idempotency and suspension. Because the abnormal conditions of TCC are similar to transaction modes such as SAGA and reliable message, etc., So we’ve put all the exception solutions in this article, “Are you still bothered by network exceptions in distributed transactions? A function call can help you fix it.

TCC practice

For the previous interbank transfer, the simplest approach is to adjust the balance during the Try phase, reverse the balance during the Cancel phase, and leave it empty during the Confirm phase. The problem with this approach is that if A succeeds in deducting money, the transfer to B fails, and the balance of A is finally rolled back and adjusted to the original value. In this process, if A finds that his balance has been deducted, but the receiver 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 funds to unfreeze, so that the user can see the data clearly at any stage.

Let’s go into the concrete development of a TCC transaction

Currently available for TCC open source framework, mainly Java language, with SeATA as the representative. Our example uses the Go language and the distributed transaction framework is DTM, which has very elegant support for distributed transactions. Now let’s explain the composition of TCC in detail

We will create two tables first, one is the user balance table, the other is the frozen funds table, the table sentence is as follows:

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() );

In the TRADING table, TRADING_BALANCE records the amount being traded.

If the constraint balance+trading_balance >= 0 is not valid, the execution fails. If the constraint is not valid, the execution fails

func adjustTrading(uid int, amount int) (interface{}, DBR := sdb.exec ("update dtm_busi. User_account_trading t join dtm_busi. User_account a on ") {error := sdb.exec ("update dtm_busi 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 DBR.Error == nil &&dbr. Rowsaffected == 0 {// If the amount is not sufficient, Return nil, FMT.Errorf("update error, balance not enough")}

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, -amount) 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 situations}

Let’s write the 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)
})

At this point, the handlers of each subtransaction are OK, and then the TCC transaction is opened for branching

/ / TccGlobalTransaction opens a global transaction _, err: = dtmcli. TccGlobalTransaction (DtmServer, Func (TCC *dtmcli. TCC) (rerr error) {// CallBranch registers the Confirm/Cancel of the transaction branch with the global transaction, Call Try res1, 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 /TransInRevert") to check for errors, return an error, roll back the transaction // If there is no error, When the function returns normally, the global transaction is committed and TM calls Confirm on each transaction branch to complete the transaction.

At this point, a complete TCC distributed transaction has been written.

If you want to fully run a successful example, then 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 the bank is going to transfer the amount to user 2 and finds that user 2’s account is abnormal and fails to return? We present a sequence diagram of the transaction failure interaction

The difference between this and a successful TCC is that when a subtransaction fails, the global transaction will be rolled back later, and the Cancel operation of each subtransaction will be called to ensure that all global transactions are rolled back.

summary

In this article, we introduced the theoretical knowledge of TCC, and also gave a complete process of writing a TCC transaction through an example, covering the normal successful completion and successful rollback. I believe readers will have a deep understanding of TCC through this article.

After reading this article, you are welcome to visit the DTM project and support Star!