OLTP(联机事务处理)系统以高并发读写为主,数据实时性要求非常高,数据以行的形式组织,最适合面向外存设计的行存储引擎。随着内存逐渐变大,服务器上万亿字节(TB)大小的内存已经很常见,内存引擎面向大内存而设计,提高系统的吞吐量和降低业务时延。OLAP(联机分析处理)系统主要面向大数据量分析场景,对数据存储效率、复杂计算效率的要求非常高。列存储引擎可以提供很高的压缩比,同时面向列的计算,CPU 指令高速缓存和数据高速缓存的命中率比较高,计算性能比较好,按需读取列数据,大大减少不必要的磁盘读取,非常适合数据分析场景。openGauss整个系统设计是可插拔、自组装的,并支持多个存储引擎来满足不同场景的业务诉求,目前支持行存储引擎、列存储引擎和内存引擎。

openGauss存储概览

早期计算机程序通过文件系统管理数据,到了20世纪60年代这种方式就开始不能满足数据管理要求了,用户逐渐对数据并发写入的完整性、高效的检索提出更高的要求。由于机械磁盘的随机读写性能问题,从20世纪80年代开始,大多数数据库一直围绕着减少随机读写磁盘进行设计。主要思路是把对数据页面的随机写盘转化为对WAL(Write Ahead Log,预写式日志)的顺序写盘,WAL持久化完成,事务就算提交成功,数据页面异步将数据刷新到磁盘上。但是随着内存容量变大和保电内存、非易失性内存的发展,以及SSD(SolidStateDisk,固态硬盘)技术的逐渐成熟,磁盘的IO(输入输出)性能得到极大提高,经历了几十年发展的存储引擎需要调整架构来发挥SSD的性能和充分利用大内存计算的优势。随着互联网、移动互联网的发展,数据量剧增,业务场景呈现多样化,一套固定不变的存储引擎不可能满足所有应用场景的诉求。因此现在的 DBMS需要设计支持多种存储引擎,根据业务场景来选择合适的存储模型。

1.数据库存储引擎要解决的问题

数据库存储引擎要解决的问题如下:

  • 存储的数据必须要保证原子性(A)、一致性©、隔离性(I)、持久性(D)。
  • 支持高并发读写,高性能。
  • 充分发挥硬件的性能,解决数据的高效存储和检索能力。

2.openGauss存储引擎概述

openGauss整个系统设计是可插拔、自组装的,支持多个存储引擎以满足不同场景的业务诉求。当前openGauss存储引擎有以下3种:

  • 存储引擎,主要面向 OLTP场景设计,例如订货、发货、银行交易系统。
  • 存储引擎,主要面向 OLAP场景设计,例如数据统计报表分析。
  • 内存引擎,主要面向极致性能场景设计,例如银行风控场景。

创建表的时候可以指定为行存储引擎表、列存储引擎表、内存引擎表,支持一个事务中包含对三种引擎表的 DML操作,可以保证事务的 ACID性质。

本文主要介绍openGauss行存储引擎,其他的存储引擎将在后面的文章进行介绍。

openGauss行存储引擎

openGauss行存储引擎采用原地更新(in-place update)设计,支持 MVCC(Multi- Version Concurrency Control,多版本并发控制),同时支持本地存储和存储与计算分离的部署方式。行存储引擎的特点是支持高并发读写,时延小,适合 OLTP交易类业务场景。

(一)行存储引擎总体架构

openGauss的行存储引擎在设计上支持 MVCC,采用集中式垃圾版本回收机制,可以提供 OLTP业务系统的高并发读写要求,支持存储、计算分离架构,存储层异步回放日志。行存储引擎架构如图1所示。
在这里插入图片描述

图1 行存储引擎架构

图注:数据页面缓存池中缓存数据页面,在数据页面中存放元组以及元组的历史版本并集中管理,使用 Vacuum(垃圾清理)线程进行定期的空间回收。

