分布式事务最经典的七种解决方案

2021-08-04 10:07:23 +08:00
 dongfuye1

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

基础理论

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

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

事务

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

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

分布式事务

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

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

XA 事务的特点是:

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

SAGA

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

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

SAGA 事务的特点:

论文里面的 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 里面扣款,Cancel 里面解冻金额,一个成功完成的 TCC 事务时序图如下:

TCC 特点如下:

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

本地消息表

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

大致流程如下:

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

容错机制:

本地消息表的特点:

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

事务消息

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

事务消息发送及提交:

正常发送的流程图如下:

补偿流程:

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

事务消息特点如下:

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

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

最大努力通知

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

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

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

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

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

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

AT 事务模式

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

异常处理

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

异常情况

下面以 TCC 事务说明这些异常情况:

空回滚:

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

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

幂等

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

悬挂:

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

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

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

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

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

子事务屏障

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

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

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

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

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

子事务屏障原理

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

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

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

子事务屏障小结

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

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

总结

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

dtm支持了 TCC 、XA 、SAGA 、事务消息、最大努力通知(使用事务消息实现),提供了简洁易用的接入。

dtm还提供了 python 、PHP 、C#、nodejs 的支持

欢迎大家访问dtm项目,给颗星星支持!

4886 次点击
所在节点    分享创造
6 条回复
kidlj
2021-08-04 10:54:27 +08:00
Friends don't let friends do dual writes.
lucienhsu
2021-08-04 18:21:41 +08:00
很完整了
dongfuye1
2021-08-05 18:01:28 +08:00
@lucienhsu 嘻嘻嘻嘻
yazoox
2021-08-07 11:55:28 +08:00
这是要推广 dtm ?不过讲得挺好的
dongfuye1
2021-08-09 08:51:04 +08:00
@yazoox 酒香还怕巷子深
xiayushengfan
2022-06-18 09:37:42 +08:00
收藏 == 学会 ? "YES" : "YES"

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/793520

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX