分布式事务七种解决方案,最后一种经典了!-程序员宅基地

技术标签: python  网络  编程语言  数据库  分布式  

随着业务的快速发展、业务复杂度越来越高,几乎每个公司的系统都会从单体走向分布式,特别是转向微服务架构。随之而来就必然遇到分布式事务这个难题,这篇文章总结了分布式事务最经典的解决方案,分享给大家。

◆  基础理论

在讲解具体方案之前,我们先了解一下分布式事务所涉及到的基础理论知识。

我们拿转账作为例子,A需要转100元给B,那么需要给A的余额-100元,给B的余额+100元,整个转账要保证,A-100和B+100同时成功,或者同时失败。看看在各种场景下,是如何解决这个问题的。

◆  事务

把多条语句作为一个整体进行操作的功能,被称为数据库事务。数据库事务可以确保该事务范围内的所有操作都可以全部成功或者全部失败。

事务具有 4 个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为 ACID 特性。

  • Atomicity(原子性):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复到事务开始前的状态,就像这个事务从来没有执行过一样。

  • Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。完整性包括外键约束、应用定义的等约束不会被破坏。

  • Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。

  • Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

◆  分布式事务

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

分布式事务就是指事务的发起者、资源及资源管理器和事务协调者分别位于分布式系统的不同节点之上。在上述转账的业务中,用户A-100操作和用户B+100操作不是位于同一个节点上。本质上来说,分布式事务就是为了保证在分布式场景下,数据操作的正确执行。

分布式事务在分布式环境下,为了满足可用性、性能与降级服务的需要,降低一致性与隔离性的要求,一方面遵循 BASE 理论(BASE相关理论,涉及内容非常多,感兴趣的同学,可以参考BASE理论):

基本业务可用性(Basic Availability)
柔性状态(Soft state)
最终一致性(Eventual consistency)
同样的,分布式事务也部分遵循 ACID 规范:

原子性:严格遵循
一致性:事务完成后的一致性严格遵循;事务中的一致性可适当放宽
隔离性:并行事务间不可影响;事务中间结果可见性允许安全放宽
持久性:严格遵循

◆  分布式事务的解决方案

◆  两阶段提交/XA

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

XA一共分为两阶段:

第一阶段(prepare):即所有的参与者RM准备执行事务并锁住需要的资源。参与者ready时,向TM报告已准备就绪。
第二阶段 (commit/rollback):当事务管理者(TM)确认所有参与者(RM)都ready后,向所有参与者发送commit命令。
目前主流的数据库基本都支持XA事务,包括mysql、oracle、sqlserver、postgre

XA 事务由一个或多个资源管理器(RM)、一个事务管理器(TM)和一个应用程序(ApplicationProgram)组成。

把上面的转账作为例子,一个成功完成的XA事务时序图如下:

35b866cfc4284a68903610140abb8563.png

如果有任何一个参与者prepare失败,那么TM会通知所有完成prepare的参与者进行回滚。

XA事务的特点是:

  • 简单易理解,开发较容易

  • 对资源进行了长时间的锁定,并发度低

如果读者想要进一步研究XA,go语言可参考DTM,java语言可参考seata

◆  SAGA

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

把上面的转账作为例子,一个成功完成的SAGA事务时序图如下:

9b08a9a34f19b376780cc949f76efb57.png



SAGA事务的特点:

  • 并发度高,不用像XA事务那样长期锁定资源

  • 需要定义正常操作以及补偿操作,开发量比XA大

  • 一致性较弱,对于转账,可能发生A用户已扣款,最后转账又失败的情况

论文里面的SAGA内容较多,包括两种恢复策略,包括分支事务并发执行,我们这里的讨论,仅包括最简单的SAGA

SAGA适用的场景较多,长事务适用,对中间结果不敏感的业务场景适用

如果读者想要进一步研究SAGA,go语言可参考DTM,java语言可参考seata

◆  TCC

关于 TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。

TCC分为3个阶段

  • Try 阶段:尝试执行,完成所有业务检查(一致性), 预留必须业务资源(准隔离性)

  • Confirm 阶段:确认执行真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源,Confirm 操作要求具备幂等设计,Confirm 失败后需要进行重试。

  • Cancel 阶段:取消执行,释放 Try 阶段预留的业务资源。Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致,要求满足幂等设计。

把上面的转账作为例子,通常会在Try里面冻结金额,但不扣款,Confirm里面扣款,Cancel里面解冻金额,一个成功完成的TCC事务时序图如下:

c033a1bda6370c72acb9874a396e9e31.png


TCC特点如下:

  • 并发度较高,无长期资源锁定。

  • 开发量较大,需要提供Try/Confirm/Cancel接口。

  • 一致性较好,不会发生SAGA已扣款最后又转账失败的情况

  • TCC适用于订单类业务,对中间状态有约束的业务

