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

背景

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

市面上使用比较多的分布式事务框架,支持 SAGA 的,大部分都是 JAVA 为主的,没有提供 C# 的对接方式,或者是对接难度大,一定程度上让人望而却步。

这里推荐一下叶东富大佬的分布式事务框架 dtm,一款跨语言的开源分布式事务管理器,优雅的解决了幂等、空补偿、悬挂等分布式事务难题。提供了简单易用、高性能、易水平扩展的分布式事务解决方案。

老黄在搜索相关分布式事务资料的时候,他写的文章都是相对比较好理解的,也就是这样关注到了 dtm 这个项目。

下面就基于这个框架来实践一下银行转账的例子。

前置工作

dotnet add package Dtmcli --version 0.3.0

成功的 SAGA

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

cbbc76e9685c8eba213d587e92adf261.png

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

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

下面是两个服务的正向操作和补偿操作的处理。

OutApi

app.MapPost("/api/TransOut", (string branch_id, string gid, string op, TransRequest req) => 
{// 进行 数据库操作Console.WriteLine($"用户【{req.UserId}】转出【{req.Amount}】正向操作,gid={gid}, branch_id={branch_id}, op={op}");return Results.Ok(TransResponse.BuildSucceedResponse());
});app.MapPost("/api/TransOutCompensate", (string branch_id, string gid, string op, TransRequest req) =>
{// 进行 数据库操作Console.WriteLine($"用户【{req.UserId}】转出【{req.Amount}】补偿操作,gid={gid}, branch_id={branch_id}, op={op}");return Results.Ok(TransResponse.BuildSucceedResponse());
});

InApi

app.MapPost("/api/TransIn", (string branch_id, string gid, string op, TransRequest req) =>
{Console.WriteLine($"用户【{req.UserId}】转入【{req.Amount}】正向操作,gid={gid}, branch_id={branch_id}, op={op}");return Results.Ok(TransResponse.BuildSucceedResponse());
});app.MapPost("/api/TransInCompensate", (string branch_id, string gid, string op, TransRequest req) =>
{Console.WriteLine($"用户【{req.UserId}】转入【{req.Amount}】补偿操作,gid={gid}, branch_id={branch_id}, op={op}");return Results.Ok(TransResponse.BuildSucceedResponse());
});

注:示例为了简单,没有进行实际的数据库操作。

到此各个子事务的处理已经 OK 了,然后是开启 SAGA 事务,进行分支调用

var userOutReq = new TransRequest() { UserId = "1", Amount = -30 };
var userInReq = new TransRequest() { UserId = "2", Amount = 30 };var ct = new CancellationToken();
var gid = await dtmClient.GenGid(ct);
var saga = new Saga(dtmClient, gid).Add(outApi + "/TransOut", outApi + "/TransOutCompensate", userOutReq).Add(inApi + "/TransIn", inApi + "/TransInCompensate", userInReq);var flag = await saga.Submit(ct);Console.WriteLine($"case1, {gid} saga 提交结果 = {flag}");

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

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

d25d3f9c1cc8e8e753f5c4d79df1e134.png

当然,上面的情况太理想了,转出转入都是一次性就成功了。

但是实际上我们会遇到许许多多的问题,最常见的应该就是网络故障了。

下面来看一个异常的 SAGA 示例

异常的 SAGA

做一个假设,用户1的转出是正常的,但是用户2在转入的时候出现了问题。

由于事务已经提交给 dtm 了,按照 SAGA 事务的协议,dtm 会重试未完成的操作。

这个时候用户2 这边会出现什么样的情况呢?

  1. 转入其实成功了,但是 dtm 收到错误 (网络故障等)

  2. 转入没有成功,直接告诉 dtm 失败了 (应用异常等)

无论是那一种,dtm 都会进行重试操作。这个时候会发生什么呢?我们继续往下看。

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

96c450ad77ac30e8ea7b439387d55151.png

再通过调整上面成功的例子,来比较直观的看看出现的情况。

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

app.MapPost("/api/TransInError", (string branch_id, string gid, string op, TransRequest req) =>
{Console.WriteLine($"用户【{req.UserId}】转入【{req.Amount}】正向操作--失败,gid={gid}, branch_id={branch_id}, op={op}");//return Results.BadRequest();return Results.Ok(TransResponse.BuildFailureResponse());
});

失败的返回有两种,一种是状态码大于 400,一种是状态码是 200 并且响应体包含 FAILURE,上面的例子是第二种

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

