聊聊事务与分布式系统 - 从零讲到通透
01 前言
本文核心是”事务“,由基础理论引出解决方案,也阐述了个人对分布式系统的理解。全文包含以下内容:
基础规范:统一基础认知,了解 SQL 规范、ACID 模型。
优秀实现:理论联系实际,剖析 MySQL InnoDB 的事务模型。
扩展进阶:X/Open DTP 模型、2PC 协议。
工程实践:柔性事务解决方案基础。
关注公众号:码神手记,第一时间获取最新干货
02 基础规范
SQL 规范的历史背景
在讲事务之前先介绍下 SQL(Structured Query Language)的历史,建立宏观认识。
在 19 世纪 70 年代,为了满足数据库查询的需要,SQL 作为一门特定领域的语言横空出世。1986 年,美国国家标准化学会(ANSI)基于 IBM 的实现将 SQL 核准为国家标准。仅在几个月之后的 1987 年,国际标准化组织(ISO)将其采纳为国际标准(ISO9075-1987)。
就像 hotspot 虚拟机是参照 JVM 规范进行实现一样,MySQL、Oracle 等数据库的 SQL 也是基于 SQL 标准实现的。标准一共有九大部分,其第一部分:Framework,对事务进行了定义:事务是一个不可分割的 SQL 语句执行序列,要么都执行成功,要么对数据库没有任何影响。不同的数据库供应商为了满足自身的需要,在 SQL 标准的实现过程中会进行一些修改,但大部分都是可以通用的。
ACID 模型
主流数据库基本都支持并发操作,事务是进行并发控制和数据恢复的基本单位,具有原子性(Atomicity,又称不可分割性)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),即 ACID 特性。如果让我们自己设计一个事务处理方案,ACID 就是用来检验方案是否正确可靠的基本原则。
原子性:一个 SQL 执行序列,要么全部成功,要么对数据库无任何影响。执行过程中发生错误时,要恢复(rollback,又称回滚)到执行之前的快照(也称保存点,savepoint)。
一致性:事务执行前后的数据完整性、准确性没有遭到破坏,这与原子性密切相关。比如 A 账户给 B 账户转账后,两个账户的总余额与转账前是一致的。
隔离性:数据库可以进行并发的事务操作,但各事务之间不能互相干扰,要防止交叉执行导致的数据不一致。
持久性:事务一旦提交,对数据库库的改变是永久的,即使系统故障也不会丢失。
数据库作为存储组件,持久性是最基本的要求。在无并发的情况下,原子性就是一致性的保证。在并发情况下,原子性和隔离性共同保证了一致性。
事务隔离级别
与多线程并发操作共享数据同理,多个事务并发操作相同的数据时可能会发生一些无法预料的事。事务隔离是数据库处理的基础能力之一,在并发事务场景下,隔离显得尤为重要。在 SQL 标准的第二部分:Foundation,定义了 4 个事务隔离级别,不同级别决定了并发事务对数据的不同影响程度。其中串行化是最高隔离级别,是最可靠的隔离级别,对性能的影响最大。隔离级别的选择就是在性能、可靠性、一致性之间进行权衡。
从低到高,事务隔离级别有四种:
读未提交(READ UNCOMMITTED)。
读已提交(READ COMMITTED)。
可重复读(REPETABLE READ)。
串行化(SERIALIZABLE)。
事务并发执行期间,不同隔离级别下可能会发生不同的现象:
P1(Dirty Read,脏读):T1 修改了一行数据,T2 在 T1 提交之前读到了这行数据。如果 T1 进行了回滚,那么 T2 拿到的数据就成了数据库中不存在的数据。
P2(Non-Repeatable Read,非可重复读):T1 读取了一行数据,然后 T2 修改或删除了这行数据并执行了提交。如果 T1 要重读这行数据,则会发现数据已被修改或删除。
P3(Phantom,幻读):T1 读取了 N 行符合某些搜索条件的数据集合,然后 T2 又生成了若干条符合 T1 搜索条件的数据,如果 T1 用同样的条件重新读取数据,将会获取不一样的数据。
三种现象出现的可能性不同,要根据业务的实际需求进行权衡,可以在事务执行之前显式地设置隔离级别。
4 种隔离级别的划分还是太抽象,怎么理解?P1/P2/P3 三种现象出现的可能性为什么不同?
并发操作共享数据的安全问题是客观存在的,由多核 CPU 并发执行进程的基本原理所决定,这是 SQL 规范必须要考虑的问题。SQL 规范是统一的国际标准,而对标准的实现则可以百花齐放,接下来以 MySQL InnoDB 的事务模型为例进行解析,相信你会有更深的理解。
03 优秀实现:InnoDB 存储引擎
本文提到的 MySQL 特指 5.7 及以上版本。
InnoDB 遵循 ACID 模型,通过使用不同的锁策略支持 SQL 规范中的每一个事务隔离级别,所有的读操作分为无锁和有锁两种情况。
无锁一致性读(Consistent Nonlocking Reads)
InnoDB 使用多版本并发控制(MVCC)的方式向查询提供数据库在某个时间点的快照。在同一个事务中,查询可以看到快照时间点之前提交的数据,但不会看到未提交或者快照时间点之后才提交的数据。旧版本数据是不能被加锁的,其读取结果是通过 undo 日志在内存中构建出来的,读取过程中也不会设置任何锁,其它事务的锁设置也都会被忽略,称为无锁一致性读。例如最常见的 SQL:
有锁读(Locking Reads)
与无锁一致性读对应的就是有锁读,InnoDB 支持以下两种有锁读:
SELECT … LOCK IN SHARE MODE:搜索中遇到的所有索引记录都会被设置共享锁,其它会话可以读取但不能修改,直到当前事务提交。如果发起查询时,数据已被其它事务修改但尚未提交,查询将会被阻塞并等待修改数据的事务结束后(提交/回滚)获取最新的值。
SELECT … FOR UPDATE:搜索中遇到的所有索引记录都会被设置排它锁,其它事务的 UPDATE、SELECT … LOCK IN SHARE MODE 都会被阻塞,在特定事务隔离级别下(指 SERIALIZABLE),SELECT 也会被阻塞。
无论是共享锁还是排它锁,默认都是锁定索引区间范围,其它事务是无法在索引区间的缝隙中插入新数据的,此时不会出现脏读、幻读以及不可重复读。除非在查询时使用到了唯一索引,查询的结果只有唯一的一行,才会只锁定对应的索引记录。
不同事务隔离级别的实现策略
REPETABLE READ:InnoDB 默认的隔离级别,同一事务中每次无锁读都读取首次读到的快照,保证了可重复读、不会脏读,但可能幻读。有锁读遵循上文中的默认规则。
READ COMMITTED:同一事务中每次无锁读都会设置并去读它自己的最新快照(仅包括已提交的数据版本),保证了不会脏读,但可能幻读以及不可重复读。有锁读时,InnoDB 只会锁定已存在的索引记录而不包括有序索引区间里的缝隙,这一点和默认有锁读规则不同,可能会发生幻读。
READ UNCOMMITTED:同一事务中每次无锁读都会设置并去读它自己的最新快照,注意与 READ COMMITTED 不同的是快照会包括未提交的数据版本,不可重复读、脏读、幻读都有可能发生。有锁读时,和 READ COMMITTED 一样,可能会幻读。
SERIALIZABLE:有锁读时遵循上文中的默认规则。无锁读时,分为两种情况:关闭或启用 autocommit。当关闭会话的 autocommit 时,InnoDB 会将所有 SELECT 查询隐式转换为 SELECT … LOCK IN SHARE MODE,即加上了共享锁,变成有锁读,不会出现脏读、幻读以及不可重复读。当开启会话的 autocommit 时,每个无锁 SELECT 本身就是一个独立的事务,而且是只读的,事务的 SQL 序列中只有一条,也就不存在脏读、幻读、不可重复读的问题了。
如何选择合适的隔离级别?
REPETABLE READ 是默认的事务隔离级别,也是最常用的。你可以使用默认隔离级别保证较强的数据一致性,也可以使用 READ COMMITTED、READ UNCOMMITTED 弱化一致性。在某些场景下,强一致性、可重复读要比更小的锁开销更加重要,此时可牺牲一定并发能力,使用数据一致性较强的事务隔离级别。SERIALIZABLE 是一致性最强,并发能力最弱的隔离级别,通常很少使用。
04 扩展进阶
分布式事务
通俗地讲,一个事务中的操作涉及多个数据库实例(跨库事务)或多个应用服务时,就叫做分布式事务。分布式事务同样要遵循 ACID 模型,但由于事务跨库,处理过程变得更复杂。系统必须能够将多个数据库实例上的操作指向同一个事务,必须考虑每个数据库实例上的操作完成情况来做出提交或回滚的决定,无论提交还是回滚,都必须在所有数据库实例上统一生效(要么都提交,要么都回滚)。
以上是对分布式事务比较通俗、便于理解的解释。当然,也有更加抽象、规范的标准。
X/Open DTP Model(X/Open 分布式事务处理模型)
成立于 1996 年的 The Open Group 组织,以厂商中立的角色致力于标准的制定与推广,分布式事务处理模型就是由该组织制定,即:X/Open Distributed Transaction Processing Model,简称 X/Open DTP Model 或 XA。该模型是一种可扩展的软件架构(XA,eXtended Architecture),定义了四种软件组件和六种组件间接口。它允许多个应用程序共享由多个资源管理器提供的资源,并在一个全局事务中协调这些应用程序的工作。
四种软件组件
Application Program(AP,应用程序):实现终端用户所需要的功能。每个应用程序都包含一个操作序列,这些操作会引用一些资源(比如:数据库中的数据)。应用程序定义了全局事务的开始与结束,在事务边界内访问资源,且通常决定着每个事务分支的提交与回滚。
Resource Managers (RMs,资源管理器):管理已定义的部分共享资源,对这些资源的访问需要使用 RM 提供的服务,比如数据库管理系统就是最常见的资源管理器。在 X/Open DTP 模型中,资源管理器对所管理资源的改变都是可恢复的。一个全局事务会关联多个 RM,每个 RM 上都会创建一个全局事务的分支(Branch)。
Transaction Manager(TM,事务管理器):管理全局事务,分配标识符给事务,监控事务进程,协调所有资源管理器完成事务以及失败恢复。应用程序通过调用事务管理器定义全局事务的开始和结束。
Communication Resource Managers(CRMs,通信资源管理器):控制一个事务或跨多个事务的应用程序之间的通信。CRM 允许 DTP 模型中的一个组件实例访问当前事务域内部或者外部的其它组件实例,上级事务可以通过 CRM 将信息传递给下级事务。在 X/Open DTP 模型中,CRMs 使用 OSI TP 服务提供跨事务管理器域的通信层。在一个事务管理器域中可以使用不同的 CRM 支持不同的通信模式。每种通信模式都有自己的优势,X/Open 提供了以下几种比较流行的通信模式:TxRPC、XATMI、Peer-to-Peer(P2P 对等网络)。
六种组件间接口
AP-RM 接口:该接口用于应用程序(AP)对资源管理器(RM)的访问,比如使用 SQL 访问数据库系统,应用程序是具备可移植性的。
AP-TM(TX)接口:提供给应用程序使用,应用程序(AP)调用该接口来协调由事务管理器(TM)管理的全局事务。
TM-RM(XA)接口:事务管理器(TM)与资源管理器(RM)之间的双向接口,事务管理器通过该接口将各个资源管理器的工作构造为一个全局事务,并协调事务的提交与恢复。
TM-CRM(XA+)接口:事务管理器(TM)与通信资源管理器(CRM)之间的双向接口。通过 CRM 对 OSI TP 的调用,支持跨多个事务管理器域的事务传播。
AP-CRM 接口:X/Open 为全局事务内应用程序间的通信提供了可移植的 API,CRMs 中对应着 API 的不同实现。
CRM-OSI(XAP-TP)接口:该接口为 CRM 和 OSI TP(Open System Interconnection Transaction Process,开放式系统互联 事务处理)服务之间的通信提供了编程接口,位于七层 OSI 模型中的表示层。多个事务管理器域之间的通信必须要用到 OSI TP 服务。
两阶段提交协议
DTP 模型中使用两阶段提交(Two-Phase Commit)进行事务的提交/回滚:
准备阶段:TM 向所有与当前事务相关的 RM 发起准备请求,每个 RM 评估自身是否能正常进行提交,并在响应中告知 TM。这类似于一个 TM 发起投票,各个 RM 进行投票的过程。
提交阶段:在第一阶段中,如果所有 RM 能在一定时间内表示 OK,则第二阶段由 TM 向所有 RM 发起提交请求。有任何一个 RM 没有准备好,TM 则向所有 RM 发起回滚请求。
从发起事务到完成,一个 RM 最多要经历 8 次 XA 接口调用。要想完全实现 DTP 模型是非常有难度的,在实践中将会面临众多的问题。
05 工程实践
微服务架构的提出者 Martin Flowler 曾提出这样的忠告:微服务架构中应尽量避免分布式事务。换而言之,分布式事务的最佳解决方案是不用分布式事务。一旦使用了分布式事务,即便一些异常情况出现概率很低,我们也需要付出一些精力和代价,去避免可能会发产生的不可接受的后果。在工程实践中,局部的完美经常性无法达到,最难和最重要的通过取舍找到全局平衡,原因可以参考分布式系统的 8 大谬论以及 FLP 不可能定理。
分布式系统 8 大谬论
最初由 Sum Microsystems 的创始人 Peter Deutsch 提出,1997 年被 Java 之父 James Gosling 完善。之所以称之为谬论(错误的假设),是因为历史上的无数实践已经做出证明。
网络是可靠的。
延迟为 0。
带宽是无限的。
网络是安全的。
网络拓扑结构不会改变。
有一个网络管理员。
数据传输成本为 0。
网络是同质的。
以上 8 点总结起来就是两个字:网络,而分布式系统必然有网络调用,网络问题值得我们在进行系统设计时好好斟酌。
FLP 不可能定理
FLP 取自 Fischer、Lynch、Patterson 三位科学家名字的首字母。1985 年,三位科学家在一篇论文中(Impossibility of Distributed Consensus with One Faulty Process)提出并证明了该定理:在网络可靠,但允许节点失效(即便只有一个)的异步模型系统中,不存在可以完全解决一致性问题的共识算法。(No completely asynchronous consensus protocol can tolerate even a single unannounced process death。)
在实践中,我们如何从 8 大谬论和 FLP 不可能定理中自我拯救?既然干不掉它们,那就想办法共存。
DTP 在单体架构下的应用
小概率的异常事件并不会使分布式失去应用价值。分布式系统在工程实践中可以做到利大于弊,可以通过付出一些尚可接受的代价去获得更大的收益。
对于单体架构的应用,事务的场景通常是一个应用程序访问一个数据库,且不存在远程调用,无需实现完整的 DTP 模型即可满足需求(仅仅实现一个 AP、一个 RM,用不上 CRM 和 OSI TP)。随着业务体量和系统复杂度的提高,单体架构开始向分布式架构演进。以业务领域为划分标准,单体应用拆分成了多个应用,大而全的单数据库实例拆分成了多数据库实,形成了一个或多个 AP 访问多个 RM 的分布式事务场景。此时我们需要在 CRM 组件以及 OSI TP 组件的支持下,使 TM 能够跨多个 AP、RM 协调全局事务的完成。至此,一个完整的基于 DTP 模型的分布式系统出现了。在这样的系统中,我们必须做出以下假设:存储设备、中间件、网络通信、应用程序,任何一个节点都有可能出现故障。如果节点能够内部纠正错误,则不会对全局事务产生负面影响,反之则可能出现一些业务无法接受的数据不一致。
Spring 是 Java 世界中流行的应用服务开发框架,对事务管理进行了统一抽象,提供了一致性的编程模型,可以支持不同的事务 API,包括:JTA、JDBC、JMS、Hibernate、JPA、MyBatis 等。使用 Spring 提供的编程模型可以有效屏蔽使用不同事务 API 时的差异,通过事务注解可实现无代码侵入的事务管理,帮助开发人员把精力集中在业务逻辑上。Atomikos 是一个第三方的 JTA 实现,支持 XA,可以集成到 Spring 工程中使用。Atomikos 有免费和收费两个版本,只有收费版可以支持跨 AP 通信的分布式事务(Atomikos 官方称之为 Microservice Transactions)。如果你的分布式事务场景不涉及跨 AP 通信,Atomikos 会是一个合适的选择,比如:一个应用程序中连接多个数据源。
DTP 在分布式架构下的限制
由于老板很努力,公司的业务蒸蒸日上,团队规模变得更大,对系统的稳定性、吞吐量、性能有了更高的要求,出现了多个 AP 访问多个 RM 的分布式事务场景。此时需要更好的解决方案(不想用收费版 Atomikos,也不想依赖 Atomikos)。于是行业内开始涌现出一些分布式事务解决方案(京东的 JDTX、阿里的 Seata),它们参考了 DTP 模型,但又不完全遵循标准的 DTP,主要原因在于 DTP 有以下几种限制:
性能:标准的 DTP 是完全同步阻塞性的协议,锁时间长,响应时间也相对较长。
资源限制:并不是所有的资源都能做 RM、支持 XA 接口,MySQL 也是 5.7 开始才较好地支持了 XA 接口。
Server 限制:Weblogic 这样的重量级 Server 可以支持跨多 AP 的分布式事务,但微服务以及容器化趋势下基本不会有人选择 Weblogic,恨不得裸机部署才觉得香。
DTP 模型完全满足 ACID(前提是使用 SERIALIZABLE 隔离级别),是分布式事务的可选方案之一。但在当代,大多数互联网公司更愿意在一致性上做出一定程度的妥协,从而换取高并发,这一点可以参考 CAP 理论与 BASE 理论。
CAP 定理
2000 年,加州大学柏克莱分校的计算机科学家埃里克·布鲁尔在分布式计算原理研讨会(PODC)上提出猜想。2000 年,麻省理工学院(MIT)的赛斯·吉尔伯特和南希·林奇发表了布鲁尔猜想的证明,使之成为一个定理。
在理论计算机科学中,CAP 定理(CAP theorem),又被称作布鲁尔定理(Brewer's theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点:
一致性(Consistency):所有节点访问同一份最新的数据副本。
可用性(Availability):每次请求都能获取到非错的响应,但是不保证获取的数据为最新数据。
分区容错性(Partition tolerance):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在 C 和 A 之间做出选择。
想象有两个节点,允许至少一个节点更新状态则会导致数据不一致,即丧失了 C 性质。如果为了保证数据一致性,将其中一个节点设置为不可用,那么又丧失了 A 性质。除非两个节点可以互相通信,才能既保证 C 又保证 A,这又会导致丧失 P 性质。
BASE 理论与柔性事务解决方案基础
2008 年 5 月 1 号,eBay 的架构师 Dan Pritchett 在 ACM 上发表了一篇文章,名为《BASE: An Acid Alternative: In partitioned databases, trading some consistency for availability can lead to dramatic improvements in scalability.》
BASE(basically available,soft state,eventually consistent)是 ACID 的替代方案,ACID 追求强一致性,而 BASE 允许数据库的一致性处在不断变化中。BASE 通过牺牲一些一致性换取可用性,这样可以显著提升系统的伸缩性。BASE 的核心是以下三点:
基本可用(basically available):在整个系统可用的前提下,允许部分分区的失败。
软状态(soft state):网络必然会有延迟,在通信完成之前,允许中间状态的存在。如果延迟足够短,对用户来说是透明或者可容忍的。
最终一致性(eventually consistent):处于中间状态时会有短暂的数据不一致,只要最终数据一致即可。
Dan Pritchett 在文章中给出了一个通用的不依赖 2PC 的分布式事务解决方案:消息队列解耦法。
将全局事务中的分支解耦。从设计上解开依赖,和尽可能不用分布式事务的思想不谋而合。
在数据库中记录全局事务信息。
将事务分支信息写入消息队列。
从消息队列中取出事务分支信息,执行并持久化。
消息队列解耦法注意事项:
消息持久化:消息队列中间件需要有持久化能力,可以在故障后自动恢复,避免丢失消息。
幂等性要求:意外的消息重复会导致事务分支重复执行,必须保证重复执行的结果是一致的。
消息顺序问题:不怕一万就怕万一,假设上游生产者意外不能按顺序写入消息,消息中间件意外不能按顺序接收消息。那么极短时间窗口内的先后两条消息,有可能并不是按照先后顺序进入消息队列。当业务与顺序性紧密相关时,要注意处理这种情况,常见的方法是借助单调递增的事务 ID、毫/微秒级时间戳判断。
BASE 是 CAP 在长期实践中得出的普适性方案,大多数场景都能适用,是柔性事务解决方案的理论基础。分布式事务解决方案分为四种模式:AT、TCC、Saga、XA,XA 模式在上文中已经讲过,很少被使用。在后续文章中,会对 AT、TCC、Saga 模式进行详细梳理。
06 参考资料
ANSI:https://blog.ansi.org/2018/10/sql-standard-iso-iec-9075-2016-ansi-x3-135/
MySQL:https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-transaction-model.html
国际标准:ISO/IEC 9075-1:2003 Infomation Technology - Database Languages -SQL -Part1: Framework (SQL/Framework)
国际标准:ISO/IEC 9075-2:2003 Infomation Technology - Database Languages -SQL -Part2: Foundation (SQL/Foundation)
分布式事务处理-XA+规范:Distributed Transaction Processing:The XA+ Specification Version 2
维基百科:https://zh.wikipedia.org
ACM:https://dl.acm.org/doi/10.1145/1394127.1394128
版权声明: 本文为 InfoQ 作者【刘绍】的原创文章。
原文链接:【http://xie.infoq.cn/article/1d7b352523b5bcd2ce2248a11】。
本文遵守【CC BY-NC】协议,转载请保留原文出处及本版权声明。
评论