如果读者想要进一步研究TCC,go语言可参考DTM,java语言可参考seata

◆  本地消息表

本地消息表这个方案最初是 ebay 架构师 Dan Pritchett 在 2008 年发表给 ACM 的文章。设计核心是将需要分布式处理的任务通过消息的方式来异步确保执行。

大致流程如下:

bdf9dc84d0f12be25a35b76749f7a437.png

写本地消息和业务操作放在一个事务里,保证了业务和发消息的原子性,要么他们全都成功,要么全都失败。

容错机制:

  • 扣减余额事务 失败时,事务直接回滚,无后续步骤

  • 轮序生产消息失败, 增加余额事务失败都会进行重试

本地消息表的特点:

  • 长事务仅需要分拆成多个任务,使用简单

  • 生产者需要额外的创建消息表

  • 每个本地消息表都需要进行轮询

  • 消费者的逻辑如果无法通过重试成功,那么还需要更多的机制,来回滚操作

适用于可异步执行的业务,且后续操作无需回滚的业务

◆  事务消息

在上述的本地消息表方案中,生产者需要额外创建消息表,还需要对本地消息表进行轮询,业务负担较重。阿里开源的RocketMQ 4.3之后的版本正式支持事务消息,该事务消息本质上是把本地消息表放到RocketMQ上,解决生产端的消息发送与本地事务执行的原子性问题。

事务消息发送及提交:

  • 发送消息(half消息)

  • 服务端存储消息,并响应消息的写入结果

  • 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)

  • 根据本地事务状态执行Commit或者Rollback(Commit操作发布消息,消息对消费者可见)

正常发送的流程图如下:

614098725240c67ba7ab3aee2c1ddb15.png

补偿流程:

对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
Producer收到回查消息,返回消息对应的本地事务的状态,为Commit或者Rollback
事务消息方案与本地消息表机制非常类似,区别主要在于原先相关的本地表操作替换成了一个反查接口

事务消息特点如下:

  • 长事务仅需要分拆成多个任务,并提供一个反查接口,使用简单

  • 消费者的逻辑如果无法通过重试成功,那么还需要更多的机制,来回滚操作

适用于可异步执行的业务,且后续操作无需回滚的业务

如果读者想要进一步研究事务消息,可参考rocketmq,为了方便大家学习事务消息,DTM也提供了简单实现

◆  最大努力通知

发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。具体包括:

有一定的消息重复通知机制。因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。
消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求。
前面介绍的的本地消息表和事务消息都属于可靠消息,与这里介绍的最大努力通知有什么不同?

可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。

最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。

解决方案上,最大努力通知需要:

  • 提供接口,让接受通知放能够通过接口查询业务处理结果

  • 消息队列ACK机制,消息队列按照间隔1min、5min、10min、30min、1h、2h、5h、10h的方式,逐步拉大通知间隔 ,直到达到通知要求的时间窗口上限。之后不再通知

最大努力通知适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口

◆  AT事务模式

这是阿里开源项目seata中的一种事务模式,在蚂蚁金服也被称为FMT。优点是该事务模式使用方式,类似XA模式,业务无需编写各类补偿操作,回滚由框架自动完成,缺点也类似AT,存在较长时间的锁,不满足高并发的场景。有兴趣的同学可以参考seata-AT

◆  分布式事务中的网络异常

在分布式事务的各个环节都有可能出现网络以及业务故障等问题,这些问题需要分布式事务的业务方做到防空回滚,幂等,防悬挂三个特性,下面以TCC事务说明这些异常情况:

空回滚:

  在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法,Cancel 方法需要识别出这是一个空回滚,然后直接返回成功。

  出现原因是当一个分支事务所在服务宕机或网络异常,分支事务调用记录为失败,这个时候其实是没有执行Try阶段,当故障恢复后,分布式事务进行回滚则会调用二阶段的Cancel方法,从而形成空回滚。

幂等:

  由于任何一个请求都可能出现网络异常,出现重复请求,所以所有的分布式事务分支,都需要保证幂等性

悬挂:

  悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。

  出现原因是在 RPC 调用分支事务try时,先注册分支事务,再执行RPC调用,如果此时 RPC 调用的网络发生拥堵,RPC 超时以后,TM就会通知RM回滚该分布式事务,可能回滚完成后,RPC 请求才到达参与者真正执行。

下面看一个网络异常的时序图,更好的理解上述几种问题

c94ea718aca89549e4a3f53399d413cd.png

业务处理请求4的时候,Cancel在Try之前执行,需要处理空回滚
业务处理请求6的时候,Cancel重复执行,需要幂等
业务处理请求8的时候,Try在Cancel后执行,需要处理悬挂