存储引擎的关键技术有:

  • 基于事务ID以及ctid(行号)的多版本管理。
  • 基于 CSN(CommitSequenceNumber,待提交事务的序列号,它是一个64位递增无符号数)的多版本可见性判断以及 MVCC机制。页面,在数据页面中存放元组以及元组的历史版本并集中管理,使用Vacuum(垃圾清理)线程进行定期的空间回收。
  • 基于大内存设计的缓冲区管理。
  • 平滑无性能波动的增量检查点(checkpoint)。
  • 基于并行回放的快速故障实例恢复。

主要模块如图2所示。
在这里插入图片描述

图2 行存储主要模块

(二)行存储的基本模型与页面组织结构

行存储的元组结构以及页面组织,是行存储 DML实现、可见性判断以及行存储各种功能与管理机制的基石。

由于行存储是基于磁盘的存储引擎,因此存储格式的设计遵从段页式设计,存储结构需要以页面(page)为单位,方便与操作系统内核以及文件系统的接口进行交互。也是由于这个原因,页面的大小需要和目标系统中一个 block(块)的大小对齐。在比较通用的 Linux内核中,页面大小一般默认为8192字节(8KB)。一个基本的 Heap (堆)页面如图3所示。
在这里插入图片描述

图3 Heap页面示意图

页面开头的位置为整个页面的头部信息,记录了这个页面的公用信息以及一些关键标识。line_pointer被 放 置 于 Header后 面,并 向 页 面 尾 部 扩 展。line_pointer为 指 向Tuple实际数据的一个指针,类似于行指针(sentinel)的作用。

这里需要一提的是,每个 Tuple在系统中的唯一标识ItemPointer,也被称为ctid,存储的是这一行所在的页面号(block number)以及其对应的line_pointer的偏移量(offset),即这个页面中第几个line_pointer。这样由一个系统内记录的ctid,可以快速定位到这个Tuple的line_pointer,也就可以根据line_pointer的指针快速定位到Tuple的实际数据。

line_pointer的必要性也可以比较容易地总结出来。由于 Tuple的数据内容本身可以是变长的,因此如果需要找到一个在页面中间的 Tuple,则需要按序遍历页面结构;而line_pointer结构本身为定长,因此可以直接以常数的复杂度找到数据所在内存位置。line_pointer sentinel的效果也十分明显:line_pointer的存在使得 Tuple的对应改动局限于页面内部,而保持全局标识ctid不发生变化;如果没line_pointer,行更新需要连带更新的元信息、索引以及系统各处信息,复杂度就不言而喻了。

被line_pointer指向的行记录本身,则是从页面结尾开始向页面头部延展,这样避免在页面填充过程中可能出现的数据移动以及空间浪费。

页面头部的 Header中储存了如下信息:

  • pd_lsn为最后一次改动此页面事务写下的 WAL[系统中一般称为事务日志 (transactIon log),简称xlog]的下一位,被xlog机制以及检查点机制所使用。
  • pd_checksum 为页面中的checksum,为了检查页面的完整性和一致性使用。
  • pd_flags是此页面的标识位,可以让上层通过对此页面进行处理的接口快速识别此页面的一些特征,比如页面是否有空行,页面是否写满,页面是否已经对所有事务全部可见,页面是否被压缩等。
  • pd_lower和pd_upper是指向页面空闲空间起止的指针,即pd_lower指向下一个line_pointer的位置,而pd_upper指向下一个行记录数据填充的位置,这样既可以快速进行页面的填充修改,也可以方便计算页面的空闲空间。
  • pd_special指针用于记录一些特殊的存储管理方式以及接口所需的内存区域。
  • pd_prune_xid记录上一次对此页面进行清理的xid(事务ID,事务号)。
  • pd_xid_base以及pd_multi_base为这个页面上xid的base基准,即该页面上所有的记录的xid都由页面自身记录的 xid(32位)与 base(64位)计算得到,是64位xid的实现方式。

