背景

银行跨行转账业务是一个典型分布式事务场景,假设 A 需要跨行转账给 B,那么就涉及两个银行的数据,无法通过一个数据库的本地事务保证转账的 ACID ,只能够通过分布式事务来解决。

聊一聊如何用C#轻松完成一个SAGA分布式事务 中介绍了借助 DTM 用 SAGA 事务模式解决了上面的银行跨行转账业务。

这一篇我们就来看看如何用 TCC 的事务模式来处理这个问题。

什么是 TCC

TCC是Try、Confirm、Cancel三个词语的缩写,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。

TCC分为3个阶段

  • Try 阶段:尝试执行,完成所有业务检查(一致性), 预留必需的业务资源(准隔离性)
  • Confirm 阶段:如果所有分支的Try都成功了,则走到Confirm阶段。Confirm真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源
  • Cancel 阶段:如果所有分支的Try有一个失败了,则走到Cancel阶段。Cancel释放 Try 阶段预留的业务资源。

对于前面的跨行转账业务,最简单的做法是,在Try阶段调整余额,在Cancel阶段反向调整余额,Confirm阶段则空操作。这么做带来的问题是,如果A扣款成功,金额转入B失败,最后回滚,把A的余额调整为初始值。在这个过程中如果A发现自己的余额被扣减了,但是收款方B迟迟没有收到余额,那么会对A造成困扰。

更好的做法是,Try阶段冻结A转账的金额,Confirm进行实际的扣款,Cancel进行资金解冻,这样用户在任何一个阶段,看到的数据都是清晰明了的。

下面我们进行一个 TCC 事务的具体开发

前置工作

dotnet add package Dtmcli --version 0.4.0

注:相比 0.3.0,0.4.0 支持了 4 个新的特性,详见 https://github.com/dtm-labs/dtmcli-csharp/releases/tag/v0.4.0

成功的 TCC

先来看一下一个成功完成的 TCC 时序图。

可以看到它的流程和 SAGA 的还是有比较大的区别。

同样的,上图的微服务1,对应我们示例的 OutApi,也就是转钱出去的那个服务。

微服务2,对应我们示例的 InApi,也就是转钱进来的那个服务。

下面我们来编写两个服务的Try/Confirm/Cancel的处理。

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($"用户【{req.UserId}】转出【{req.Amount}】Try 操作,bb={bb}");
// tx 参数是事务,可和本地事务一起提交回滚
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($"用户【{req.UserId}】转出【{req.Amount}】Confirm操作,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($"用户【{req.UserId}】转出【{req.Amount}】Cancel操作,bb={bb}");
await Task.CompletedTask;
}); return Results.Ok(TransResponse.BuildSucceedResponse());
});

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($"用户【{req.UserId}】转入【{req.Amount}】Try操作,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($"用户【{req.UserId}】转入【{req.Amount}】Confirm操作,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($"用户【{req.UserId}】转入【{req.Amount}】Cancel操作,bb={bb}");
await Task.CompletedTask;
}); return Results.Ok(TransResponse.BuildSucceedResponse());
});

到此各个子事务的处理已经OK了,在上面的代码中,下面这几行是子事务屏障相关代码,只要按照这个方式来调用您的业务逻辑,子事务屏障保证重复请求、悬挂、空补偿情况出现时,您的业务逻辑不会被调用,保证了正常业务的正确进行

var bb = bbFactory.CreateBranchBarrier(context.Request.Query);
await bb.Call(db, async (tx) =>
{
// 业务操作...
});

然后准备开启 TCC 事务,进行分支调用

var cts = new CancellationTokenSource();

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