面对上述复杂的网络异常情况,目前看到各家建议的方案都是业务方通过唯一键,去查询相关联的操作是否已完成,如果已完成则直接返回成功。相关的判断逻辑较复杂,易出错,业务负担重。

在项目DTM中,出现了一种子事务屏障技术,使用该技术,能够达到这个效果,看示意图:

0c9d3034f184e61c06288f87639e3eea.png

所有这些请求,到了子事务屏障后:不正常的请求,会被过滤;正常请求,通过屏障。开发者使用子事务屏障之后,前面所说的各种异常全部被妥善处理,业务开发人员只需要关注实际的业务逻辑,负担大大降低。
子事务屏障提供了方法ThroughBarrierCall,方法的原型为:

func ThroughBarrierCall(db *sql.DB, transInfo *TransInfo, busiCall BusiFunc)

业务开发人员,在busiCall里面编写自己的相关逻辑,调用该函数。ThroughBarrierCall保证,在空回滚、悬挂等场景下,busiCall不会被调用;在业务被重复调用时,有幂等控制,保证只被提交一次。

子事务屏障会管理TCC、SAGA、XA、事务消息等,也可以扩展到其他领域

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

  • 开启事务

  • 如果是Try分支,则那么insert ignore插入gid-branchid-try,如果成功插入,则调用屏障内逻辑

  • 如果是Confirm分支,那么insert ignore插入gid-branchid-confirm,如果成功插入,则调用屏障内逻辑

  • 如果是Cancel分支,那么insert ignore插入gid-branchid-try,再插入gid-branchid-cancel,如果try未插入并且cancel插入成功,则调用屏障内逻辑

  • 屏障内逻辑返回成功,提交事务,返回成功

  • 屏障内逻辑返回错误,回滚事务,返回错误

在此机制下,解决了网络异常相关的问题

  • 空补偿控制--如果Try没有执行,直接执行了Cancel,那么Cancel插入gid-branchid-try会成功,不走屏障内的逻辑,保证了空补偿控制

  • 幂等控制--任何一个分支都无法重复插入唯一键,保证了不会重复执行

  • 防悬挂控制--Try在Cancel之后执行,那么插入的gid-branchid-try不成功,就不执行,保证了防悬挂控制

对于SAGA事务,也是类似的机制。

子事务屏障技术,为DTM首创,它的意义在于设计简单易实现的算法,提供了简单易用的接口,在首创,它的意义在于设计简单易实现的算法,提供了简单易用的接口,在这两项的帮助下,开发人员彻底的从网络异常的处理中解放出来。

该技术目前需要搭配DTM事务管理器,目前SDK已经提供给go语言的开发者。其他语言的sdk正在规划中。对于其他的分布式事务框架,只要提供了合适的分布式事务信息,能够按照上述原理,快速实现该技术。

◆  总结

本文介绍了分布式事务的一些基础理论,并对常用的分布式事务方案进行了讲解,在文章的后半部分还给出了事务异常的原因、分类以及优雅的解决方案。

往期推荐

2 w+深入详解 Kafka,从源码到架构全部讲透

Nginx 高可用秘籍

高性能缓存Nginx+Redis,就是这么能打!

Spring 事务失效的 8 大场景,看看你都遇到过几个?

项目经理:Elasticsearch举例说明分布式系统架构怎样设计?

dd25af676a3f51ae57822e5e2e8c5c78.gif

回复干货】获取精选干货视频教程

回复加群】加入疑难问题攻坚交流群

回复mat】获取内存溢出问题分析详细文档教程

回复赚钱】获取用java写一个能赚钱的微信机器人

回复副业】获取程序员副业攻略一份

75b40ca56363c8496a427f9760ac80b3.png

889600e34afdd494ea967020346ecf09.gif

戳这儿

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u012811805/article/details/121313006

智能推荐

使用python脚本将视频素材转化为图片数据集并打标_python给视频打标签-程序员宅基地

文章浏览阅读1.7k次,点赞2次,收藏17次。手把手教会你使用python脚本将视频素材转化为图片数据集并打标_python给视频打标签

gojs去掉节点点击时的边框颜色_修改go.js点击时的边框颜色-程序员宅基地