每个记录(上文元组的数据部分)是数据库中最基本的数据存储单位,其自身的结构以及记录的信息也是系统中数据存储方式、DML、事务 ACID 特性的关键。数据部分结构如图4所示。
在这里插入图片描述

图4 数据部分结构
  • xmin是最初始的事务ID(Transaction ID,简称xid),即插入此条记录的事务ID。
  • xmax是删除或更新此条记录的xid。如果此记录未被更改或删除,那么xmax为0。
  • t_cid记录的是命令ID(Command ID),命令ID 用于一个事务内部多步操作的一种记录与跟踪。
  • t_ctid记录了此条记录的ctid值,或者是更新版本的ctid值。这个会在后面展开 DML时讲到。
  • 两个t-infomask是事务以及存储数据状态的标识位,用于快速判断。xmin、xmax两个事务ID,结合其映射的 Clog(提交日志)和 CSN Log,一同构成了可见性判断的核心关键要素。

(三)行存储的多版本管理以及 DML操作

openGauss行存储的多版本机制与业界比较常见的关系数据库有较大的不同,核心区别为行存储的多版本在更新的时候并不是就地更新,而是在原有页面中保留上一个版本,转而在这个页面(如果空间不够会在新页面中)中创建一个新的版本进行历史版本的累积与更新。

相应的页面中会同时存有不同版本的同一行数据,拿到不同快照的事务,在读写这些不同版本时互不冲突,有着很好的并发性能。对历史版本的检索可以在页面本身或邻近页面进行,也不需要额外的CPU开销以及IO 开销,有着非常高的效率。同时,事务管理以及持久化角度也变得非常清晰简洁,省去了类似于就地更新所需要的记录、执行以及持久化的 Undo(回退)等相关操作。

以下就以一个 DML的例子简单介绍行存储结构以及 MVCC的实现。

假设我们在一个xid为10的事务中,在一个只有一列varchar(变长字符串类型) 数据的表中插入一条数据 ‘A’,该行数据存入编号为0的数据页面上,则该行存储结构如图5所示。

在这里插入图片描述

图5 行存储结构示意图1

可以看到xmax为0,此时该记录为有效记录。

假设在此基础上,在事务xid=20中做了删除此行的操作,则此记录的行存储结构如图6所示。
在这里插入图片描述

图6 行存储结构示意图2

此时xmax被标记为20,如果此事务提交,那么此行最终会被回收。

如果在之前插入(insert)操作的基础上,在事务 xid=30 中连续对该行做两次更新。

第一次更新的行存储结构如图7所示。
在这里插入图片描述

图7 行存储结构示意图3

原有行失效,通过t-ctid记录新版本的ctid值,进而指向下一行。

第二次更新的行存储结构如图8所示。
在这里插入图片描述

图8 行存储结构示意图4

第二个版本也变为历史版本,通过ctid指向最新版本,不过值得注意的是,第二个版本的xmin、xmax都为30,即此版本在同一事务中被删除,而最新版本 xmin仍为30,只是t-cid从0增加为1[假设此事务连续执行了两次更新(update)操作]

更新后的页面如图9所示。
在这里插入图片描述

图9 行存储结构示意图5

以上几个简单的例子比较直观地展示了行存储的基本存储结构、行存储的 DML以及行存储的 MVCC是如何结合在一起共同作用的。

存储引擎内部,索引也是重要的组成部分,索引本身指向存储的是key(键)到ctid(行号)的映射。上面也提到过了,ctid实际上指向的是line_pointer的检索信息,因此索引的页面上存储的信息及其与数据页面的关系如图10所示。
在这里插入图片描述

图10 索引的页面上存储的信息及其与数据页面的关系

当然,可能会出现更新操作的新版本无法放入旧版本所在页面的情况,这种情况下页面和索引情况的对比如图11所示。

在这里插入图片描述