var res = await tccGlobalTransaction.Excecute(gid, async (tcc) =>
{
// 用户1 转出30元
var res1 = await tcc.CallBranch(userOutReq, outApi + "/TransOutTry", outApi + "/TransOutConfirm", outApi + "/TransOutCancel", cts.Token); // 用户2 转入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 提交结果 = {res}");

到这里,一个完整的 TCC 分布式事务就编写完成了。

需要注意的地方:

  1. 依赖 TccGlobalTransaction ,这个是单例的
  2. tcc 的 CallBranch 方法就是事务分支的调用

搭建好 dtm 的环境后,运行上面的例子,会看到下面的输出。

成功的示例都是相对比较简单的。

下面来看一个 TCC 回滚的例子。

TCC 的回滚

假如银行将金额准备转入用户2时,发现用户2的账户异常,返回失败,会怎么样?我们修改代码,模拟这种情况:

在 InApi 加多一个转入Try失败的处理接口

app.MapPost("/api/TransInTryError", (IBranchBarrierFactory bbFactory, HttpContext context, TransRequest req) =>
{
var bb = bbFactory.CreateBranchBarrier(context.Request.Query); Console.WriteLine($"用户【{req.UserId}】转入【{req.Amount}】Try--失败,bb={bb}"); return Results.Ok(TransResponse.BuildFailureResponse());
});

再来看一下事务失败交互的时序图

这个跟成功的 TCC 差别就在于,当某个子事务返回失败后,后续就回滚全局事务,调用各个子事务的 Cancel 操作,保证全局事务全部回滚。

再调整一下调用方,把转入 Try 操作替换成上面这个返回错误的接口。

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 提交结果 = {res}");

需要注意的是 CallBranch 方法在对应的微服务返回失败后会抛出异常,进而触发全局事务的回滚操作,这个时候 dtm 才会触发 Cancel 的操作。

运行结果如下:

重点看三个地方,

  • 转入的 Cancel 操作并没有执行,因为这里模拟的是转入失败的情况,子事务屏障判定为空补偿了
  • 没有输出分支调用的结果,是因为执行第二个分支的时候没有返回成功的结果
  • 输出的提交结果为空,表明这个事务是失败的,成功的话会返回这个事务的 gid

写在最后

在这篇文章里,通过 2 个简单的例子,完整给出了编写一个 TCC 事务的过程,涵盖了正常成功完成,异常回滚的情况。

希望对研究分布式事务的您有所帮助。

本文示例代码: DtmTccDemo

参考资料

聊一聊如何用C#轻松完成一个TCC分布式事务的更多相关文章

  1. 聊一聊如何用C#轻松完成一个SAGA分布式事务

    背景 银行跨行转账业务是一个典型分布式事务场景,假设 A 需要跨行转账给 B,那么就涉及两个银行的数据,无法通过一个数据库的本地事务保证转账的 ACID ,只能够通过分布式事务来解决. 市面上使用比较 ...

  2. 终于有人把“TCC分布式事务”实现原理讲明白了!

    之前网上看到很多写分布式事务的文章,不过大多都是将分布式事务各种技术方案简单介绍一下.很多朋友看了还是不知道分布式事务到底怎么回事,在项目里到底如何使用. 所以这篇文章,就用大白话+手工绘图,并结合一 ...

  3. TCC分布式事务的实现原理(转载 石杉的架构笔记)

    拜托,面试请不要再问我TCC分布式事务的实现原理![石杉的架构笔记] 原创: 中华石杉 目录 一.写在前面 二.业务场景介绍 三.进一步思考 四.落地实现TCC分布式事务 (1)TCC实现阶段一:Tr ...

  4. 拜托,面试请不要再问我TCC分布式事务的实现原理!(转)

    一.写在前面 之前网上看到很多写分布式事务的文章,不过大多都是将分布式事务各种技术方案简单介绍一下.很多朋友看了不少文章,还是不知道分布式事务到底怎么回事,在项目里到底如何使用. 所以咱们这篇文章,就 ...

  5. 【转载】终于有人把“TCC分布式事务”的实现原理讲明白了

    之前网上看到很多写分布式事务的文章,不过大多都是将分布式事务各种技术方案简单介绍一下.很多朋友看了还是不知道分布式事务到底怎么回事,在项目里到底如何使用. 所以这篇文章,就用大白话+手工绘图,并结合一 ...

  6. 终于有人把“TCC分布式事务”实现原理讲明白了

    所以这篇文章,就用大白话+手工绘图,并结合一个电商系统的案例实践,来给大家讲清楚到底什么是 TCC 分布式事务. 首先说一下,这里可能会牵扯到一些 Spring Cloud 的原理,如果有不太清楚的同 ...

  7. 关于如何实现一个Saga分布式事务框架的思考

    关于Saga模式的介绍,已经有一篇文章介绍的很清楚了,链接在这里:分布式事务:Saga模式. 关于TCC模式的介绍,也已经有一篇文章介绍的很清楚了,链接在这里:关于如何实现一个TCC分布式事务框架的一 ...

  8. TCC分布式事务的实现原理

    目录 一.写在前面 二.业务场景介绍 三.进一步思考 四.落地实现TCC分布式事务 (1)TCC实现阶段一:Try (2)TCC实现阶段二:Confirm (3)TCC实现阶段三:Cancel 五.总 ...

  9. tcc分布式事务框架解析

    前言碎语 楼主之前推荐过2pc的分布式事务框架LCN.今天来详细聊聊TCC事务协议. 2pc实现:https://github.com/codingapi/tx-lcn tcc实现:https://g ...

随机推荐

  1. [Kaiming]Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification

    目录 概 主要内容 PReLU Kaiming 初始化 Forward case Backward case He K, Zhang X, Ren S, et al. Delving Deep int ...

  2. 关于使用JupyterNotebook运行代码运行到一半会闪退的问题

    前几个星期使用Jupyter远程使用师兄的电脑跑了一段沐老师的代码学习,但是总是跑了一段就闪退,同时因为是远程桌面,远程桌面也被挤掉了. 具体表现就是:运行代码--表现正常,打印记录--远程桌面掉线- ...

  3. Java初学者作业——编写程序计算实发工资(实践1)

    返回本章节 返回作业目录 需求说明: 腾讯为Java工程师提供了基本工资(8000元).物价津贴及房租津贴.其中物价津贴为基本工资的40%,房屋津贴为基本工资的25%.要求编写程序计算实发工资. 实现 ...

  4. Java初学者作业——编写Java程序,根据输入的某个班级的学员成绩,计算该班级学员的平均成绩,要求输入班级的人数。

    返回本章节 返回作业目录 需求说明: 编写Java程序,根据输入的某个班级的学员成绩,计算该班级学员的平均成绩,要求输入班级的人数. 实现思路: 声明变量sum.count以及avg用于存储总成绩.班 ...

  5. nexus私服SNAPSHOT仓库maven-metadata.xml缺失导致的Could not find artifact:***.jar

    环境:maven项目,使用Nexus私服(ip:192.168.10.100),jenkins实现代码的编译和打包. 问题分析思路:在2021年元旦假期前,jenkins上的编译打包任务一直正常工作, ...

  6. SpringBoot 之 Dao层模拟数据库操作

    单表操作: # src/main/java/com/wu/dao/DepartmentDao .java @Repository public class DepartmentDao { privat ...

  7. VirtualBox 虚拟机怎样设置共享文件夹

    首次在VirtualBox装完系统后,很经常用到的操作就是:想将主机的东西拉倒虚拟机进行使用或安装,那怎么将主机的文件拿到虚拟机呢? 1.在虚拟机 > 设置中选择 >安装增强功能,经过这个 ...

  8. Pytest_用例分组(6)

    用例分组 pytest进行分组测试的方法是使用装饰器 @pytest.mark.标记名称,被标记为相同名称的用例可以看做为同一个组. 分组用例的运行方式是在执行命令中追加 -m "标记名称& ...

  9. mutation中修改state中的状态值,却报[vuex] do not mutate vuex store state outside mutation handlers.

    网上百度说是在mutation外修改state中的状态值,会报下列错误,可我明明在mutations中修改的状态值,还是报错 接着百度,看到和我类似的问题,说mutations中只能用同步代码,异步用 ...

  10. Python常用功能函数系列总结(二)

     本节目录 常用函数一:sel文件转换 常用函数二:refwork文件转换 常用函数三:xml文档解析 常用函数四:文本分词 常用函数一:sel文件转换 sel是种特殊的文件格式,具体应用场景的话可以 ...