- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.
这篇CFSDN的博客文章关于MySQL与Golang分布式事务经典的七种解决方案由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.
前言:
随着业务的快速发展、业务复杂度越来越高,几乎每个公司的系统都会从单体走向分布式,特别是转向微服务架构。随之而来就必然遇到分布式事务这个难题。 这篇文章首先介绍了相关的基础理论,然后总结了最经典的事务方案,最后给出了子事务乱序执行(幂等、空补偿、悬挂问题)的解决方案,分享给大家.
在讲解具体方案之前,我们先了解一下分布式事务所涉及到的基础理论知识.
我们拿转账作为例子,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 规范:
由于分布式事务方案,无法做到完全的acid的保证,没有一种完美的方案,能够解决掉所有业务问题。因此在实际应用中,会根据业务的不同特性,选择最适合的分布式事务方案.
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)组成.
这里的rm、tm、ap三个角色是经典的角色划分,会贯穿后续saga、tcc等事务模式.
把上面的转账作为例子,一个成功完成的xa事务时序图如下:
如果有任何一个参与者prepare失败,那么tm会通知所有完成prepare的参与者进行回滚.
xa事务的特点是:
如果读者想要进一步研究xa,go语言以及php、python、java、c# 、node等都可参考dtm 。
saga是这一篇数据库论文sagas提到的一个方案。其核心思想是将长事务拆分为多个本地短事务,由saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作.
把上面的转账作为例子,一个成功完成的saga事务时序图如下:
saga一旦到了cancel阶段,那么cancel在业务逻辑上是不允许失败了。如果因为网络或者其他临时故障,导致没有返回成功,那么tm会不断重试,直到cancel返回成功.
saga事务的特点:
论文里面的saga内容较多,包括两种恢复策略,包括分支事务并发执行,我们这里的讨论,仅包括最简单的saga 。
saga适用的场景较多,长事务适用,对中间结果不敏感的业务场景适用 。
如果读者想要进一步研究saga,可参考dtm,里面包括了saga成功、失败回滚的例子,还包括各类网络异常的处理.
关于 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事务时序图如下:
tcc的confirm/cancel阶段在业务逻辑上是不允许返回失败的,如果因为网络或者其他临时故障,导致不能返回成功,tm会不断的重试,直到confirm/cancel返回成功.
tcc特点如下:
try/confirm/cancel
接口。saga
已扣款最后又转账失败的情况tcc
适用于订单类业务,对中间状态有约束的业务如果读者想要进一步研究tcc,可参考dtm 。
本地消息表这个方案最初是 ebay 架构师 dan pritchett 在 2008 年发表给 acm 的文章。设计核心是将需要分布式处理的任务通过消息的方式来异步确保执行.
大致流程如下:
写本地消息和业务操作放在一个事务里,保证了业务和发消息的原子性,要么他们全都成功,要么全都失败.
容错机制:
本地消息表的特点:
适用于可异步执行的业务,且后续操作无需回滚的业务 。
在上述的本地消息表方案中,生产者需要额外创建消息表,还需要对本地消息表进行轮询,业务负担较重。阿里开源的rocketmq 4.3之后的版本正式支持事务消息,该事务消息本质上是把本地消息表放到rocketmq上,解决生产端的消息发送与本地事务执行的原子性问题.
事务消息发送及提交:
发送消息(half消息) 服务端存储消息,并响应消息的写入结果 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行) 根据本地事务状态执行commit或者rollback(commit操作发布消息,消息对消费者可见) 。
正常发送的流程图如下:
补偿流程:
对没有commit/rollback的事务消息(pending状态的消息),从服务端发起一次“回查” producer收到回查消息,返回消息对应的本地事务的状态,为commit或者rollback 事务消息方案与本地消息表机制非常类似,区别主要在于原先相关的本地表操作替换成了一个反查接口 。
事务消息特点如下:
适用于可异步执行的业务,且后续操作无需回滚的业务 。
发起通知方通过一定的机制最大努力将业务处理结果通知到接收方。具体包括:
有一定的消息重复通知机制。因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。 消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求。 前面介绍的的本地消息表和事务消息都属于可靠消息,与这里介绍的最大努力通知有什么不同?
可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证.
最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方.
解决方案上,最大努力通知需要:
ack
机制,消息队列按照间隔1min
、5min
、10min
、30min
、1h
、2h
、5h
、10h
的方式,逐步拉大通知间隔 ,直到达到通知要求的时间窗口上限。之后不再通知最大努力通知适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口 。
这是阿里开源项目seata中的一种事务模式,在蚂蚁金服也被称为fmt。优点是该事务模式使用方式,类似xa模式,业务无需编写各类补偿操作,回滚由框架自动完成,缺点也类似xa,存在较长时间的锁,不满足高并发的场景。从性能的角度看,at模式会比xa更高一些,但也带来了脏回滚这样的新问题。有兴趣的同学可以参考seata-at 。
在分布式事务的各个环节都有可能出现网络以及业务故障等问题,这些问题需要分布式事务的业务方做到防空回滚,幂等,防悬挂三个特性.
下面以tcc事务说明这些异常情况:
空回滚:
在没有调用 tcc 资源 try 方法的情况下,调用了二阶段的 cancel 方法,cancel 方法需要识别出这是一个空回滚,然后直接返回成功.
出现原因是当一个分支事务所在服务宕机或网络异常,分支事务调用记录为失败,这个时候其实是没有执行try阶段,当故障恢复后,分布式事务进行回滚则会调用二阶段的cancel方法,从而形成空回滚.
幂等:
由于任何一个请求都可能出现网络异常,出现重复请求,所以所有的分布式事务分支,都需要保证幂等性 。
悬挂:
悬挂就是对于一个分布式事务,其二阶段 cancel 接口比 try 接口先执行.
出现原因是在 rpc 调用分支事务try时,先注册分支事务,再执行rpc调用,如果此时 rpc 调用的网络发生拥堵,rpc 超时以后,tm就会通知rm回滚该分布式事务,可能回滚完成后,try 的 rpc 请求才到达参与者真正执行.
下面看一个网络异常的时序图,更好的理解上述几种问题 。
cancel
在try之前执行,需要处理空回滚cancel
重复执行,需要幂等cancel
后执行,需要处理悬挂面对上述复杂的网络异常情况,目前看到各家建议的方案都是业务方通过唯一键,去查询相关联的操作是否已完成,如果已完成则直接返回成功。相关的判断逻辑较复杂,易出错,业务负担重.
在项目https://github.com/yedf/dtm中,出现了一种子事务屏障技术,使用该技术,能够达到这个效果,看示意图:
所有这些请求,到了子事务屏障后:不正常的请求,会被过滤;正常请求,通过屏障。开发者使用子事务屏障之后,前面所说的各种异常全部被妥善处理,业务开发人员只需要关注实际的业务逻辑,负担大大降低.
子事务屏障提供了方法throughbarriercall,方法的原型为:
func throughbarriercall(db *sql.db, transinfo *transinfo, busicall busifunc) 。
业务开发人员,在busicall里面编写自己的相关逻辑,调用该函数。throughbarriercall保证,在空回滚、悬挂等场景下,busicall不会被调用;在业务被重复调用时,有幂等控制,保证只被提交一次.
子事务屏障会管理tcc、saga、事务消息等,也可以扩展到其他领域 。
子事务屏障技术的原理是,在本地数据库,建立分支事务状态表sub_trans_barrier,唯一键为全局事务id-子事务id-子事务分支名称(try|confirm|cancel) 。
insert ignore
插入gid-branchid-try
,如果成功插入,则调用屏障内逻辑insert ignore
插入gid-branchid-confirm
,如果成功插入,则调用屏障内逻辑insert ignore
插入gid-branchid-try
,再插入gid-branchid-cancel
,如果try未插入并且cancel插入成功,则调用屏障内逻辑在此机制下,解决了网络异常相关的问题 。
cancel
插入gid-branchid-try
会成功,不走屏障内的逻辑,保证了空补偿控制gid-branchid-try
不成功,就不执行,保证了防悬挂控制对于saga、事务消息等,也是类似的机制.
子事务屏障技术,为https://github.com/yedf/dtm首创,它的意义在于设计简单易实现的算法,提供了简单易用的接口,在首创,它的意义在于设计简单易实现的算法,提供了简单易用的接口,在这两项的帮助下,开发人员彻底的从网络异常的处理中解放出来.
该技术目前需要搭配yedf/dtm事务管理器,目前sdk已经提供给go、python语言的开发者。其他语言的sdk正在规划中。对于其他的分布式事务框架,只要提供了合适的分布式事务信息,能够按照上述原理,快速实现该技术.
我们以前面介绍的saga事务为例,以dtm作为事务框架,来完成一个具体的分布式事务。本例子采用go语言,如果您对此不感兴趣,可以直接跳到文章最后的小结.
我们先编写核心业务代码,调整用户的账户余额 。
1
2
3
4
|
func qsadjustbalance(uid
int
, amount
int
) (interface{}, error) {
_, err := dtmcli.sdbexec(sdbget(),
"update dtm_busi.user_account set balance = balance + ? where user_id = ?"
, amount, uid)
return
dtmcli.resultsuccess, err
}
|
下面我们来编写具体的正向操作/补偿操作的处理函数 。
1
2
3
4
5
6
7
8
9
10
11
12
|
app.post(qsbusiapi+
"/transin"
, common.wraphandler(func(c *gin.context) (interface{}, error) {
return
qsadjustbalance(2, 30)
}))
app.post(qsbusiapi+
"/transincompensate"
, common.wraphandler(func(c *gin.context) (interface{}, error) {
return
qsadjustbalance(2, -30)
}))
app.post(qsbusiapi+
"/transout"
, common.wraphandler(func(c *gin.context) (interface{}, error) {
return
qsadjustbalance(1, -30)
}))
app.post(qsbusiapi+
"/transoutcompensate"
, common.wraphandler(func(c *gin.context) (interface{}, error) {
return
qsadjustbalance(1, 30)
}))
|
到此各个子事务的处理函数已经ok了,然后是开启saga事务,进行分支调用 。
1
2
3
4
5
6
7
8
9
|
req := &gin.h{
"amount"
: 30} // 微服务的载荷
// dtmserver为dtm服务的地址
saga := dtmcli.newsaga(dtmserver, dtmcli.mustgengid(dtmserver)).
// 添加一个transout的子事务,正向操作为url: qsbusi+
"/transout"
, 逆向操作为url: qsbusi+
"/transoutcompensate"
add
(qsbusi+
"/transout"
, qsbusi+
"/transoutcompensate"
, req).
// 添加一个transin的子事务,正向操作为url: qsbusi+
"/transout"
, 逆向操作为url: qsbusi+
"/transincompensate"
add
(qsbusi+
"/transin"
, qsbusi+
"/transincompensate"
, req)
// 提交saga事务,dtm会完成所有的子事务/回滚所有的子事务
err := saga.submit()
|
至此,一个完整的saga分布式事务编写完成.
如果您想要完整运行一个成功的示例,那么按照yedf/dtm项目的说明搭建好环境之后,通过下面命令运行saga的例子即可:
go run app/main.go quick_start 。
假设提交给dtm的事务中,调用转入操作时,出现短暂的故障怎么办?按照saga事务的协议,dtm会重试未完成的操作,这时我们要如何处理?故障有可能是转入操作完成后出网络故障,也有可能是转入操作完成中出现机器宕机。如何处理才能够保障账户余额的调整是正确无问题的?
我们使用了子事务屏障功能,保证多次重试,只会有一次成功提交.
我们把处理函数调整为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
func sagabarrieradjustbalance(sdb *sql.tx, uid
int
, amount
int
) (interface{}, error) {
_, err := dtmcli.stxexec(sdb,
"update dtm_busi.user_account set balance = balance + ? where user_id = ?"
, amount, uid)
return
dtmcli.resultsuccess, err
}
func sagabarriertransin(c *gin.context) (interface{}, error) {
return
dtmcli.throughbarriercall(sdbget(), mustgettrans(c), func(sdb *sql.tx) (interface{}, error) {
return
sagabarrieradjustbalance(sdb, 1, reqfrom(c).amount)
})
}
func sagabarriertransincompensate(c *gin.context) (interface{}, error) {
return
dtmcli.throughbarriercall(sdbget(), mustgettrans(c), func(sdb *sql.tx) (interface{}, error) {
return
sagabarrieradjustbalance(sdb, 1, -reqfrom(c).amount)
})
}
|
这里的dtmcli.troughbarriercall调用会使用子事务屏障技术,保证第三个参数里的回调函数仅被处理一次.
您可以尝试多次调用这个transin服务,仅有一次余额调整。您可以运行以下命令,运行新的处理方式:
go run app/main.go saga_barrier 。
假如银行将金额准备转入用户2时,发现用户2的账户异常,返回失败,会怎么样?我们调整处理函数,让转入操作返回失败 。
1
2
3
|
func sagabarriertransin(c *gin.context) (interface{}, error) {
return
dtmcli.resultfailure, nil
}
|
我们给出事务失败交互的时序图 。
这里有一点,transin的正向操作什么都没有做,就返回了失败,此时调用transin的补偿操作,会不会导致反向调整出错了呢?
不用担心,前面的子事务屏障技术,能够保证transin的错误如果发生在提交之前,则补偿为空操作;transin的错误如果发生在提交之后,则补偿操作会将数据提交一次;如果transin还在进行中,则补偿操作会等待transin最终提交/回滚,然后再提交补偿/空回滚.
您可以将返回错误的transin改成:
1
2
3
4
5
6
|
func sagabarriertransin(c *gin.context) (interface{}, error) {
dtmcli.throughbarriercall(sdbget(), mustgettrans(c), func(sdb *sql.tx) (interface{}, error) {
return
sagabarrieradjustbalance(sdb, 1, 30)
})
return
dtmcli.resultfailure, nil
}
|
最后的结果余额依旧没有问题 。
本文介绍了分布式事务的一些基础理论,并对常用的分布式事务方案进行了讲解;在文章的后半部分还给出了事务异常的原因、分类以及优雅的解决方案;最后以一个可运行的分布式事务例子,将前面介绍的内容以简短的程序进行演示.
到此这篇关于关于mysql与golang分布式事务经典的七种解决方案的文章就介绍到这了,更多相关分布式事务经典的七种解决方案内容请搜索我以前的文章或继续浏览下面的相关文章希望大家以后多多支持我! 。
原文链接:https://segmentfault.com/a/1190000040321750 。
最后此篇关于关于MySQL与Golang分布式事务经典的七种解决方案的文章就讲到这里了,如果你想了解更多关于关于MySQL与Golang分布式事务经典的七种解决方案的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我正在尝试运行这段代码,用随机数替换字符串中的一个字符: //Get the position between 0 and the length of the string-1 to insert
我有一个包含 3 个位置的数组,假设它的所有位置都是数字 5。 [5 5 5] 我怎样才能以保持 555 的方式将它传递给 var?就像这样。 n:= 555 最佳答案 与使用任何其他语言的方式相同:
我使用 go dep 工具版本 v0.4.1,现在当我运行 dep init 时它会按预期创建 2 个文件,当我打开 gopkg.lock 我发现例如以下内容 [[projects]] name
我正在制作学习联系申请。我有一个 NewContact()。 // Contact - defines the fields of an entire Contact type Contact str
我一直在尝试使用该模块: https://godoc.org/github.com/hirochachacha/go-smb2#RemoteFile.ReadAt 为了在 Windows 机器上对我的
我需要在 golang 中编译 golang 中的程序。有没有不使用 exec.Command("go","build") 的原生形式? 最佳答案 不幸的是,我认为使用 exec.Command 是利
编写输出有效 go 代码的 go 应用程序可能最好使用内置的“go”包及其一些子包(“go/ast”、“go/token”、“go/printer”、等)。 要创建字符串文字表达式,您需要创建一个 a
我正在尝试使用 Golang 和 gin 为我的 api 和前端编写代理。如果请求转到除“/api”之外的任何内容,我想代理到 svelte 服务器。如果出现“/api/something”,我想在
我偶然发现了这个博客:using go as a scripting language并尝试创建一个可用于运行 golang 脚本的自定义图像,即 FROM golang:1.15 RUN go ge
我刚开始接触golang,我需要从json字符串中获取数据。 {"data" : ["2016-06-21","2016-06-22","2016-06-25"], "sid" : "ab", "di
关闭。这个问题是opinion-based .它目前不接受答案。 想要改进这个问题? 更新问题,以便 editing this post 可以用事实和引用来回答它. 关闭 3 年前。 Improve
我是 goland 的新手,试图在我的第一个项目中使用它。我注意到在 goland 中它没有显示通过容器引入的相同 golang SDK。 这是我的 Dockerfile: FROM golang:1
我正在试用 golang-neo4j-bolt-driver 包 github.com/johnnadratowski/golang-neo4j-bolt-driver 我已经导入了包并正在使用创建新
如果我安装了Go发行版软件包,则会在/usr/lib/golang/pkg中看到很多文件,在/usr/lib/golang/src中看到非常相似的文件集。这两组之间有什么关系? pkg是从src中的源
我发现 golang 上下文对于在客户端-服务器请求范围内取消服务器的处理很有用。 我可以使用 http.Request.WithContext 方法发出带有上下文的 http 请求,但是如果客户端不
我正在尝试将一个 golang 数组(还有 slice、struct 等)放置到 HTML 中,这样当从 golang gin web 框架返回 HTML 时,我可以在 HTML 元素内容中使用数组元
目前正在使用这个 ffmpeg 命令编辑视频 ffmpeg -i "video1.ts" -c:v libx264 -crf 20 -c:a aac -strict -2 "video1-fix.ts
我需要从 play.golang.org 链接读取 golang 代码并保存到 .go 文件。我想知道 play.golang.org 是否有任何公共(public) API 支持。我用谷歌搜索但没有
我第一次使用 IntelliJ 的最新 (2014-01-03) Golang 插件。 通常,我的终端工作流程是 go build && ./executable -args=1 所以我试图创建一个启
这个问题只是在构建之间随机出现,现在甚至我们的生产 repo,几个月都没有改变,在构建时也会出现这个问题。我已经坚持了一段时间。它不会发生在我们的本地机器上,只有在使用 dockerfile 时才会发
我是一名优秀的程序员,十分优秀!