图11 新版本无法放入旧版本所在页面时的页面和索引情况对比

此种情况下,索引会有两条记录(entry),两条记录代表了 key对应新旧版本的ctid,这样方便从索引直接跨页面进行搜索。

(四)基于 CSN的 MVCC机制

openGauss采用行级MVCC机制,历史版本集中存储,垃圾清理代价低。每个事务有一个单独的事务状态存储区域,记录了该事务的状态信息和 CSN。CSN 在openGauss内部使用一个全局自增的长整数作为逻辑的时间戳,模拟数据库内部的时序。

例如,如图12所示,图中每个非只读事务在运行过程中会取得一个 xid(事务号),在事务提交时会推进 CSN,同时会将当前 CSN 与事务的xid映射关系保存起来。

在这里插入图片描述

图12 CSN-xid映射

因此当一个事务拿到的快照为 CSN=3时,事务 TX2、TX4、TX6、TX7、TX8的CSN 分别为4、6、5、7、8,对于该事务的快照而言,这几个事务的修改都不可见。

MVCC解决的是读写并发冲突问题。更新数据的时候,原地更新,把老版本放到历史版本区页面里,同时维护新版本元组到老元组的指针。读元组的时候,根据快照Snapshot.CSN 来判断应该读到哪个版本。

数据库在执行SQL的时候,首先会获取一个快照时间戳Snapshot,当扫描数据页面的时候,根据Snapshot.CSN 和事务状态来判断哪个元组版本可见或者都不可见。主要分以下3种场景:

  • 元组的事务状态区中是回滚状态或者运行中状态,不可见。
  • 元组的事务状态区中是提交状态,如果 Snapshot.CSN 比事务区里的CSN小,当前元组不可见,读取前一个版本继续比较 CSN。反之可见。
  • 元组的事务状态区中是待提交状态,需要等待提交。CSN 本身与xid也会留存一个映射关系,以便将事务本身及其对应的可见性进行关联,这个映射关系会留存在 CSN Log中,如图13所示。
    在这里插入图片描述
图13 CSN Log中映射关系

此映射机制类似于 Clog本身,只不过不同的是,Clog记录的是事务ID 的相关运行状态(运行中/提交/回滚),如图14所示。
在这里插入图片描述

图14 Clog记录的事务ID的相关运行状态

进一步结合前面讲过的行头的结构(其中的 xmin、xmax)、Clog以及上述 CSNLog的映射机制,MVCC的大致判断流程如图15所示。
在这里插入图片描述

图15 MVCC判断流程

简单地总结如下:

  • 如果当前事务ID小于一行的xmin,那么就需要检索xmin对应的 Clog,读取此事务状态,以此来判断此行数据是否对当前事务可见。
  • 如果当前事务ID 大于一行中的 xmax,那么说明此行数据的更新/删除发生于本事务开始之前,此行数据对本事务一定不可见(但不排除此行数据的新版本对本事务可见,因为新旧版本是单独进行判断的)。
  • 如果xid落在了xmin、xmax中间,就需要依据 CSN 来判断本事务的快照下对应数据是否应该被看到,需要检索 CSN Log来进行对比判断。

(五)行存储的空间回收

通过上面所介绍的行存储的多版本并发控制机制,可以发现由于更新和删除并不实际在页面中删除页面本身,数据库长时间运行后,会有大量的历史版本残存在存储空间中,造成了空间的膨胀。为了解决这一问题,存储引擎内部需要定期对历史数据进行清理,以保证数据库的健康运行。

行存储对于存储空间的清理存在于多个层面,有多种方式。其中在页面一级的机制,称为heap_page_prune。顾名思义,就是在页面内部进行空间的清理。这种清理模式能够比较好地解决更新多版本带来的同一个数据记录关联的长长的历史版本堆叠、标记删除的记录以及无效的记录。这种pruning(空间回收)的手段在对页面进行读取的过程中由页面的空闲空间阈值触发,仅改动 heap页面本身,不对索引页面进行改动。因此heap_page_prune是一种较为轻量化的清理方式。举例如下:

