暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

03 | 分布式事务的产生原因及常见的解决方案(NewSQL、Distributed SQL等)

新架构思考 2021-09-06
1232


在导读部分我们详细介绍了各阶段系统架构的发展脉络,可以说,我们已经全面进入了微服务架构的时代。微服务架构要求每个服务都有自己独立的数据库,不同的服务不推荐使用同一个数据库。如果必须使用同一个数据库,那么你需要考虑这两个服务的耦合性的高低,是保持拆分还是合并成一个服务。

 

尽管微服务架构的设计思路有利于复杂的服务拆分,大大提高了系统的可扩展能力,支撑大规模用户访问的能力也得到了大幅度提升,但也很容易产生一个新的问题跨数据库(源)的分布式事务问题。

由于多个数据库(源)的数据是无法保证一致的,如果没有采用很好的低成本的解决方案提前避免,后续就一定会成为“雷区”,稍有不慎就会产生一些意想不到的、数据不一致的事故。

试想一下,一个采用了微服务架构的电商网站,由于没有很好的分布式事务保证,在海量用户购买了某些商品后,产生用户的账户没有扣款成功,或扣款的金额不正确,这样的系统将会对商家产生多么巨大的损失。

在单数据库(源)下,通常不会产生数据一致性的问题,是因为关系型数据库本身所提供的事务机制能够保证结果的正确性,而多数据源(多数据库)情况下,数据库本身所提供的事务机制已经对此无能为力。

 

那么什么是分布式事务,在什么情况下会产生分布式事务,我们又应该如何避免/解决分布式事务呢?这一讲我们就来重点聊聊这些问题。

 

什么是分布式事务?

 

分布式事务是在跨数据源的多个服务调用下产生的。它不仅要求数据满足本地事务 ACID 特性,还要求满足分布式架构下的数据一致性

 

其中本地事务的 ACID 特性,是指原子性、一致性、隔离性和持久性,这些特性我们将在 02 讲“深入剖析事务的本质及其 ACID 特性”中详细展开。这里我们简单做个介绍。

 

  • 原子性要求一个事务内的所有操作作为不可分割的整体,必须一次性全部执行成功或者全部执行失败,不允许有部分成功或部分失败的情况。

  • 一致性是指事务操作前后,数据库从一个一致的状态过渡到另外一个一致的状态。以常见的银行转账操作为例,转账前两个账户的余额总和与转账之后的余额总和要保持不变。

  • 隔离性是指并发操作的多个事务之间是相互隔离的,一个事务的执行不会受到另外一个事务执行的干扰。

  • 持久性指的是事务在提交之后,不会受到各种系统故障的影响,保证数据能够永久存储,不丢失。

 

下面我们以电商网站购物下单为例,分析说明分布式事务具体的应用场景,如下图所示:

 

                                    电商购物系统流程图

 

购物下单时涉及的业务流程如下:

  1. 用户购买商品后,系统调用订单服务创建支付订单;

  2. 调用账户服务进行用户余额扣款;

  3. 调用优惠券服务扣减用户的优惠券。

 

在执行过程中,订单服务对订单库的修改操作可能执行成功也可能执行失败,账户服务和优惠券服务也存在同样的问题。但当整个购物流程执行完毕,要保证三个服务对各自库的数据修改都执行成功,或者都执行失败,不能出现订单库创建订单成功,而账户服务对账户库扣款失败这样的情况。

 

只有单个数据源时,我们还可以用单机事务来保证任务顺利执行,但是在数据源独立的情况下,三个服务是各自执行任务的。当执行完订单服务后,可能因为网络通信异常导致账户库扣款失败,但此时订单库的数据已经执行成功了,这就需要一个第三方协调服务撤销之前订单库的修改,否则将产生数据不一致的问题。

 

结合这个例子理解分布式事务就更容易了,即在跨数据库和数据源的情况下,采用微服务架构进行服务拆分,并进行分布式多个节点部署之后,仍然能够保证分布式环境下的数据一致性,不会因为网络分区等问题导致多个数据源之间的数据不一致。

 

但是这里也有一个问题,有多个微服务拆分的情况下一定会产生分布式事务吗?

 

请注意,在刚刚我们说明分布式事务含义时并没有说微服务拆分一定会产生分布式事务问题,我来举两个反例,说明何种情况下不能产生分布式事务。

 

  1. 共享数据库的微服务架构

 

我曾经在一个 SaaS 公司里遇到过这样一个场景:公司系统架构是分层设计,整体分为上层的 SaaS 服务层和下层的 PaaS 服务层,PaaS 服务层又具体分为 PaaS 服务网关和 PaaS 聚合服务,所有的 PaaS 服务涉及的数据都存储在一个 DB 中。由于 PaaS 聚合服务业务逻辑复杂、功能庞大,已经到了非拆不可的地步,所以我们对它进行了服务的拆分

 