文章浏览阅读1.8k次。myDiagram = $(go.Diagram, "myDiagramDiv", { "LinkDrawn": showLinkLabel, "LinkRelinked": showLinkLabel, "undoManager.isEnabled": true, ..._修改go.js点击时的边框颜色

0基础学会 线程同步(互斥锁pthread_mutex)(内附C语言源码)_pthread_mutex_lock-程序员宅基地

文章浏览阅读1.8k次,点赞3次,收藏2次。0基础学会 线程同步(互斥锁pthread_mutex)(内附C语言源码)_pthread_mutex_lock

从C到Py:Python中的基本函数-程序员宅基地

文章浏览阅读923次,点赞14次,收藏8次。从C语言向Python的一些入门知识,介绍Python的相关软件和一些基本函数

AndroidManifest 合并出错Suggestion: add ‘tools:replace=“android:label“‘ to <application> element_tools:replace="android:label-程序员宅基地

文章浏览阅读857次。AndroidManifest 合并出错tools:replace=“android:label”在flutter开发过程中,由于添加了一些插件依赖不一致导致了冲突,一下是解决方案* What went wrong:Execution failed for task ':app:processDebugMainManifest'.> Manifest merger failed : Attribute application@label value=(pop_law_flutter) fro_tools:replace="android:label

【验证成功】eZ-FET Lite 5528 官方修改版_ez-fet下载器-程序员宅基地

文章浏览阅读897次。立创链接烧写步骤(同时也是launchpad板载ezfet固件恢复步骤):STEP1:找一个可以烧写5528的烧写器,launchpad板载的Ez-FET或者FET430UIF都可以,按照标识连接到目标板上。STEP2:电脑上安装UniFlash工具,下载 Ez-FET lite的资源包STEP3:解压资源包,找到eZ-FET lite rev 1.10 Release Package\Firmware\EZFET_LITE_Rev1_1_BSL_1_1.txt这个固件文件,复制出来S_ez-fet下载器

随便推点

使spyder不弹出绘图窗口的设置方法_禁用内联绘图-程序员宅基地

文章浏览阅读4.7k次,点赞5次,收藏5次。使spyder不弹出绘图窗口的设置方法:第①步:spyder→菜单栏→工具→偏好设置→IPython控制台→绘图→图形的后端→后端→行内→Apply;第②步:spyder→菜单栏→查看→窗格→绘图(勾选此选项)_禁用内联绘图

解决ubuntu无法连接wifi的问题: 安装无线网卡驱动_mediatek 网卡的ubuntu驱动-程序员宅基地

文章浏览阅读1.5w次,点赞2次,收藏28次。因为工作的需要,我在电脑上安装了win10和ubuntu的双系统,然而装完ubuntu后却傻眼了: 网络连接里根本没有wifi选项。这可不行,我的任务之一就是配置一个浏览器,没有wifi岂不是什么都做不了。然而,折腾了一个星期的我,在网上找各种教程,从安装附加驱动到配置网关、DNS,全都失败了,期间还因为不熟悉linux操作,重装了ubuntu系统,差点连windows都打不开了。万般无奈之下_mediatek 网卡的ubuntu驱动

Mathematica和Wolfram语言面向数学的入门指南:二维绘图-程序员宅基地

文章浏览阅读1.3k次。Mathematica在其三十年的开发历程中,在技术计算领域确立了最先进的技术,并为全球技术创新人员、教育工作者、学生和其他人士提供了最主要的计算环境。免费试用mathematica chinese12.3.1生成多项式函数的二维曲线:(区间 {x,min,max} 定义了绘图范围.)或者绘制一组不等式的二维区域:(&& 是 And 的符号.)有许多有用的选项可以用来自定义可视化,比如,可以加上图例:或对绘图进行填充来查看曲线下的面积:用

基于MAX7800羽毛板语音控制ESP8266小车-程序员宅基地

文章浏览阅读1.3k次,点赞7次,收藏8次。这是我第二次参加嵌入式相关的网上比赛活动,第一次是RT-Thread的【基于RT-Thread+RA6M4的智能鱼缸系统设计之鱼我所欲也】活动,作品是022年暑假做的获得第六名,还是比较 开心!我相信我的付出,期待这一次的成绩!虽然这次比赛初期我没有进行kws20 demo的训练试验,但是勇敢的我说是站在巨人的肩膀上,有点小遗憾我的时间和电脑都比较拉胯,下次争取做一些训练,跑一跑模型!

Ubuntu18.04双网卡 (支持内外网同时访问) 配置_ubuntu18.04配置双网卡-程序员宅基地

文章浏览阅读8k次,点赞3次,收藏34次。临时修改路由 系统重启失效查看系统网卡信息ifconfig查看系统路由信息ruote -n增加一条路由route add -net 192.168.0.0 netmask 255.255.0.0 gw 192.168.0.254 dev em1ps: 上面net中192.168.0.0通配内网192.168开头的ip地址, netmask与net是对应的, gw 是网卡em1对应的内网网关地址,实现了内网访问;示例:route add -net 192.168.64.0 netmask _ubuntu18.04配置双网卡

windows10下编译 fast rcnn_windows fast rcnn-程序员宅基地

文章浏览阅读1k次。error :Unknown layer type: ROIPooling;no module named cv2_windows fast rcnn