如有一个记录a,被前后更新,导致同时有6个历史版本保存于两个不同的页面中,如图16所示。
在这里插入图片描述

图16 记录a的6个历史版本

页面级别的自我清理效果为图17所示。

在这里插入图片描述

图17 页面级别的自我清理

可以看到,清理过程中分别对页面1和页面2中的内容进行了回收,但是由于之前的跨页面导致的两个索引记录指向不同页面,却被保留了下来。

在页面级别的清理之外,还有表级别、数据库级别的整体清理,这个机制称之为Vacuum 操作。Vacuum 操作在整个数据库级别进行废旧元组的清理,同时也会清理索引。Vacuum 操作可以由数据库用户对数据库或数据库内对象主动调起,同时数据库后台也会有工作线程在满足阈值时或者定期进行数据库自动的 Vacuum 操作,如图18所示。
在这里插入图片描述

图18 Vacuum 操作

Vacuum 自身除了清理空间外,也顺带承担了更新统计信息的功能,以便优化器能更准确地进行代价估算。

在 Vacuum 操作过程中,还会对整个数据库级别都可见的元组进行freeze操作。举例来说,当一个元组被插入并提交,而后续没有更新操作,数据库系统上也不再有早于这个提交的事务时间点,需要对这条元组做可见性判断的事务,此时认为此元组就可以被任何人看见了,那么其相关的事务ID 就可以被转化为一个特殊的事务ID——Freeze xid,以表示这种状态。当 Vacuum 操作清理整个系统时,系统中最小活跃事务之前的提交日志(Clog),也同上面说到的,不再被需要,因此 Vacuum 操作也会对这部分提交日志进行清理和回收。

当然,Vacuum 操作本身是一个相对高成本的操作,因此,每个表文件会有一个对应的可见性映射(visibility map),来记录这个表数据文件中对应的页面是否已经处于全部可见状态,这种情况下 Vacuum 操作在执行过程中就可以跳过这部分页面,节省开销。由于一般系统中存储的绝大部分数据都不与当前活跃事务相关,因此此优化可以大大提升 Vacuum 操作的效率。

(六) 行存储的共享缓存管理

前面提到,行存储是一个基于磁盘的存储引擎。为了避免磁盘的IO 的高昂开销,存储引擎会缓存一部分页面在内存中,便于随时对其进行检索和更改。存储引擎会对缓存的页面进行筛选、替换和淘汰,保证留存在缓存的页面能够提高整个引擎的执行效率。

存储中也有着种类较多的缓存,除去正常数据页面的缓存之外,还存在用于缓存各类表的元信息的数据表缓存(relationcache),以及用于加速数据库系统信息以及系统表操作的系统表缓存(catalogcache)。这些种类的缓存都以页面的形式归共享缓冲区结构管理。

共享缓冲区由大量的页面槽位构成,槽位本身有对应的描述结构体,以及用于管理处于这个操作的并发操作的页面级别锁,并配有一个空闲链表来进行空闲空间管理,如图19所示。
在这里插入图片描述

图19 共享缓冲区

存储引擎中操作对事务的读写请求,都会先传递至共享缓冲区。对一个页面的请求会先在缓冲区内进行搜索,如果未命中,则获取一个空的槽位(可能需要淘汰掉已经在缓冲区中不常用的页面),再与文件系统进行交互将所需页面读到槽位中,加锁并使用。根据业务的特征和负载及共享缓冲区的大小,已经在缓冲区内的数据页面会被反复命中,避免了与磁盘的IO 开销,从而加速整个事务处理流程。