拆分后的聚合服务被划分成三个子服务:服务 A、服务 B 和服务 C,这三个服务可以分别进行分布式部署。但由于拆分得不彻底,数据库层面实际上还是利用同一个数据库存储,如下图所示:

 

单数据库的SaaS服务架构图

 

我们将这样的系统称为数据库共享的微服务架构,由于微服务耦合性强、拆分得不彻底,该架构还处于单体架构微服务架构的过渡阶段,因此也被称为分布式单体架构

 

之所以很多公司仍然保留分布式单体架构,是因为一旦数据库跟随服务进行拆分,跨库的分布式事务将很难解决,传统的单库下的事务处理将失效,必须要使用一个独立的第三方模块来协调不同库的数据的一致,这会使原本的业务处理流程更加复杂。所以,在没有特别好的分布式事务解决方案出现前,该方案恰恰是适应现状最好的架构方案,这也体现了架构实际是多种因素折中平衡的产物

 

可能你会有这样的疑问:在数据库没有拆分的情况下,这样的架构会不会导致数据库瓶颈,数据库能否支撑高并发、海量用户请求?

 

这样的担心不无道理,但在当时的场景下是合适的。因为企业服务 SaaS 领域系统的特点是功能极其复杂,但用户的瞬间数据请求量不如 ToC 互联网那样凶猛。在企业服务这个场景下,该系统架构在企业客户数没有大幅度增长的阶段是完全可以满足企业发展需求的。

 

总而言之,该架构最大的好处是:在没有好的分布式事务解决方案出现之前,避免了极其棘手的分布式事务问题。

 

  1. 事件驱动的微服务架构

 

我们来看一个电商系统购物的场景:用户点击购买产品并支付后,电商平台会调用分布式订单服务创建订单,并将订单保存在订单库中,然后给用户发送购买成功的短信通知。如下图所示:

 

事件驱动的电商通知服务

 

在这个场景中,我们并没有直接调用订单服务和短信发送服务,而是用消息中间件进行解耦,短信服务通过订阅消息中间件队列中的事件内容后执行短信发送操作。

 

如果订单创建成功,而短信发送服务失败,不必进行订单的回滚操作,可以利用消息中间件的可靠性重复投递机制进行重试,直到成功发送短信。当多次重试仍不成功时,我们可以交由人工处理。

 

在这种基于事件驱动的微服务架构中,由于微服务的调用已经通过消息中间件解耦异步化,上游订单服务不关心下游短信发送服务的成功与失败,下游短信发送服务的失败也不构成对上游订单服务的回滚操作,所以也不存在分布式事务的问题。

 

通过上面两个反例,我们可以总结出微服务架构下分布式事务产生的原因:当业务拆分成多个微服务后,微服务的数量急剧增加,微服务架构的指导原则要求每一个微服务都有自己独立的数据库,进行服务自治,因此当多个微服务同步调用的时候就涉及跨数据库的数据一致性问题,就产生了分布式事务

 

如何解决分布式事务问题

 

解决分布式事务问题的技术,仍然以单机数据库系统中的事务处理技术为基础,同时增加了一些扩展技术用来重点解决下列问题:

  • 处理分布式的各种数据异常;

  • 做到分布式架构下的顺序一致性;

  • 做到跨节点的原子提交;

  • 做到在网络分区或有较高网络延时情况下的事务响应。

 

解决分布式事务问题,需要整体考虑,根据我们有三类解决方案。

 

方案 1 提前规避法

 

假设我们将业务拆分成了多个微服务,每个微服务都有独立的数据库,当一个服务需要访问其他服务提供的 API 接口时,它不但要保证本地数据库执行成功,也需要保证调用的服务对数据库操作执行成功。在没有第三方协调双方执行结果的情况下,很难保证操作结果,所以我们将思路放在了如何规避“用一个分布式事务来保证两个库操作的一致性”上。

 

针对这种问题,各大厂的设计思路也都是尽量规避分布式事务的发生。我们可以从两个层面展开规避。

 

一是业务规避,需要技术和产品团队在产品层面规避跨库的数据实时显示。

 

二是技术层面的规避,通常我们采用消息中间件进行服务的异步化调用,单个服务执行本地库操作后,再通过消息中间件解耦,异步调用其他库执行的逻辑。如果另外一个库的执行操作失败,则重试操作,直到成功为止。

 

经过解耦,我们就不必要求两个数据库的数据状态同时成功或者失败。一个库执行成功之后,将其状态存储到消息中间件中,另外一个库可以消费消息中间件中存储的状态进行后续的操作,这种功能在消息中间件中称为事务消息。

 

