background

Inter-bank transfer business is A typical distributed transaction scenario. Assuming that A needs to transfer inter-bank transfer to B, the data of two banks will be involved. The ACID of transfer cannot be guaranteed by local transaction of one database, but can only be solved by distributed transaction.

In a chat about how to use C# to easily complete a SAGA distributed transaction, introduced DTM with SAGA transaction mode to solve the above inter-bank transfer business.

In this article we will look at how TCC’s transaction mode can be used to solve this problem.

What is the 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.

TCC is divided into three phases

  • Try phase: Try execution, complete all service checks (consistency), reserve necessary service resources (quasi-isolation)
  • Confirm phase: If all branch tries are successful, the Confirm phase is reached. Confirm Performs services without any service check and uses only the service resources reserved in the Try phase
  • Cancel phase: If one of the tries of all branches fails, the Cancel phase is performed. Cancel Releases service resources reserved during the Try phase.

For the previous inter-bank transfer business, the simplest way is to adjust the balance in the Try phase, reverse the balance in the Cancel phase, and empty the Confirm phase. The problem with this is that if A is successfully deducted, the amount transferred to B fails, and the balance of A is eventually rolled back to its original value. In this process, if A finds that her balance has been deducted, but the receiver B has not received the balance, it will cause trouble to A.

It is better to freeze the amount of transfer A at the Try stage, Confirm for the actual deduction, and Cancel for the unfreezing of funds, so that the user can see the data clearly at any stage.

Now let’s do a specific development of TCC transactions

Pre – work

Dotnet add Package Dtmcli --version 0.4.0Copy the code

Note: compared to 0.3.0, 0.4.0 supports 4 new features, see github.com/dtm-labs/dt…

The success of TCC

Let’s take a look at a successfully completed TCC sequence diagram.

You can see that its process is quite different from SAGA’s.

Similarly, microservice 1 in the figure above corresponds to OutApi of our example, which is the service that transfers money out.

Microservice 2, which corresponds to InApi for our example, is the service that transfers money in.

Let’s write the Try/Confirm/Cancel handlers for the two services.

OutApi

