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 network exception
During the whole process of global transactions, various network exceptions may occur in TCC, such as null rollback, idempotent and suspension. Because the exceptions of TCC are similar to SAGA, reliable message and other transaction modes, we put all the solutions to exceptions in this article Are you still troubled by the network anomaly of distributed transactions? A function call helps you handle it Explain
TCC practice
For the previous inter-bank transfer operation, the simplest way is to adjust the balance in the Try stage, reverse the balance in the Cancel stage, and leave it blank in the Confirm stage. The problem with this is that if A deducts successfully, the amount transferred to B fails, and finally rolls back to adjust the balance of A to the initial value. In this process, if A finds that his balance has been deducted, but the payee 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 unfreezing of funds, so that the data users see at any stage is clear.
Let's develop a TCC transaction
Our example uses the Java language and the distributed transaction framework is https://github.com/yedf/dtm , its support for distributed transactions is elegant. Let's explain the composition of TCC in detail
First, we create two tables, one is the user balance table and the other is the frozen fund table. The statement for creating the table is as follows:
create table if not exists dtm_busi.user_account( id int(11) PRIMARY KEY AUTO_INCREMENT, user_id int(11) UNIQUE, balance DECIMAL(10, 2) not null default '0', trading_balance DECIMAL(10, 2) not null default '0', create_time datetime DEFAULT now(), update_time datetime DEFAULT now(), key(create_time), key(update_time) );
In the table, trading_balance records the amount being traded.
We first write the core code to freeze / unfreeze funds, and check the constraint balance + trading_ Balance > = 0. If the constraint is not tenable, the execution fails
public void adjustTrading(Connection connection, TransReq transReq) throws Exception { String sql = "update dtm_busi.user_account set trading_balance=trading_balance+?" + " where user_id=? and trading_balance + ? + balance >= 0"; PreparedStatement preparedStatement = null; try { preparedStatement = connection.prepareStatement(sql); preparedStatement.setInt(1, transReq.getAmount()); preparedStatement.setInt(2, transReq.getUserId()); preparedStatement.setInt(3, transReq.getAmount()); if (preparedStatement.executeUpdate() > 0) { System.out.println("Transaction amount updated successfully"); } else { throw new FailureException("Transaction failed"); } } finally { if (null != preparedStatement) { preparedStatement.close(); } } }
Then adjust the balance
public void adjustBalance(Connection connection, TransReq transReq) throws SQLException { PreparedStatement preparedStatement = null; try { String sql = "update dtm_busi.user_account set trading_balance=trading_balance-?,balance=balance+? where user_id=?"; preparedStatement = connection.prepareStatement(sql); preparedStatement.setInt(1, transReq.getAmount()); preparedStatement.setInt(2, transReq.getAmount()); preparedStatement.setInt(3, transReq.getUserId()); if (preparedStatement.executeUpdate() > 0) { System.out.println("Balance updated successfully"); } } finally { if (null != preparedStatement) { preparedStatement.close(); } } }
Let's write a specific Try/Confirm/Cancel processing function
@RequestMapping("barrierTransOutTry") public Object TransOutTry(HttpServletRequest request) throws Exception { BranchBarrier branchBarrier = new BranchBarrier(request.getParameterMap()); logger.info("barrierTransOutTry branchBarrier:{}", branchBarrier); TransReq transReq = extracted(request); Connection connection = dataSourceUtil.getConnecion(); branchBarrier.call(connection, (barrier) -> { System.out.println("user: +" + transReq.getUserId() + ",Transfer out" + Math.abs(transReq.getAmount()) + "Yuan preparation"); this.adjustTrading(connection, transReq); }); connection.close(); return TransResponse.buildTransResponse(Constant.SUCCESS_RESULT); } @RequestMapping("barrierTransOutConfirm") public Object TransOutConfirm(HttpServletRequest request) throws Exception { BranchBarrier branchBarrier = new BranchBarrier(request.getParameterMap()); logger.info("barrierTransOutConfirm branchBarrier:{}", branchBarrier); Connection connection = dataSourceUtil.getConnecion(); TransReq transReq = extracted(request); branchBarrier.call(connection, (barrier) -> { System.out.println("user: +" + transReq.getUserId() + ",Transfer out" + Math.abs(transReq.getAmount()) + "Meta submission"); adjustBalance(connection, transReq); }); connection.close(); return TransResponse.buildTransResponse(Constant.SUCCESS_RESULT); } @RequestMapping("barrierTransOutCancel") public Object TransOutCancel(HttpServletRequest request) throws Exception { BranchBarrier branchBarrier = new BranchBarrier(request.getParameterMap()); logger.info("barrierTransOutCancel branchBarrier:{}", branchBarrier); TransReq transReq = extracted(request); Connection connection = dataSourceUtil.getConnecion(); branchBarrier.call(connection, (barrier) -> { System.out.println("user: +" + transReq.getUserId() + ",Transfer out" + Math.abs(transReq.getAmount()) + "Meta rollback"); this.adjustTrading(connection, transReq); }); connection.close(); return TransResponse.buildTransResponse(Constant.SUCCESS_RESULT); } // The TransIn correlation function is similar to TransOut and is omitted here
At this point, the processing functions of each sub transaction are OK, and then start the TCC transaction for branch calls
@RequestMapping("tccBarrier") public String tccBarrier() { // Create dmt client DtmClient dtmClient = new DtmClient(ipPort); //Create tcc transaction try { dtmClient.tccGlobalTransaction(dtmClient.genGid(), TccTestController::tccBarrierTrans); } catch (Exception e) { log.error("tccGlobalTransaction error", e); return "fail"; } return "success"; } public static void tccBarrierTrans(Tcc tcc) throws Exception { // User 1 transferred out 30 yuan Response outResponse = tcc .callBranch(new TransReq(1, -30), svc + "/barrierTransOutTry", svc + "/barrierTransOutConfirm", svc + "/barrierTransOutCancel"); log.info("outResponse:{}", outResponse); // User 2 transferred 30 yuan Response inResponse = tcc .callBranch(new TransReq(2, 30), svc + "/barrierTransInTry", svc + "/barrierTransInConfirm", svc + "/barrierTransInCancel"); log.info("inResponse:{}", inResponse); }
So far, a complete TCC distributed transaction is written.
If you want to run a successful example in its entirety, follow dtmcli-java-sample After setting up the environment and starting up, run the following command to run the example of tcc
curl http://localhost:8081/tccBarrier
Rollback of TCC
What if the bank finds that the account of user 2 is abnormal and fails to return when transferring the amount out of user 2? In our example, the balance of available users is 10000, and initiating a transfer of 100000 will trigger an exception and fail:
curl http://localhost:8081/tccBarrier
This is a sequence diagram of transaction failure interactions
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.
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 idempotent, dangling and null compensation to be handled in distributed transactions, please refer to another article: Distributed transaction you can not know the pit, a function call to help you handle it
For more comprehensive knowledge of distributed transactions, please refer to Seven classic solutions for distributed transactions
The examples used in this paper are selected from yedf/dtmcli-java-sample It supports multiple transaction modes: TCC, SAGA, XA, cross language support for transaction messages, and supports clients in golang, python, PHP, nodejs and other languages. It provides sub transaction barrier function to gracefully solve idempotent, suspension, null compensation and other problems.
After reading this article, welcome to visit https://github.com/yedf/dtm Project, give star support!