var saga = new Saga(dtmClient, gid).Add(outApi + "/TransOut", outApi + "/TransOutCompensate", userOutReq).Add(inApi + "/TransInError", inApi + "/TransInCompensate", userInReq);

运行结果如下:

c09a217d0775ddde2f332f7eb4a94777.png

在这个例子中,只考虑补偿/重试成功的情况下。

用户1 转出的 30 块钱最终是回到了他的帐号上,他没有出现损失。

用户2 就有点苦逼了,转入没有成功,返回了失败,还触发了转入的补偿机制,结果就是把用户2 还没进帐的 30 块钱给多扣了,这个就是上面的情况2,常见的空补偿问题。

这个时候就要在进行转入补偿的时候做一系列的判断,转入有没有成功,转出有没有失败等等,把业务变的十分复杂。

如果出现了上述的情况1,会发生什么呢?

用户2 第一次已经成功转入 30 块钱,返回的也是成功,但是网络出了点问题,导致 dtm 认为失败了,它就会进行重试,相当于用户2  还会收到第二个转入 30 块钱的请求!也就是说这次转帐,用户2 会进账 60 块钱,翻倍了,也就是说这个请求不是幂等。

同样的,要处理这个问题,在进行转入的正向操作中也要进行一系列的判断,同样会把复杂度上升一个级别。

前面有提到 dtm 提供了子事务屏障的功能,保证了幂等、空补偿等常见问题。

f897c30aae85df90d12ce870475888fb.png

再来看看这个子事务屏障的功能有没有帮我们简化上面异常处理。

子事务屏障

子事务屏障,需要根据 trans_typegidbranch_idop 四个内容进行创建。

这4个内容 dtm 在回调时会放在 querysting 上面。

客户端里面提供了 IBranchBarrierFactory 来供我们使用。

空补偿

针对上面的异常情况(用户2 凭空消失 30 块钱),对转入的补偿进行子事务屏障的改造。