app.MapPost("/api/TransOutTry".async (IBranchBarrierFactory bbFactory, HttpContext context, TransRequest req) => 
{
    var bb = bbFactory.CreateBranchBarrier(context.Request.Query);

    using var db = Db.GeConn();
    await bb.Call(db, async (tx) =>
    {
        Console.WriteLine($" users"{req.UserId}"Roll out"{req.Amount}Try operation, bb={bb}");
        // The tx argument is a transaction that can be committed and rolled back along with the local transaction
        await Task.CompletedTask;
    });

    return Results.Ok(TransResponse.BuildSucceedResponse());
});

app.MapPost("/api/TransOutConfirm".async (IBranchBarrierFactory bbFactory, HttpContext context, TransRequest req) =>
{
    var bb = bbFactory.CreateBranchBarrier(context.Request.Query);

    using var db = Db.GeConn();
    await bb.Call(db, async (tx) =>
    {
        Console.WriteLine($" users"{req.UserId}"Roll out"{req.Amount}Confirm operation, bb={bb}");
        await Task.CompletedTask;
    });

    return Results.Ok(TransResponse.BuildSucceedResponse());
});

app.MapPost("/api/TransOutCancel".async (IBranchBarrierFactory bbFactory, HttpContext context, TransRequest req) =>
{
    var bb = bbFactory.CreateBranchBarrier(context.Request.Query);

    using var db = Db.GeConn();
    await bb.Call(db, async (tx) =>
    {
        Console.WriteLine($" users"{req.UserId}"Roll out"{req.Amount}Cancel operation, bb={bb}");
        await Task.CompletedTask;
    });

    return Results.Ok(TransResponse.BuildSucceedResponse());
});
Copy the code

InApi


app.MapPost("/api/TransInTry".async (IBranchBarrierFactory bbFactory, HttpContext context, TransRequest req) =>
{
    var bb = bbFactory.CreateBranchBarrier(context.Request.Query);

    using var db = Db.GeConn();
    await bb.Call(db, async (tx) =>
    {
        Console.WriteLine($" users"{req.UserId}"Into"{req.Amount}Try operation, bb={bb}");
        await Task.CompletedTask;
    });

    return Results.Ok(TransResponse.BuildSucceedResponse());
});

app.MapPost("/api/TransInConfirm".async (IBranchBarrierFactory bbFactory, HttpContext context, TransRequest req) =>
{
    var bb = bbFactory.CreateBranchBarrier(context.Request.Query);

    using var db = Db.GeConn();
    await bb.Call(db, async (tx) =>
    {
        Console.WriteLine($" users"{req.UserId}"Into"{req.Amount}Confirm operation, bb={bb}");
        await Task.CompletedTask;
    });

    return Results.Ok(TransResponse.BuildSucceedResponse());
});

app.MapPost("/api/TransInCancel".async (IBranchBarrierFactory bbFactory, HttpContext context, TransRequest req) =>
{
    var bb = bbFactory.CreateBranchBarrier(context.Request.Query);

    using var db = Db.GeConn();
    await bb.Call(db, async (tx) =>
    {
        Console.WriteLine($" users"{req.UserId}"Into"{req.Amount}Cancel operation, bb={bb}");
        await Task.CompletedTask;
    });

    return Results.Ok(TransResponse.BuildSucceedResponse());
});
Copy the code

That each transaction processing is OK, in the code above, the following lines is the child transaction barrier related code, just follow this way to invoke your business logic, ensure repeat request transaction barrier, hanging, empty compensation happens, your business logic will not be called, to ensure the normal business properly

var bb = bbFactory.CreateBranchBarrier(context.Request.Query);
await bb.Call(db, async (tx) =>
{
    // Business operation...
});
Copy the code

Then you are ready to start the TCC transaction and make the branch call

var cts = new CancellationTokenSource();

var gid = await dtmClient.GenGid(cts.Token);

var res = await tccGlobalTransaction.Excecute(gid, async (tcc) =>
{
    // User 1 transfers out 30 yuan
    var res1 = await tcc.CallBranch(userOutReq, outApi + "/TransOutTry", outApi + "/TransOutConfirm", outApi + "/TransOutCancel", cts.Token);

    // User 2 transfers to $30
    var res2 = await tcc.CallBranch(userInReq, inApi + "/TransInTry", inApi + "/TransInConfirm", inApi + "/TransInCancel", cts.Token);
    
    Console.WriteLine($"case1, branch-out-res= {res1} branch-in-res= {res2}");
}, cts.Token);

Console.WriteLine($"case1, {gid}TCC submission result ={res}");
Copy the code

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

Points to note:

  1. Depending on TccGlobalTransaction, this is singleton
  2. TCC’s CallBranch method is a call to the transaction branch

After setting up the DTM environment, run the above example and see the following output.

Successful examples are relatively simple.

Let’s look at an example of a TCC rollback.

TCC rolled back

If the bank prepares to transfer the amount to user 2, it finds that the account of user 2 is abnormal and returns failure, what will happen? We modify the code to simulate this situation:

Add an interface to InApi to handle incoming Try failures

app.MapPost("/api/TransInTryError", (IBranchBarrierFactory bbFactory, HttpContext context, TransRequest req) =>
{
    var bb = bbFactory.CreateBranchBarrier(context.Request.Query);

    Console.WriteLine($" users"{req.UserId}"Into"{req.Amount}Try-- failed, bb={bb}");

    return Results.Ok(TransResponse.BuildFailureResponse());
});
Copy the code

Again, take a look at the sequence diagram of transaction failure interactions

This differs from successful TCC in that when a sub-transaction returns with failure, the global transaction is subsequently rolled back, invoking the Cancel operation of each sub-transaction to ensure that all global transactions are rolled back.

Adjust the caller again to replace the call into Try with the interface above that returns an error.

var cts = new CancellationTokenSource();

var gid = await dtmClient.GenGid(cts.Token);

var res = await tccGlobalTransaction.Excecute(gid, async (tcc) =>
{
    var res1 = await tcc.CallBranch(userOutReq, outApi + "/TransOutTry", outApi + "/TransOutConfirm", outApi + "/TransOutCancel", cts.Token);
    var res2 = await tcc.CallBranch(userInReq, inApi + "/TransInTryError", inApi + "/TransInConfirm", inApi + "/TransInCancel", cts.Token);

    Console.WriteLine($"case2, branch-out-res= {res1} branch-in-res= {res2}");
}, cts.Token);

Console.WriteLine($"case2, {gid}TCC submission result ={res}");
Copy the code

Note that DTM will trigger Cancel only when the CallBranch method throws an exception if the microservice fails to return, which triggers the rollback of the global transaction.

The running results are as follows:

Focus on three areas,

  • The Cancel operation for roll-in was not performed because it simulates a roll-in failure and the subtransaction barrier determines that it is null compensated
  • The result of the branch call is not printed because the second branch does not return a successful result
  • The output commit result is empty, indicating that the transaction failed, and the gid of the transaction is returned on success

Write in the last

In this article, the entire process of writing a TCC transaction is presented through two simple examples, covering normal successful completion and exception rollback.

I hope it will be helpful for you to study distributed transactions.

This article sample code: DtmTccDemo

The resources

  • Easily complete a TCC distributed transaction, nanny level tutorial with Go
  • Complete a distributed transaction TCC tutorial with Python
  • dtm-labs/dtmcli-csharp

Follow my public account “Bucai Old Huang” and share with you what huang sees and hears in the first time.