对页面的更改也会放在缓存中并被标为脏页面。此时后台写线程(background图19 共享缓冲区writer)会定期对脏页面进行清理和刷盘操作,把空间返还给缓冲区。另一方面,检查点操作在进行时也会将所有的页面刷盘,确保数据的持久化。这里需要注意的一个概念是,当一个事务提交后,这个事务执行过程中更改的页面并不一定被刷盘至磁盘,事务本身的持久化机制实际上是由事务强制刷盘的 WAL,也就是xlog来保证的。在检查点操作后,因为相关页面都已经持久化至磁盘,因此检查点操作时间点之前的xlog, 就可以被回收了。这个机制会在后续的章节继续展开。

共享缓冲区实际上是内存与持久化存储中协调管理调度的核心机制,对数据库管理系统的效率有着很大的影响。为了进一步提升缓冲区中页面的命中率,一些可能会影响缓冲区内页面与业务关联性的操作,都会使用一个专门单独开辟的缓冲区,即环状缓冲区(ringbuffer)。批量的读/写及 Vacuum 页面清理,都属于这类操作。

(七)并行日志系统设计

数据库的日志系统非常关键,它是数据持久化的关键保证。传统数据库一般都采用串行刷日志的设计,因为日志有顺序依赖关系。例如:一个由事务产生的 Redo/Undo日志是有前后依赖关系的。openGauss的日志系统采用多个Log Writer(日志写盘)线程并行写的机制,充分发挥SSD的多通道IO 能力,如图20所示。
在这里插入图片描述

图20 并行刷日志示意图

关键设计如下:

  • 整个事务的 WAL日志不能拆分到多个事务日志共享缓冲区,必须写到一个事务日志共享缓冲区。
  • 故障恢复 WAL,并行恢复,必须按照 LSN(日志序列号)大小顺序恢复。
  • 每个事务结束前需要保证对应的事务日志 LSN 已经刷盘完成。
  • 事务分配事务日志共享缓冲区考虑 NUMA 架构适配。

(八)持久化及故障恢复系统设计

数据库的日志系统非常关键,它是数据持久化的关键保证。基于事务ID 的多版本管理及历史版本的累积及清理方式,行存储引擎主要以 Redo日志(也就是上文提到的 XLog)作为主要的持久化手段,配以增量的检查点及日志的并行回放,支持数据库实例的快速故障恢复。

1.事务的Redo日志机制

Redo日志在事务对数据进行修改时产生,用来记录事务修改后的数据或是事务对数据做 的 具 体 操 作。比 如,简 单 的INSERT/UPDATE/DELETE 操 作 会 产 生 如图21所示的 Redo日志。
在这里插入图片描述

图21 Redo日志

一些非事务直接修改的关键操作也会记录到 Redo日志,比如新页面的申请、显式的事务提交、检查点等。记录 Redo日志的原则,就是在数据库发生故障后,可以从最后一个检查点开始,通过 Redo日志的回放,恢复到与数据库实例发生故障前的状态一致。

Redo日志除了应用于数据恢复、数据备份与还原以及数据库主备实例之间的主备同步、不同数据库实例/集群间的同步都需要依赖 Redo日志的机制。为了保障数据的一致性,在事务修改的相关页面刷盘之前,需要先把对应的 Redo日志刷盘,也就是遵循 WAL的原则。

因为事务的提交以及操作之间的顺序对于数据一致性是至关重要的,因此 Redo日志也必须将此顺序记录下来。每条 Redo日志都配有一个日志序列号,即 Log Sequence Number(LSN)。在行存储的系统中,LSN 为一个递增的64位无符号整数。系统中各类机制,如检查点及主备实例之间的同步机制、仲裁机制,都需要依靠系统中推进的LSN 或是恢复出来的 LSN 作为重要的标记或判断依据。

2.全量与增量检查点

