深入浅出事务的本质,附 OceanBase 事务解析 14 问!
作者:颜然,蚂蚁集团资深技术专家
OceanBase 初创成员之一,OceanBase 分布式数据库事务研发负责人,目前负责事务引擎、高可用架构、负载均衡、性能优化等方面的工作。
事务的前世
每个人的手机和电脑中几乎不会安装一款叫“数据库”的应用,但是几乎每一款 App 内部都会使用一个叫“数据库”的更底层的系统来存储数据。每个人日常生活中都会订票、购物、付款,但也许还没有意识到这些操作的背后都是由“数据库”系统来支撑。那么究竟什么是“数据库”?“数据库”和“事务”又有什么关系呢?
计算机系统在其发展的历程中经历了多次重要的事件,而上个世纪 60 年代迎来一次重大变革,为计算机系统带来了巨变,自那之后,计算机就开始以数字化的方式重塑整个世界的进程。这次变革就是“磁盘”的大规模使用。在磁盘之前,计算机使用“磁带”来存储数据。计算机使用的磁带,和听歌使用的磁带,或者看电影用的录像带,本质是一种东西。这是个暴露年龄的比喻,毕竟这两种东西现在已经只能在博物馆里才能看到了。磁带的最大问题是不方便找东西,“倒带”是个极其需要耐心的活。这就决定了,当时的计算机所处理的数据都是通过后期誊录到磁带上的,计算机只是用于统计和汇报所需。
最初的磁盘是个大家伙,看下图也许分辨不出大小,但实际上它有微波炉那么大。这第一块硬盘,呃,不能称为一块,这一台硬盘只能存储 3.75MB 的数据。当然,随着技术的不停迭代,硬盘越做越小,容量越做越大。再到后面,又出现了升级版的闪存介质,硬盘就小到无孔不入了。但是不论大小,这些存储设备随机访问的特性都是一样的。
磁盘最重要的特性是“随机访问”,即用户需要查找的东西,可以轻松地定位到。计算机的快速处理能力,加上磁盘对数据的快速定位和修改能力,珠联璧合,为计算机系统打开了新世界的大门。自此,计算机开始参与到人类社会的方方面面。比如,航空公司的第一个售票系统 Sabre,使用计算机和磁盘直接存储每一个待售卖的航班客票信息和购票人信息。这些信息存储在硬盘上,被售票员实时访问和修改,直接替代了以纸质方式记录客票信息的方式。在此之后,银行、通信、交通等各行业,都使用磁盘来存储其业务信息,并使用计算机来响应各种“实时处理”的需求。
对于数据处理需求的极速膨胀,专门用于数据存储和访问的“数据库管理系统”也应运而生,并被作为一个独立的系统而广泛接受。一个软件系统之所以能独立出来并被市场接受,原因在于其可复用性,也就是说,在众多的业务场景中,使用了这个软件要比不使用的时候,更方便,效率更高,世界也会因此而变得更美好。
数据库管理系统帮助使用者解决的通用性问题主要有:
数据存储与管理
数据访问
数据变更
高可用
数据的存储与管理解决了数据如何管的问题,它本质上是对存储设备的提炼和抽象,让用户不用关心数据在硬盘上具体是如何放置的,而只用关心数据的模型即可。从早期到现在,数据模型层出不穷,在关系模型之前,有网络模型和层次模型,在关系模型之后,有对象模型、文档模型、图模型等。
数据的访问解决如何在数据中做查询和分析,在数据模型之上,数据库系统会提供抽象的接口让用户方便使用,比如 SQL 语句就是在关系模型之上有超强表达能力的数据访问接口。
数据变更解决的就是修改数据的问题,而修改数据的核心在于怎么保证数据的正确性和一致性,把数据改错了,是用户最不愿看到的情况。而“事务”功能就是“数据变更”功能的核心。事务功能提供给使用者 4 个著名的特性:原子性 Atomicity、一致性 Consistency、隔离性 Isolatation、持久性 Durability,简称 ACID。使用者将多个数据变更的操作以一个事务的整体提交到数据库管理系统上进行操作,那么数据库管理系统就能保证使用者做的数据变更具有 ACID 的特性。
高可用特性是近些年来人们对系统特别强调的一个要求。以前,系统往往强调可靠性,而现在系统则更多的改为可用性。究其原因,随着互联网逐渐深入人们的生活,在线化项目的快进,服务不可用对于人们生活的影响越来越大,所以人们对于系统的要求也从可靠性变成了可用性。这是一种质的变化,可用性等于可靠性加上服务的连续性。高可用也和事务特性密切相关,我们将在后文提及。
再来,我们说回事务处理。
数据库管理系统本是给另一些软件开发者使用的,用于存储、查询、修改业务数据以及表达业务逻辑。既然已经有了关系模型,有了 SQL 语句来给开发者使用。作为写程序的开发者,对存储在硬盘上的数据做一些修改,直接改不可以吗,为什么还需要“事务”功能呢?难点就在于计算机系统并不太好操作,设想在售票系统售票过程中,如果我们修改了预定座位信息,但是还没有收钱,计算机故障了,待计算机恢复的时候,想要把这个错误的信息核对出来并修复,那可就一点也不简单了。而事务功能,则可以解决开发者这方面的顾虑。
事务的挑战
事务功能究竟解决了哪些难题,从而让使用者觉得世界更美好的呢?
事务功能从本质上做了两件重要的事情,即“故障恢复”和“并发控制”。前者保证在计算机系统出现故障时,数据修改的原子性和持久性。后者保证在系统内的数据处于并发操作时,操作与操作之间可以保持隔离性。这两者共同的目的是保证数据库管理系统在对数据进行操作时,数据本身要保持完整性和一致性。比如,张三给李四的转账操作时我们要同时修改张三的账户和李四的账户,这次转账操作不会因为故障而导致凭空多出来钱或少了钱,也不会因为同时进行张三和王五的转账操作,而导致张三的钱变成负数。
“故障恢复”和“并发控制”挑战性高吗?是的,非常高。
先说“故障恢复”。计算机系统可能会出现什么故障呢?存储数据的硬盘会损坏?主板可能突然烧坏导致系统停机?机器会断电?网线会时断时续?交换机会停摆,甚至夏季用电高峰期整个机房可能面临无电可用的情况,然而储备的柴油发电机只能支持几个小时?上述情形都是本人曾经遇到过的情况。
事务的“故障恢复”功能就是为计算机编制好应对这些异常的手册,通过异常处理的流程,保证原子性和持久性。目前最常见的,也是 OceanBase 使用的故障恢复的方案,即使用日志。
日志指的是一本记录操作的“流水账”。为了实现很多个数据变更操作一起成功,而记录修改的硬盘又不能让多个写操作一次性完成。事务功能的做法是把一个事务内所有要做的修改转化成在日志系统里的记录,把所有要修改的内容都记录完之后,记录一个结束标记“搞定”,整个事务是否原子的完成,就取决于这个“搞定”标记最终是否完成。一旦最终的标记完成,就意味着事务内的所有修改都已经被记录下来了,即使实际数据变更说因为故障没有完成,但是当系统重新恢复后,日志里都记录着这个事务需要做的所有事情,再重新做一遍即可。
保证刚刚完成的事务不会丢失的秘密就在于这个日志。数据库会把这个日志记录在不只一块硬盘上,保证如果有一块硬盘意外损坏了,还有其他硬盘上存储的日志可以用来恢复事务的修改。为了提升保障能力,比如保障当机器损坏时日志不会丢,当机房故障时日志不会丢,甚至城市内的多个机房都发生故障时日志亦然不会丢,日志需要保存在更多的地方。OceanBase 的三地五中心架构下,日志会存储在三个城市的五个数据中心里,因为有 Paxos 协议加持,所以有任意两个城市存储成功,事务就能提交成功,同时还保证了任何一个城市的全部机房出现故障,事务数据都不会丢。
悄悄告诉大家一个内幕消息,目前还没有数据库能对地球故障进行容灾,如果地球没了,数据就都丢了, OceanBase 还需努力。
数据库的高可用特性也和刚才说的多个副本的处理机制密切相关。最新产生的日志通过 Paxos 协议同步到其他副本,根据部署的规格不同,可能是同一个城市的其他机房,也可能是另一个城市的机房,那么日志和对应的数据就在多个机房或者多个城市有备份,当出现机器或者机房故障时,根据 Paxos 协议,会激活另外一个具有完好副本的机房,继续提供数据库的服务。这也是 OceanBase 数据库高可用能力的基石。
再来,我们谈谈“并发控制”。
如果数据库系统一次只处理一个用户的一个事务,那么就没有“并发控制”的需要了。一个事务接着一个事务做,最安全。但是这样太低效了,放着 CPU、硬盘、网络这些资源利用不起来。数据库管理系统作为一种系统软件,其内在的使命就是把计算机系统的硬件能力尽可能发挥出来,让同样的硬件可以更好地满足实际业务的需要。“效率”是系统软件与生俱来的使命,而“高效”也是复杂性的由来所在。
事务的“并发控制”其根本是在解决对于数据读和写的并发问题。首先,数据库系统要决定并发控制的粒度,常见的并发控制粒度有以数据存储页面为单位的和以数据行为单位的。一般来说,粒度越小越好。粒度越小,相互之间的影响越小。OceanBase 采用的并发控制粒度是“行”。其次,要解决读和写的并发问题。这里要分别列举说一说。第一,读与读之间的并发操作;第二,写与写之间的并发操作;第三,读与写之间的并发操作。
读与读之间,不需要特别的处理,一份数据被读多少次,数据还在那里,不增不减。
写与写之间,不能同时进行,否则数据就会被改乱了。OceanBase 通过行级别的互斥锁机制,即一个事务修改过的行,都会通过行锁保护上,在事务结束之前,这些行是不允许被其他事务修改的。
读与写之间,多版本并发控制是目前的大势所趋,OceanBase 采用的也是多版本并发控制。事务执行过程中更改的数据都是以新版本的形式存储在系统中,保证更新前的数据依然在系统中,在事务提前之前,事务更改的数据都是不生效的,这时有读取操作读到这些行时,可以直接读之前的数据。这是多版本并发控制的最大魅力,一行数据在做修改时,并不影响同时间进行的数据读取。通过读与写之间是隔离开的设计,系统就可以尽可能“榨干”硬件的能力。
分布式事务
下面,我们要讨论分布式事务。
分布式环境给事务功能带来了新的挑战,所有的一切都是因为分布式系统里的“延迟”带来的。分布式系统什么样的延迟问题带来了这么大的挑战呢?
延迟这件事情,伴随着计算机系统的每一次通信,而通信是所有计算机操作的基础。
CPU 访问内存耗时 50 纳秒,而同一个机房内的网络延迟是 100 微秒。也就是说,CPU 的一个从本机内存里取数据的操作,从发起指令开始到把数据取到,需要 50 纳秒。如果需要取的数据在另一台机器上,即使是在同一个机房内,那也需要 100 微秒,两者相差 2 千倍。
这种延迟差别带来两个影响,一是要考虑通信的延迟,设计专门的算法;二是一台机器故障时,其他的机器不会立即知道,容灾处理需要考虑这种新的故障场景。
考虑事务功能要解决的“故障恢复”和“并发控制”这两件事情,在分布式环境下会遇到哪些新的挑战呢?
先来看“并发控制”。
多版本并发控制机制需要一种表达全局快照版本的方式,通常有两种:一种拿的快照叫 Read View,另一种拿的快照叫 Read Version。这两者的差别是,事务是否是在提交时才能确定版本号。如果事务不是在提交时才确定版本号,那么快照版本需要包含所有活跃的事务列表,这是很难扩展的一种架构。 OceanBase 使用的是后者,即一个快照版本是一个时间戳,因此命名为 Read Version。这种模式的扩展能力要强很多,但是依然需要有一个全局的单点提供时间戳。在 OceanBase 里,提供全局时间戳的叫全局时间戳服务 GTS,虽然这个时间戳服务每秒可以处理几百万次请求,用户的业务需求应该都能满足,但是我们还是希望 OceanBase 的处理能力是完全可扩展的。OceanBase 采用了聚合方式取时间戳,每台服务器同一时间并发的事务可以聚合起来取时间戳,这大大降低了对于时间戳服务的请求。OceanBase TPC-C 测试时,每分钟要执行 15 亿个事务,时间戳服务支持起来完全不在话下。
对比业界另一个分布式数据库 CockRoachDB。CRDB 采用的快照模式与 OceanBase 是一样的,都是 Read Version 方式。但是,CRDB 没有采用全局时间戳服务,而是使用了混合逻辑时钟 HLC。
HLC 的优势是其本质是逻辑时钟,基于通信的方式保证有因果关系的事件之间时钟值是有严格顺序的,这种模式不需要一个单点来生成时间戳。但是,HLC 有一个巨大的缺陷,在数据的一致性上无法做到与传统数据库相同的全局有序的事务处理,按照 CRDB 系统自己的描述,他们只能保证单行的线性一致性。
怎么理解单行的线性一致性呢?就是数据库只保证如果两个事务修改的是同一行,那么其先后关系才是明确的。反过来理解,如果两个事务修改的是不同的行,那么数据库不保证其数据修改的先后顺序。绕不绕?给大家举一个这种场景下会遇到的问题就容易理解了。交易业务中的一个常见场景,如用户下单买一件商品,有一张表存储这个订单是否支付完成的状态,另一张表存储这个订单是否发货的状态。付款系统处理了用户付款动作,并修改了订单状态为已支付,然后通知发货系统,发货系统确认订单已付款后,发起了发货动作,并修改订单状态为已发货。已支付和已发货是先后修改的数据,但是是在两张不同的表里。如果这时有另一个查询动作来查询这个订单的这两行数据,在 CRDB 中是有可能看到已发货的状态,同时是未支付的状态。也就是说,看到了后面一次的修改,但是没有看到前面一次的修改,这会给数据库的使用者添加很多负担,也是 OceanBase 为什么没有选择这种方式的原因。
再看“故障恢复”。
在分布式环境下,事务的更新会出现在多台机器上,用于完成“故障恢复”的事务日志也会分别在不同的机器上记录,这时日志记在了多处,想要保证记录操作的日志全部都能记全,就需要新的流程。这个新流程就是“两阶段提交协议”。两阶段提交的新增流程就是先让每台机器记录自己的操作日志,但是先别标记最终成功,待协调者确认了所有操作日志都记录成功后,再记下事务提交成功的记录,这就保证了日志记录原子成功的特性。
刚才描述的是经典两阶段提交协议的流程,而 OceanBase 的极致优化,则让两阶段提交协议可以在更短的时间内完成。OceanBase 的策略是不依赖协调者最终记的事务成功日志,而是依赖每一个参与者上记录的操作日志,在两阶段执行过程中如果出现了系统故障,那么由参与者互相校验事务的日志是否记全了,最终依然可以通过确认所有的参与者都记全了各自的操作日志,来确认事务最终提交的状态。如果不是所有人都记全了,那么事务就没有完成提交,最终就会呈现回滚状态。
OceanBase 的两阶段提交协议将事务的提交延迟从传统协议中三次日志同步延迟减小到只有一次日志同步延迟,极大提高了事务提交的效率。
继续对比 CRDB。与传统数据库和 OceanBase 有很大差别的一点,CRDB 的整个事务持久化模型由 KV 系统完成,事务不再操作独立的日志系统,所以 CRDB 失去了用日志系统记录系统变更的机会,完全依赖于 KV 系统的操作。从 CRDB 的设计原则来看,这是在简化分布式系统的复杂度,但是这个简化也引入了大量的负担。在 CRDB 中,事务提交时依然要保证事务内的修改在故障时的原子性。但是,事务内不同的修改是直接操作 KV 中的对应的行,CRDB 要保证事务内修改的所有行原子的生效,相当于,每一个行都是 CRDB 事务的参与者,虽然最终也能完成事务“故障恢复”的工作,但是开销很大。
总结
时至今日,事务特性不只在数据库管理系统中使用,也被应用于其他系统里以描述类似的需求,比如最新的 Intel 服务器 CPU 支持以事务特性来访问内存,这就把事务的原子性和并发控制引入了内存系统中。这么做的好处是可以让程序开发者更方便地使用内存来表达复杂的修改逻辑。
事务的内核就是把复杂的操作逻辑隐藏在系统内部,给使用人员暴露出简单的接口,让使用的人更方便。把复杂留在系统内,把简单留给使用者,这是数据库系统的最大价值,也是 OceanBase 各种功能开发中秉承的原则。
----------
【彩蛋环节】
QA: 结合用户关注的 14 个问题,颜然老师了解答,具体问答见下,希望能给到大家一点帮助。
问 1、OceanBase 作为一款分布式数据库,其内部是如何界定一个事务是属于分布式还是属于单机事务,比如以下情况:
a. 同一个事务涉及到多个副本在同一个 observer 节点
b. 同一个事务涉及的多个副本在不同的 observer 节点
答:OceanBase 是按分区区分是否为分布式事务。OceanBase 会自动追踪一个事务内的修改,到事务最终执行 COMMIT 命令时,如果发现修改都在一个分区上,就会通过分区内的一阶段提交逻辑完成事务原子提交,如果修改的内容出现在多个分区,就会走两阶段提交。
问 2、OceanBase 作为一个分布式数据库是如何做到交换机级故障后,服务快速恢复的?
答:OceanBase 通过多副本来容灾,如果要容交换机故障,只需要跨交换机部署 OceanBase 的多个副本,多个副本通过 Paxos 协议保证一致性。如果交换机出现故障,只要故障的交换机之外还有剩下多数的副本,这些副本会重新选主并恢复数据库服务。
问 3、OceanBase 分布式事务的两阶段提交优化为一次落盘两次 RPC,能否具体讲讲故障情况下怎么保证一致性?
答:事务的所有修改依然是依赖日志来进行持久化,OceanBase 的优化是让所有参与者同时记录日志,只要这次日志都记录成功了,事务的所有变更就是完整的,根据这些日志里记录的修改,就能恢复事务内所有变更,如果因为故障有参与者没有记录成功,那么识别出这种情况后,就走事务的回滚逻辑,将事务内的所有修改回滚掉。所以,整体只需要依赖参与者的一次日志持久化就能保证前面的逻辑。两次 RPC 通信,就是用来确认所有参与者都持久化成功,以及告诉所有参与者事务最终是否成功。
问 4、请问一下 OceanBase 多版本控制,同个主键不同版本数据是如何组织的,是存在同一个树里还是怎么实现?
答:一共有两种组织方式:在内存中和硬盘上。内存 MemTable 是 btree 组织的行结构,行内多版本是链表结构,硬盘 SSTable 中是按照行数据和行内的多版本进行连续存储。
问 5、能介绍下 spanner 里面时间相关的东西吗?
答:spanner 的 TrueTime 本质上是各台服务器机器在普通的 wall clock 上加了一个误差值,要保证因果关系时,就要等完使用的时间戳对应的误差值。
问 6、OceanBase 分布式事务的内部类似一个优化后的 XA 方案,其对事务查询还有其他优化吗?比如说 prepare 后,对查询可以直接读取,但是对于写操作还是需要二阶段异步完成后才可以写?是否提供微服务级的分布式事务协调?外部 XA 调用?或者是其他事务解决方案?
答:OceanBase 现在也支持通过 XA 协议与其他系统做联合事务,在这个使用模式下,OceanBase 就是 XA 里的一个 RM ,只是 OceanBase 这个 RM 内部还是会通过两阶段协议保证自己内部的分布式事务的原子性。相当于是级联的两层两阶段提交。XA 协议本身不解决并发控制的问题,只解决原子提交。如果 XA prepare 后就允许读取的话,相当于没有并发控制,可能出现一些事务的数据问题,比如一个 RM 的 XA prepare 即使成功,其他 RM 可能失败,最终事务还会回滚,即刚被读取的数据就又被回滚掉了。微服务使用 XA 来解决事务的需求是一种较优的方向,相比较外部的事务系统,X 对业务开发最为友好。
问 7、请问 OceanBase 支持的隔离等级有哪些呢?
答:OceanBase 的 mysql 模式支持 rc rr s、oracle 模式支持 rc s ,和对应兼容的系统一致。
问 8、请问 OceanBase 是通过行级锁还是 occ 的方式来处理事务冲突呢?
答:OceanBase 实现的是行级别锁,使用悲观锁的方式处理写写冲突。
问 9、跨机房,尤其是跨 IDC,网络 IO 就成为不可忽视的性能瓶颈,一般都是异步化解决性能问题,OceanBase 是如何做到解决性能瓶颈的同时又保证查询的实时性?
答:这种跟部署和使用方式有关系。如果数据库的请求本身涉及大量跨机的处理,一般最好是把这种处理聚集在一个机房内,利用机房内的低延迟网络处理数据。只利用跨机房和跨 IDC 做副本间的数据同步,保证高可用,副本同步只同步修改日志,并且是批量聚合的方式,处理能力不受延迟影响。
问 10、使用多地多中心时,主副均匀分布在各个 zone,网络抖动频繁,会多次发生故障切换吗?是否会造成数据不一致?
答:如果 SQL 处理就要跨机房,那么网络抖动会导致处理变慢甚至超时,但数据不会不一致。
问 11、
1.OceanBase 是单点授时还是多点授时,异地多机房的场景下对性能有没有影响?
答:单点授时,异地多机房同时服务会有延迟大的影响。
2.两阶段提交简化成一阶段提交,具体方案能否说一下?和 CRDB 的并行提交是否为同一概念?
答:OceanBase 的最佳实践是利用多租户的能力,单个租户在一个地域服务,利用跨地域同步做容灾。多个租户可以分布在不同的地域提供服务。
问 12、请问 gts 是如何维护可靠性的,如果所提供的 gts 服务器故障了 OceanBase 是如何保证系统正常运行的?
答:GTS 本身也依赖 Paxos 的多副本提供高可用。
问 13、 OceanBase 对于死锁检测有什么措施吗? 如果是超时机制的话,对于长事务有特别处理吗?
答:OceanBase 最新的版本实现了分布式的死锁检测,同时保留超时机制,作为应用可选的功能。
问 14、
1、2PC 中,事务的协调者 crash 后,事务的参与者可以相互通信协商。用的协议是什么?
答:在 OceanBase 内部还是重建了内存中的两阶段状态机,只是在状态机中新增了一些协议,让参与者可以来驱动创建。
2、具体是怎么参与驱动的呢?
OceanBase 内事务的两阶段状态机本来就是参与者来负责完成的,一般是第一个参与者会在内存里构建并记录其他参与者的状态。
最后的最后,您有任何疑问都可以通过以下方式联系到我们~
联系我们
欢迎广大 OceanBase 爱好者、用户和客户随时与我们联系、反馈,方式如下:
评论