• .Net分布式事务及落地解决方案


    本文主要介绍常见的分布式事务及其在.Net平台下的落地方案,参考了很多资料,主要来自DTM的官方文档、知乎

    本文发布于我的个人博客站点:
    文章链接

    常用的分布式事务模式

    XA(二阶段提交)

    XA是由X/Open组织提出的分布式事务的规范,XA规范主要定义了(全局)事务管理器™和(局部)资源管理器(RM)之间的接口。本地的数据库如mysql在XA中扮演的是RM角色

    XA一共分为两阶段:

    第一阶段(prepare):即所有的参与者RM准备执行事务并锁住需要的资源。参与者ready时,向TM报告已准备就绪。

    第二阶段 (commit/rollback):当事务管理者™确认所有参与者(RM)都ready后,向所有参与者发送commit命令。

    目前主流的数据库基本都支持XA事务,包括mysql、oracle、sqlserver、postgre

    流程图如下:

    img

    TCC

    TCC分为三个阶段:

    • Try
      对各个服务的资源做检测,对资源进行提前锁定或者预留(一般来说Try成功的话到了Confirm阶段就默认会成功了,如果不成功则可能原因是网络异常、服务器宕机等)
    • Confirm
      在各个服务中执行实际的操作
    • Cancel
      如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,即执行已操作成功的业务逻辑的回滚操作

    以跨银行转账为案例:

    • Try阶段
      先把两个银行账户中的资金给它冻结住,不让操作了

    • Confirm阶段
      执行实际的转账操作,A银行账户的资金扣减,B银行账户的资金增加

    • Cancel阶段
      如果任何一个银行的操作执行失败,那么就需要回滚进行补偿
      比如A银行账户如果已经扣减了,但是B银行账户资金增加失败了,那么就得把A银行账户资金给加回去

    适用场景

    对一致性要求较高,常见于资金类的场景,可以使用TCC,自己编写大量的业务逻辑,自己判断一个事务中的各个环节是否ok,不ok就执行补偿/回滚代码

    本地消息表

    利用数据库+消息队列的方式实现异步分布式事务,流程如下:

    1. A系统在本地一个事务里操作的同时,插入一条数据到消息表

    2. 接着A系统将这个消息发送到MQ

    3. B系统接收到消息后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息

    4. B系统执行成功后,就会更新自己本地消息表的状态以及A系统消息表的状态

    5. 如果B系统处理失败,那么就不会更新消息表状态,那么此时A系统会定时扫描自己的消息表,如果有未处理的消息,会再次发送到MQ中去,让B再处理

    这个方案保证了最终一致性
    哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止

    SAGA

    Saga是这一篇数据库论文SAGAS提到的一个分布式事务方案。其核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果各个本地事务成功完成那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作

    举例

    例如我们要进行一个类似于银行跨行转账的业务,将A中的30元转给B,根据Saga事务的原理,我们将整个全局事务,切分为以下服务:

    • 转出(TransOut)服务,这里转出将会进行操作A-30
    • 转出补偿(TransOutCompensate)服务,回滚上面的转出操作,即A+30
    • 转入(TransIn)服务,转入将会进行B+30
    • 转入补偿(TransInCompensate)服务,回滚上面的转入操作,即B-30

    关于空补偿、悬挂、幂等问题及解决方案

    以下内容摘自DTM的文档

    异常分类

    分布式系统最大的敌人可能就是NPC了,在这里它是Network Delay, Process Pause, Clock Drift的首字母缩写。我们先看看具体的NPC问题是什么:

    • Network Delay,网络延迟。虽然网络在多数情况下工作的还可以,虽然TCP保证传输顺序和不会丢失,但它无法消除网络延迟问题。
    • Process Pause,进程暂停。有很多种原因可以导致进程暂停:比如编程语言中的GC(垃圾回收机制)会暂停所有正在运行的线程;再比如,我们有时会暂停云服务器,从而可以在不重启的情况下将云服务器从一台主机迁移到另一台主机。我们无法确定性预测进程暂停的时长,你以为持续几百毫秒已经很长了,但实际上持续数分钟之久进程暂停并不罕见。
    • Clock Drift,时钟漂移。现实生活中我们通常认为时间是平稳流逝,单调递增的,但在计算机中不是。计算机使用时钟硬件计时,通常是石英钟,计时精度有限,同时受机器温度影响。为了在一定程度上同步网络上多个机器之间的时间,通常使用NTP协议将本地设备的时间与专门的时间服务器对齐,这样做的一个直接结果是设备的本地时间可能会突然向前或向后跳跃。

    分布式事务既然是分布式的系统,自然也有NPC问题。因为没有涉及时间戳,带来的困扰主要是NP。

    空补偿

    Cancel执行时,Try未执行,事务分支的Cancel操作需要判断出Try未执行,这时需要忽略Cancel中的业务数据更新,直接返回

    一般解决方案:针对该问题,在服务设计时,需要允许空补偿,即在没有找到要补偿的业务主键时,返回补偿成功,并将原业务主键记录下来,标记该业务流水已补偿成功

    悬挂

    Try执行时,Cancel已执行完成,事务分支的Try操作需要判断出Cancel已执行,这时需要忽略Try中的业务数据更新,直接返回

    一般解决方案:需要检查当前业务主键是否已经在空补偿记录下来的业务主键中存在,如果存在则要拒绝执行该笔服务,以免造成数据不一致

    幂等

    由于任何一个请求都可能出现网络异常,出现重复请求,所有的分布式事务分支操作,都需要保证幂等性(即多次请求和一次请求的结果是一致的,比如将余额修改为100,不管调用几次都是100,而将余额减100,多次调用结果会不一样,因此在设计时需要考虑到这个问题)

    DTM的子事务屏障

    dtm中,首创了子事务屏障技术,使用该技术,能够非常便捷的解决异常问题,极大的降低了分布式事务的使用门槛:

    barrier

    子事务屏障技术的原理是,在本地数据库,建立分支操作状态表dtm_barrier,唯一键为全局事务id-分支id-分支操作(try|confirm|cancel)

    1. 开启本地事务
    2. 对于当前操作op(try|confirm|cancel),insert ignore一条数据gid-branchid-op,如果插入不成功,提交事务返回成功(常见的幂等控制方法)
    3. 如果当前操作是cancel,那么在insert ignore一条数据gid-branchid-try,如果插入成功(注意是成功),则提交事务返回成功
    4. 调用屏障内的业务逻辑,如果业务返回成功,则提交事务返回成功;如果业务返回失败,则回滚事务返回失败

    在此机制下,解决了乱序相关的问题

    • 空补偿控制–如果Try没有执行,直接执行了Cancel,那么3中Cancel插入gid-branchid-try会成功,不走屏障内的逻辑,保证了空补偿控制
    • 幂等控制–2中任何一个操作都无法重复插入唯一键,保证了不会重复执行
    • 防悬挂控制–Try在Cancel之后执行,那么Cancel会在3中插入gid-branchid-try,导致Try在2中不成功,就不执行屏障内的逻辑,保证了防悬挂控制

    这里说一下我的理解,在try阶段,如果try的op插入成功了,在cancel阶段先插入try,正常是失败的,如果插入成功的话,说明try阶段的时候插入失败了,也就是认为业务未执行,那么cancel中对应的补偿也就不需要调用了,也就是说通过在数据库中通过gid-branchid-op字段的来同时实现避免空补偿和防悬挂,同时gid-branchid-op是唯一的,因此也避免了重复请求的问题,接口的幂等性也不需要我们自己考虑了

    分布式事务在.Net中的落地方案

    DTM

    官网:https://dtm.pub/

    强烈推荐,支持多种事务模式,包括SAGA、TCC、二阶段消息

    .Net的Demo:https://github.com/dtm-labs/dtmcli-csharp-sample 里面包含了SAGA、TCC和子事务屏障的示例代码,看完后再阅读一下dtm的.Net SDK和官方的文档,基本上就熟悉的差不多了

    CAP

    官网:https://github.com/dotnetcore/CAP

    CAP 是一个基于 .NET Standard 的 C# 库,它是一种处理分布式事务的解决方案,同样具有 EventBus 的功能。

    其底层通过数据库+消息队列的方式来保证分布式事务的可靠性,但是由于是异步的,对于补偿机制实现起来较为复杂,适合不需要补偿机制的场景(不断重试直到成功,重试达到一定次数后报警)

    如何选择合适的事务模式

    强一致性事务:

    • 二阶段消息模式: 适合不需要回滚的场景(dtm特有)
    • saga模式: 适合需要回滚的场景
    • tcc事务模式: 适合一致性要求较高的场景
    • xa事务模式: 适合并发要求不高,没有数据库行锁争抢的场景(基本不用)

    异步分布式事务(最终一致):

    • 本地消息表

    如果用DTM的话最好还是用子事务屏障,不需要自己考虑空悬挂、幂等、空补偿等问题,写起业务来十分的舒适,降低自己的心智负担

    一个个人认为较好的知乎回答

    对于严格资金要求绝对不能错的场景,可以用TCC方案

    如果是一般的分布式事务场景,订单插入之后要调用库存服务更新库存,库存数据没有资金那么的敏感,可以用可靠消息最终一致性方案

    你其实用任何一个分布式事务的这么一个方案,都会导致你那块儿代码会复杂10倍。很多情况下,系统A调用系统B、系统C、系统D,我们可能根本就不做分布式事务。如果调用报错会打印异常日志。

    每个月也就那么几个bug,很多bug是功能性的,体验性的,真的是涉及到数据层面的一些bug,一个月就几个,两三个?如果你为了确保系统自动保证数据100%不能错,上了几十个分布式事务,代码太复杂;性能太差,系统吞吐量、性能大幅度下跌。

    99%的分布式接口调用,不要做分布式事务,直接就是监控(发邮件、发短信)、记录日志(一旦出错,完整的日志)、事后快速的定位、排查和出解决方案、修复数据。
    每个月,每隔几个月,都会对少量的因为代码bug,导致出错的数据,进行人工的修复数据,自己临时动手写个程序,可能要补一些数据,可能要删除一些数据,可能要修改一些字段的值。

    比你做50个分布式事务,成本要来的低上百倍,低几十倍

    trade off,权衡,要用分布式事务的时候,一定是有成本,代码会很复杂,开发很长时间,性能和吞吐量下跌,系统更加复杂更加脆弱反而更加容易出bug;好处,如果做好了,TCC、可靠消息最终一致性方案,一定可以100%保证你那块数据不会出错。

    1%,0.1%,0.01%的业务,资金、交易、订单,我们会用分布式事务方案来保证,会员积分、优惠券、商品信息,其实不要这么搞了

    以上内容摘自:小知在知乎的回答,参考文末链接3

    参考资料

    以上内容及图片或部分内容摘自如下博客 or 回答

    1. 常用的分布式事务解决方案有哪些? - 网易数帆的回答 - 知乎 https://www.zhihu.com/question/64921387/answer/225784480
    2. 面试必问:分布式事务六种解决方案 - 敖丙的文章 - 知乎 https://zhuanlan.zhihu.com/p/183753774
    3. 常用的分布式事务解决方案有哪些? - 小知的回答 - 知乎 https://www.zhihu.com/question/64921387/answer/1976701060

    关于分布式事务这方面的内容,可以重点参考Dtm的官方手册,写的非常详细,地址:https://dtm.pub/guide/start.html,不仅包含了Dtm框架下各种分布式事务的实现方式,还有不同业务场景下的解决方案提供,非常方便学习

    另外,大多数问题可以在dtm的github repository上查找issue,或者在CAP的github repository上查找issue,大多数问题都能在这里找到,或者进dtm的微信群交流

  • 相关阅读:
    【5G PHY】5G SS/PBCH块介绍(一)
    课程目录《C语言程序设计:一个小球的编程之旅》
    【Leetcode】【中等】1726.同积元组
    如何在页面中制作悬浮发布按钮弹窗
    BFC(边距重叠解决方案)
    【LeetCode热题100】--101.对称二叉树
    【Linux】gitee仓库的注册使用以及在Linux上远程把代码上传到gitee上的方法
    USART串口协议
    C++模拟OpenGL库——图形学状态机接口封装(一):用状态模式重构部分代码及接口定义
    云原生Docker镜像管理
  • 原文地址:https://blog.csdn.net/m0_37316917/article/details/125516951