在上述对事务日志以及共享缓冲区的描述中,有一个关键的信息,那就是事务日志的持久化与事务提交是同步的,但事务内对页面相关修改的持久化与事务提交不是同步的;也就是说,事务提交需要与这个事务相关的 Redo日志被强制刷盘,但是并不强制要求相关的页面也被强制刷盘。当一个数据库实例故障重启后,实例在启动过程中,之前没有能够及时刷盘的改动需要使用事务日志进行恢复。但是日志回放的代价是很高的,性能也相对比较慢。为了避免每次数据库都需要从头恢复事务日志,数据库自身会定期创建检查点,用户也可以通过命令手动创建检查点。

创建检查点的过程中,存储引擎会将数据缓冲区中脏页写到磁盘中,并记录日志文件和控制文件。记录信息中的recLSN 代表着此次检查点中,在此 LSN 之前的日志对应的所有改动均已被持久化,下次的数据恢复可以直接从此 LSN 开始;同时在此LSN 之前的事务日志,在其他用途(主备实例同步、数据备份等)时,也可以被回收重新使用。

由于检查点本身需要将缓冲区内所有的脏页面刷盘(全量检查点),因此每次检查点从性能角度会对数据库实例所在物理环境引入大量的IO,磁盘的峰值往往意味着性能的波动。同时因为存在大量的IO 开销,因此检查点的打点不能过于频繁,recLSN推进较慢,那么重启数据库时也就会存在较多的 Redo日志需要回放,存在重启恢复时间过长的问题。为了解决这一问题,行存储引擎引入了增量检查点的概念。

在增量检查点机制下,会维护一个脏页面队列(dirtypagequeue)。脏页是按照LSN 递增的顺序放到队列中的,定期由一个专门刷脏页面的后台线程页面刷盘线程(pagewriter)进行定期定量的刷脏页下盘操作,脏页面队列如图22所示。

在这里插入图片描述

图22 脏页面队列

队列中维护一个recLSN,记录目前已经被刷盘的脏页对应的 LSN 大小,即在队列中脏页对应的事务提交、其相对的事务日志下盘后,此recLSN 标记会被更新。在触发增量检查点时,并不需要等待脏页刷盘,而是可以使用当前脏页队列的recLSN 作为检查点的recLSN 记录。增量检查点的存在使得整个系统中的IO 更加平滑,并且系统的故障恢复时间更短,可用性更高。

3.并行回放

Redo日志的回放指的是将 Redo日志中记录的改动重新应用到系统/页面中的过程,这个过程通常发生在实例故障恢复抑或是主备实例之间的数据同步过程中的备机实例上(即主实例的改动,备机实例也需要回放完成,以达到与主实例状态一致的效果)。当前数据库所在物理实例往往有较多的 CPU 核,而日志回放却往往还是单线程进行运作,在日志回放的过程中数据库实例无法充分利用物理环境资源。

为了能够充分利用 CPU 多核的特点,显著加快数据库异常后恢复及备机实例日志回放的速度,行存储引擎采用了多线程并行方式回放日志,如图23所示。
在这里插入图片描述

图23 多线程并行方式回放日志

整个并行回放系统的设计采用生产者-消费者模型,分配模块负责解析、分配日志到回放模块,回放模块负责消费、回放日志。

为了 达 成 这 一 设 计,实 现 中 采 用 了 带 阻 塞 功 能 的 无 锁 SPSC(Single Producer Single Consumer)队列。分配线程作为生产者将解析后的日志放入回放线程的列队中,回放线程从队列中消费日志进行回放。无锁SPSC队列如图24所示。
在这里插入图片描述

图24 无锁SPSC队列

为了提升整体并行回放机制的可靠性,会在对一个页面的回放动作中,对事务日志中的 LSN 和页面结构中的last_LSN[详见前面章节中描述的 HeapPageHeader(堆页面头)结构体]进行校验,以保证回放过程中数据库系统的一致性。
在这里插入图片描述

Gauss松鼠会是汇集数据库爱好者和关注者的大本营,
大家共同学习、探索、分享数据库前沿知识和技术,
互助解决问题,共建数据库技术交流圈。
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