app.MapPost("/api/BarrierTransInCompensate", async (string branch_id, string gid, string op, string trans_type, TransRequest req, IBranchBarrierFactory factory) =>
{var barrier = factory.CreateBranchBarrier(trans_type, gid, branch_id, op);using var db = Db.GeConn();await barrier.Call(db, async (tx) =>{// 转入失败的情况下,不应该输出下面这个Console.WriteLine($"用户【{req.UserId}】转入【{req.Amount}】补偿操作,gid={gid}, branch_id={branch_id}, op={op}");// tx 参数是事务,可和本地事务一起提交回滚await Task.CompletedTask;});Console.WriteLine($"子事务屏障-补偿操作,gid={gid}, branch_id={branch_id}, op={op}");return Results.Ok(TransResponse.BuildSucceedResponse());
});

Call 方法就是关键所在了,需要传入一个 DbConnection 和真正的业务操作,这里的业务操作就是在控制台输出补偿操作的信息。

同样的,我们再调整一下调用方,把转入补偿操作替换成上面带子事务屏障的接口。

var saga = new Saga(dtmClient, gid).Add(outApi + "/TransOut", outApi + "/TransOutCompensate", userOutReq).Add(inApi + "/TransInError", inApi + "/BarrierTransInCompensate", userInReq);

再来运行这个例子。

4e8f3a838a75cb0e70f5b226cb02bf47.png

会发现转入的补偿操作并没执行,控制台没有输出补偿信息,而是输出了

Will not exec busiCall, isNullCompensation=True, isDuplicateOrPend=False

这个就表明了,这个请求是个空补偿,是不应该执行业务方法的,即空操作。

再来看一下,转入成功的,但是 dtm 收到了失败的信号,不断重试造成重复请求的情况。

幂等

针对用户2 转入两次 30 块钱的异常情况,对转入的正向操作进行子事务屏障的改造。

app.MapPost("/api/BarrierTransIn", async (string branch_id, string gid, string op, string trans_type, TransRequest req, IBranchBarrierFactory factory) =>
{Console.WriteLine($"用户【{req.UserId}】转入【{req.Amount}】请求来了!!!gid={gid}, branch_id={branch_id}, op={op}");var barrier = factory.CreateBranchBarrier(trans_type, gid, branch_id, op);using var db = Db.GeConn();await barrier.Call(db, async (tx) =>{var c = Interlocked.Increment(ref _errCount);// 模拟一个超时执行if (c > 0 && c < 2) await Task.Delay(10000);Console.WriteLine($"用户【{req.UserId}】转入【{req.Amount}】正向操作,gid={gid}, branch_id={branch_id}, op={op}");await Task.CompletedTask;});return Results.Ok(TransResponse.BuildSucceedResponse());
});

这里通过一个超时执行来让 dtm 进行转入正向操作的重试。

同样的,我们再调整一下调用方,把转入的正向操作也替换成上面带子事务屏障的接口。

var saga = new Saga(dtmClient, gid).Add(outApi + "/TransOut", outApi + "/TransOutCompensate", userOutReq).Add(inApi + "/BarrierTransIn", inApi + "/BarrierTransInCompensate", userInReq);

再来运行这个例子。

c25f5bbaa2d085353cea199015a3d5e4.png

可以看到转入的正向操作确实是触发了多次,第一次实际上是成功,只是响应比较慢,导致 dtm 认为是失败了,触发了第二次请求,但是第二次请求并没有执行业务操作,而是输出了

Will not exec busiCall, isNullCompensation=False, isDuplicateOrPend=True

这个就表明了,这个请求是个重复请求,是不应该执行业务方法的,保证了幂等。

到这里,可以看出,子事务屏障确实解决了幂等和空补偿的问题,大大降低了业务判断的复杂度和出错的可能性

写在最后

在这篇文章里,也通过几个例子,完整给出了编写一个 SAGA 事务的过程,涵盖了正常成功完成,异常情况,以及成功回滚的情况。希望对研究分布式事务的您有所帮助。

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

参考资料

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

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

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

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

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

相关文章

梯度消失和梯度爆炸_知识干货-动手学深度学习-05 梯度消失和梯度爆炸以及Kaggle房价预测...

梯度消失和梯度爆炸考虑到环境因素的其他问题Kaggle房价预测梯度消失和梯度爆炸深度模型有关数值稳定性的典型问题是消失&#xff08;vanishing&#xff09;和爆炸&#xff08;explosion&#xff09;。当神经网络的层数较多时&#xff0c;模型的数值稳定性容易变差。PyTorch的默…

Js中 关于top、clientTop、scrollTop、offsetTop的用法

2019独角兽企业重金招聘Python工程师标准>>> Js中 关于top、clientTop、scrollTop、offsetTop的用法 网页可见区域宽&#xff1a; document.body.clientWidth; 网页可见区域高&#xff1a; document.body.clientHeight; 网页可见区域宽&#xff1a; document.body.…

UVa 1639 (期望) Candy

题意&#xff1a; 两个盒子里各有n颗糖&#xff0c;每天有p的概率从第一个盒子里取一颗糖&#xff0c;1-p的概率从第二个盒子里去一颗糖。直到某一天打开某个盒子忽然发现没糖了&#xff0c;求另一个盒子里剩余糖果数的期望。 分析&#xff1a; 紫书上面已经分析的很清楚了&…

八皇后问题(三)

八皇后问题(三) 思路: 用递归实现,既然是深度优先搜索,采用回溯思想,一般都可以用递归来实现。 代码: #include<stdio.h> #include<math.h>int a[512] = {0}; int n; int sum; int check_queen(int a[], int n) {for (int i = 1; i < n; i++)if (fabs(n…

C#中的串口通信SerialPort

前言大家好&#xff0c;我是阿辉。今天这篇文章带大家学习下C#中的串口通讯。在日常的开发工作中&#xff0c;如果工作内容是CS方向的同学应该很容易接触到串口通讯方面的业务需求。那么也就很容易想到C#中SerialPort类&#xff0c;它就是专门来处理串口通讯相关的。了解什么是…

日本的酒店主题能有多丰富?

1 伤害性不大&#xff0c;侮辱性极强&#xff08;素材来源网络&#xff0c;侵删&#xff09;▼2 好家伙&#xff0c;这字医生都看不懂&#xff01;&#xff08;via.你的野王&#xff0c;侵删&#xff09;▼3 当家里点了电子蜡烛&#xff08;素材来源网络&#xff0c;侵删&…

python编写请求参数带文件_转载:如何编写一个带命令行参数的Python文件

看到别人执行一个支持命令行参数的python文件&#xff0c;瞬间觉得高大上起来、牛逼起来&#xff0c;那么如何编写一个带命令行参数的python脚本呢&#xff1f;不用紧张&#xff0c;下面将简单易懂地让你学会如何让自己的python脚本&#xff0c;支持命令行参数。首先你要知道py…

6款程序员不得不爱的bootstrap模板

Bootstrap是基于jQuery框架开发的&#xff0c;它在jQuery框架的基础上进行了更为个性化和人性化的完善&#xff0c;形成一套自己独有的网站风格&#xff0c;并兼容大 部分jQuery插件。Bootstrap中包含了丰富的Web组件&#xff0c;根据这些组件&#xff0c;可以快速的搭建一个漂…

C# 使用XML序列化对象(一)

在System.Xml.Serialization命名空间中提供了XML序列化类XmlSerializer用于将对象序列化为XML。 下面看一个最简单的例子&#xff1a; public class A{public int a{get;set;}}public string XmlSerialize<T>(T obj){XmlSerializer xs new XmlSerializer(typeof(T));usi…

数据结构之深度优先搜索(用栈实现)问题

深度优先搜索 可以这样理解,向四边延伸搜索,然后遇到不能搜索的时候就回退,也就是回溯思想,然后再去其它可能地方搜索。 题目: 定义一个二维数组: int maze[5][5] = { 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0,…

20161114记录一件工作的事

记录 今天&#xff0c;心痛了一次。 做了半年多的项目&#xff0c;被砍了。我去&#xff0c;心血又一次白白付出了。听到这样的消息&#xff0c;心情总会不好。 所以&#xff0c;我今天心情不好&#xff0c;不要理我哦&#xff0c;千万不要理我&#xff0c;我会发飙的 转载于:h…

Natasha 4.0 探索之路系列(四) 模板 API

相关文章Natasha 4.0 探索之路系列(一) 概况Natasha 4.0 探索之路系列(二) 「域」与插件Natasha 模板Natasha 在编译单元的基础上进行了封装整理, 并提供了多种模板帮助开发者构建功能.使用此篇的 API 前提是您对 C# 非常熟悉, 对系统的一些类型足够了解.据此 Natasha 将拒绝与…

青年博士离职高校被索赔10.5万违约金,后博士上诉至法院,判决来了!

全世界只有3.14 % 的人关注了爆炸吧知识本文转自募格学术我们都知道&#xff0c;现在入职高校往往都要签订一定的服务年限合同&#xff0c;如果合同期未满想要离职的话&#xff0c;需要给高校支付巨额违约金。但根据《劳动法》&#xff0c;有些巨额违约金从法律层面来说是站不住…

python os.walk模块_Python之os.walk和os.path.walk

一、os.walk()函数声明&#xff1a;os.walk(top,topdownTrue,οnerrοrNone)(1)参数top表示需要遍历的顶级目录的路径。(2)参数topdown的默认值是“True”表示首先返回顶级目录下的文件&#xff0c;然后再遍历子目录中的文件。当topdown的值为"False"时&#xff0c;表…

Windows Service下的Timer计时器的使用

在C#中提供了三种类型的计时器&#xff1a;1、基于 Windows 的标准计时器(System.Windows.Forms.Timer)2、基于服务器的计时器(System.Timers.Timer)3、线程计时器(System.Threading.Timer) 一、基于 Windows 的标准计时器(System.Windows.Forms.Timer) 首先注意一点就是&#…

linux c 之使用-O来优化gcc

比如一般文件编译 gcc bfs.c -o bfs然后查看花了多少时间 time ./bfs 然后看结果 使用-O 优化&#xff0c;输入下面命令 gcc -O bfs.c -o bfs 再看所需时间&#xff0c;输入下面命令 time ./bfs 结果如图 我们还可以进一步优化用 -O1 -O2 -O3&#xff0c;列如 gcc -O2 bfs.c -…

Hbase Java API详解

2019独角兽企业重金招聘Python工程师标准>>> HBase是Hadoop的数据库&#xff0c;能够对大数据提供随机、实时读写访问。他是开源的&#xff0c;分布式的&#xff0c;多版本的&#xff0c;面向列的&#xff0c;存储模型。 在讲解的时候我首先给大家讲解一下HBase的整…

对C#未来的一点感悟

01—C#有前途吗&#xff1f;在国内C#比较冷门&#xff08;相对Python、Java&#xff09;&#xff0c;很多初学者可能都会对C#的未来有忧虑&#xff0c;害怕没有前途&#xff0c;担心找不到好的工作。其实&#xff0c;大可必须这样&#xff0c;就我在上海而言&#xff0c;我之前…

python 框架 优化_python+unittest框架第六天unittest之优化测试报告

今天的内容主要是&#xff0c;用第三方的HTMLRUNner 第三方的报告来优化之前第五天批量执行案例的测试报告。案例的部分看第五天的批量执行笔记~HTMLRUNner他可以生成更美观的测试报告&#xff0c;基于前辈造的车子&#xff0c;我们直接改造使用。先看下运行代码部分&#xff1…