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

背景

银行跨行转账业务是一个典型分布式事务场景,假设 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 时序图。

22bdb6b3ad3561cd6d41a532706dc916.png

可以看到它的流程和 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 的环境后,运行上面的例子,会看到下面的输出。

75b537a818eea6a0cedd0c05205012e2.png

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

下面来看一个 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());
});

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

dd438f254d3fd553b619b667e2af4dde.png

这个跟成功的 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 的操作。

运行结果如下:

5948de1c006414987a7ed311ddacf92e.png

重点看三个地方,

  • 转入的 Cancel 操作并没有执行,因为这里模拟的是转入失败的情况,子事务屏障判定为空补偿了

  • 没有输出分支调用的结果,是因为执行第二个分支的时候没有返回成功的结果

  • 输出的提交结果为空,表明这个事务是失败的,成功的话会返回这个事务的 gid

写在最后

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

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

本文示例代码:https://github.com/catcherwong-archive/2022/tree/main/DtmTccDemo

参考资料

  • https://segmentfault.com/a/1190000040331793

  • https://segmentfault.com/a/1190000040396649

  • https://github.com/dtm-labs/dtmcli-csharp

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/291674.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Xcode6.1 模拟器路径

Xcode 5的iOS模拟器的应用的目录是在~/Library/Application Support/iPhone Simulator/<iOS_Version>/Applications/{Application_ID} Xcode 6的目录改为~/Library/Developer/CoreSimulator/Devices/{Device_ID}/data/Containers/Bundle/Application/{Application_ID}/这…

新年伊始 .Net7 preview1 发布!

虎年伊始&#xff0c;.NET 7.0就要来了&#xff0c;还学的动吗&#xff1f;从github能看到&#xff0c;截止到2月8号&#xff0c;.NET 7.0 Preview1已经全部开发完成&#xff0c;连Preview2也完成了85%&#xff0c;这进度杠杠的&#xff01;微软这几年大力推进.NET稳定更新&…

数据挖掘课程实验(8个实验报告)

是从实验一到实验八的 链接&#xff1a;https://download.csdn.net/download/qq_44872173/15558967

hutol json null值没了_JSON数据处理框架Jackson精解第一篇-序列化与反序列化核心用法...

Jackson是Spring Boot默认的JSON数据处理框架&#xff0c;但是其并不依赖于任何的Spring 库。有的小伙伴以为Jackson只能在Spring框架内使用&#xff0c;其实不是的&#xff0c;没有这种限制。它提供了很多的JSON数据处理方法、注解&#xff0c;也包括流式API、树模型、数据绑定…

Linux网络操作系统实验报告(1~12)

共12个 链接在此&#xff1a;https://download.csdn.net/download/qq_44872173/15559247 如下是目录&#xff1a; 实验一部分标题如下&#xff1a;

解读WPF中的Binding

1.Overview基于MVVM实现一段绑定大伙都不陌生&#xff0c;Binding是wpf整个体系中最核心的对象之一这里就来解读一下我花了纯两周时间有哪些秘密。这里我先提出几个问题应该是大家感兴趣的&#xff0c;如下&#xff1a;&#xff08;1&#xff09;INotifyPropertyChanged是如何被…

maven 导入数据库

2019独角兽企业重金招聘Python工程师标准>>> 一.mysql 配置 基本代码 1. pom.xml 文件配置:jeesite.property jdbc.typemysql jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/yaoshi?useUnicodetrue&characterEncodingutf-8 jdbc.user…

下载matlab安装包太慢_MATLAB 2020a商业数学中文版软件下载安装教程

【软件语言】&#xff1a;简体中文 【支持系统】&#xff1a;Win7/Win8/Win10【软件类别】&#xff1a;安装版【更新时间】&#xff1a;2020年5月15日【下载地址】&#xff1a;www.rjazbs.me/t-2945.html客服微信&#xff1a;rjazbsMATLAB是一款商业数学软件&#xff0c;用于算…