这种基于消息中间件的事务消息能力来避免分布式事务的架构思路,在不影响用户使用体验的前提下,保证了在很短时间段之后跨库数据的最终一致性,能够有效地降低分布式事务处理的复杂度,目前业界互联网公司已经广泛应用。更具体的内容我们会在第 19 讲 "如何基于消息中间件解决分布式事务?" 中详细介绍。

 

方案 2 事务中间件框架

 

通过中间件层解决通用的分布式事务问题是一个很好的想法,我们将具有这样功能的中间件框架叫作事务中间件。

 

事务中间件框架提供了跨库的分布式事务的能力,上层应用通过引入少量代码就可以获取这种能力。这样上层微服务应用不需要关心分布式事务的细节实现,只专注自己的业务拆分和业务逻辑实现即可。

 

这种方式也加快了微服务化的改造进程,事务中间件同其他中间件一样,都不涉及具体的业务逻辑,统一为业务应用提供底层支撑能力,这些底层能力包括:服务注册中心、配置中心、链路追踪系统、消息系统、日志系统等,是微服务架构不可或缺的一部分。

 

目前有很多事务中间件框架在各大公司使用,但还不是很成熟,比如:

  • 基于 JTA 模式的 Atomikos、Bitronix、ByteJTA;

  • 基于 TCC 模式的 TCC-transaction、ByteTCC 等;

  • 基于 SAGA 模式的 Apache ServiceComb 等。

 

其中成熟度比较高的是 Seata 框架。它经过阿里的内部系统验证,开源反馈到社区后,逐渐成为追捧的热点,并且已经得到了各大公司的大量应用反馈。Seata 框架目前支持 XA 协议、SAGA 协议、TCC 模式以及 AT 模式,不同的模式可以应用在不同公司的应用场景。这部分内容我们会在第二模块重点介绍。

 

分布式事务框架图

方案 3 分布式数据库

 

传统的数据库无法承担分布式事务这样的重任,于是产生了各种各样的分布式数据库系统。

分布式数据库也就是我们常说的 NewSQL、Distributed SQL,知名的产品如下表所示:

 

公司

产品

Google

Spanner

Cockroach Labs

CockroachDB(蟑螂数据库)

AWS

Aurora

PingCAP

TiDB

蚂蚁集团

Oceanbase

腾讯

TDSQL

巨杉软件

SequoiaDB

阿里云

PolarDB

关于比较知名的CockroachDB,有下面几个特点:

  1. 前谷歌员工推出的支持SQL和分布式事务的跨数据中心部署的分布式(Distributed)数据库;

  2. 采用了乐观的(optimistic)、无锁的(Lock-Free)、多版本的(multi-version)、基于时间戳排序(timestamp-ordered)的并发控制技术,实现了两阶段事务模型;

  3. 支持可序列化快照隔离(SSI:Serializable Snapshot Isolation),解决了写偏序(Write Skew)的异常。

TiDB是国产的非常优秀的分布式数据库,关于它的内容,我们在第20讲:基于 TiDB 框架的分布式事务的实现,会详细细介绍。

综合来看,这些产品的一致性目标不仅要满足单机数据库系统下的事务一致性,也要确保每个事务符合 ACID,还需要通过采用分布式并发访问控制等一系列复杂技术最终避免各种分布式的一致性问题。

 

总结

 

通过这一讲的内容,相信你已经了解了什么是分布式事务,以及产生分布式事务问题的解决思路。

 

在实际工作中,如果你遇到了分布式事务问题,并能够在业务层面上规避,这是成本最低的解决方案。你需要先跟产品团队商议,既要避免同时显示实时的跨库数据,又不能影响用户功能的使用。如果业务层面无法规避,比如转账功能,技术团队可以采用异步消息中间件解耦微服务,来规避分布式事务问题。如果业务要求一致性、实时性比较高,并且不太适合采用消息这种异步解耦的方式,那么就可以采用事务中间件框架比如 Seata 来解决,其次可以考虑分布式数据库这种方案。

 

你在遇到分布式事务问题时采用的是哪种解决方案呢,又为什么选择这个方案呢,欢迎在留言区与我一起讨论。

 

本讲内容到这里就结束了,下一讲我会带你深入剖析事务的本质及其 ACID 特性,我们下一讲再见。

相关文章推荐:

开篇词:如何实现自我基础设施新重建-从掌握一致性与分布式事务开始

导读 | 从单体到服务网格的系统架构演进之路

01 | 分布式事务及常见的解决方案

02 | 深入剖析事务的本质及其 ACID 特性

03 | 汇总分析 5 种常见数据读写异常

04 | Spring 中的事务隔离级别和传播机制

10 | 从乐观的思路(OCC、MVCC)来看并发控制的技术实现

09 | 概念辨析:分布式系统中的一致性和ACID中一致性概念异同

05 | 并发控制机制是如何保证事务的一致性和隔离性的?

06 | 底层硬核原理:从锁的思路来看并发控制的技术实现

文章转载自新架构思考,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论