phpstorm+Xdebug断点调试PHP

运行环境&#xff1a; PHPSTORM版本 : 8.0.1 PHP版本 : 5.6.2 xdebug版本&#xff1a;php_xdebug-2.2.5-5.6-vc11-x86_64.dll ps : php版本和xdebug版本一定要相对应 1. PHP安装xdebug扩展 php.ini的配置&#xff0c;下面的配置仅供参考&#xff0c;路径要换成自己的&#xff0…

EF Core 6 新功能汇总(三)

在这篇文章中&#xff0c;我将重点介绍 EF Core 6 中 LINQ 查询功能的增强。这是 EF Core 6 新功能汇总的第三篇文章&#xff1a;EF Core 6 新功能汇总&#xff08;一&#xff09;EF Core 6 新功能汇总&#xff08;二&#xff09;EF Core 6 新功能汇总&#xff08;三&#xff0…

SpringMvc项目中使用GoogleKaptcha 生成验证码

前言&#xff1a;google captcha 是google生成验证码的一个工具类&#xff0c;其原理是将随机生成字符串保存到session中&#xff0c;同时以图片的形式返回给页面&#xff0c;之后前台页面提交到后台进行对比。 1、jar包准备 官方提供的pom应该是 <dependency> <grou…

wpsppt流程图联系效果_风险隐患排查的手段—HAZOP 与检查表的区别及应用效果

HAZOP 与检查表的区别HAZOP 分析可以在工厂运行周期内的任何时间段进行&#xff0c;既适用于设计阶段&#xff0c;也适用于在役的工艺装置。在化工项目的设计阶段采用HAZOP 方法进行分析&#xff0c;能识别设计、设备及操作程序中的潜在危险&#xff0c;比如装置设备是否装有安…

软件测试实验报告下载 实验一到实验五

实验一&#xff1a; 传送门在此&#xff1a;https://download.csdn.net/download/qq_44872173/15559951 目录如下&#xff1a;

linux网络编程之并发服务器的三种实现模型 (超级经典)

转载 &#xff1a; http://blog.csdn.net/tennysonsky/article/details/45671215 服务器设计技术有很多&#xff0c;按使用的协议来分有 TCP 服务器和 UDP 服务器&#xff0c;按处理方式来分有循环服务器和并发服务器。 循环服务器与并发服务器模型 在网络程序里面&#xff0c…

IT人的自我导向型学习:学习的4个层次

[原文链接] 谈起软件开发一定会想到用什么技术、采用什么框架&#xff0c;然而在盛行的敏捷之下&#xff0c;人的问题逐渐凸显出来。不少企业请人来培训敏捷开发技术&#xff0c;却发现并不能真正运用起来&#xff0c;其中一个主要原因就是大家还没有很好的学习能力。没有学习&…

动态ram依靠什么存储信息_ROM、RAM、DRAM、SRAM和FLASH傻傻分不清

ROM、RAM、DRAM、SRAM和FLASH各类储存器在电脑、手机、电子设备、嵌入式设备及相应的开发中普遍应用的&#xff0c;但是很多还是傻傻分不清楚。下面就简单介绍下这几个吧&#xff01;ROM和RAMROM&#xff1a;只读存储器或者固化存储器&#xff1b;RAM&#xff1a;随机存取存储器…

软件项目管理课后题下载【共5个章(1、3、4、5、6)】

都整理好了&#xff0c;链接在此&#xff1a;https://download.csdn.net/download/qq_44872173/15560093 目录如下&#xff1a;

linux c之snprintf()和sprintf()区别

1、snprintf函数 int snprintf(char *str, size_t size, const char *format, ...); 将可变个参数(...)按照format格式化成字符串,然后将其复制到str中 (1) 如果格式化后的字符串长度 < size,则将此字符串全部复制到str中,并给其后添加一个字符串结束符(/0); (2) 如果…