加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

常见关系库MVCC浅析2

(2022-04-16 01:22:03)
标签:

mvcc

分类: 工作学习
本文继续论述数据库的MVCC实现,主要包括PolarDB-X、TIDB、OceanBase、TDSQL。

PolarDB-X

常见关系库MVCC浅析2

 PolarDB-x采用的是TSOTimeStamp Oracle)的分布式事务系统,TSO分配递增的逻辑时钟事务SCN号,数据节点的MVCC数据行可见性判定也是基于该SCN号。如下图所示,PolarDB-x的数据节点没有使用原生的InnoDB MVCC实现,而是使用一个叫Lizard SCN的事务MVCC实现。具体而言,在原InnoDB数据行系统隐藏列的基础上,新增2个隐藏列SCNUBASCN列存储改行的事务提交SCN号,UBA列存储该行相关事务对应的系统事务表对应的slot。系统事务表是一个事务提交与否最准确的判定标准,数据行中的SCN字段通常不在事务提交的时候进行填充,大部分的行记录 SCN 回填,都是 Delayed Record Cleanout,即在读取行记录上 SCN 时,发现是 NULL 值,就根据 UBA 地址查询 transaction slot 找到需要回填的 SCN,并做压力和负载的自动化优化。这块的设计类似OracleMVCC实现,不同的是OracleMVCC是块级别的,PolarDB-x是行级别的。

    PolarDB-x的事务快照就是当前事务获取的读SCN号,可见性判定就是比对行SCN号与事务读SCN号,如果行SCN<=SCN,那么可见,否则基于innodb的回滚段找历史版本。

PolarDB-x引入Lizard SCN事务的主要原因是2个:

1. 在单机多核的场景下,MySQL InnoDB 的事务系统作为一个内存结构,存在严重的 Write Transaction 和 Read Query 的干扰,无法高效使用多核能力,Lizard SCN 单机事务系统,解绑读写之间对事务系统的争抢,能够在OLTP Read-Write 的真实业务场景满载多核能力。

2. 在多节点集群的场景下,由于数据分片,一个业务场景会有多个节点参与,单机事务无法保证多个节点参与的事务 Atomicity 以及查询的一致性,Lizard SCN 分布式事务系统,提供了一整套的解决方案来满足。



TIDB

不是类似mysql那样基于回滚段,而是在tikv层把所有历史版本数据一起存储,key是原本key+mvcc版本号。版本高的在前面。如下是官网的描述。

常见关系库MVCC浅析2

这里的Version存储的是什么呢?是事务的编号或时间戳,由TIDBPD模块分配。TIDB的事务模型是基于googlepercolaterPD模块作为TSO为事务分配时间戳。TIDB事务的start_ts是事务begin开始的时候申请的,而commit_ts是在事务提交阶段申请的。和percolater一样,数据行中存在2个系统列:lockwrite

事务操作的第一行数据为primary key数据行。事务其他行的Lock列存储事务的start_ts和指向primary key数据行的信息。当事务提交的时候,会清理lock列信息,并将commit_ts信息存储到write列中。primary key行的lock清理是事务提交时做完的,而其他非primary key列的lock信息则是异步后台清理的。

 

Percolater的说明可以参考https://tikv.org/deep-dive/distributed-transaction/percolator/。如下是主要实现的一个说明:


常见关系库MVCC浅析2

 

如下是TIDB中数据行存储信息的keyvalue的示例,使用了3rocksdb column family(CF)

常见关系库MVCC浅析2


所以从write CF是可以查到数据行对应事务提交版本号commit_ts。对于MVCC读而言,一个事务的读快照(事务的启动版本号start_ts)版本号>=对应行的事务提交版本号commit_ts,即这个行可读。否则看它的历史版本。由于TIDB的数据组织是高版本放前面,低版本放后面,所以可以方便地查找历史版本。

所以MVCC查询的一个基本流程就是:

1. 事务begin,PD获取start_ts 记为read_view_ts

2. 读取key基于keylock CF中查找是否有锁信息并结合primay key还有write cf信息,判断当前行是否可读。因为lock CF是异步清理的,所以会需要通过primary keywrite cf来确认是否事务已经提交。

 

除了最新行外,其他的行就可以通过遍历write CFkeykey中包含了commit_ts),来确认一个可读的版本,即第一个write CF commit_ts<</font>事务read_view_ts的行。Write CF信息为${key}_${commit_ts}-->${start_ts} 其中key包含commit_ts的,而tidb的数据组织是高版本放前面低版本放后面,所以找到一个合适的commit_ts其实就是对write CF对应key的一个顺序遍历。基于找到的commit_ts,就可以获取对应可见数据行事务的start_ts,而后基于default CF存储的信息${key}_${start_ts} --> ${value} 就可以通过keystart_ts找到当前事务可以读到的数据行版本。



OceanBase

OceanBase采用基线数据+内存增量数据的分层 LSMTree 结构。数据分为两部分:基线数据和增量数据。基线数据是持久到磁盘上的数据,一旦生成就不会再修改,称之为 SSTable。增量数据存在于内存,用户写入都是先写到增量数据,称之为 MemTable,通过 Redo Log 来保证事务性。

所有的事务操作产生的增量数据在内存中据以稀疏格式存储,仅仅保存被修改的列的值(或修改操作如“+1”),如下是一个针对一行数据的多个事务历史版本的存储示例:


常见关系库MVCC浅析2

每次事务对行数据的更新记录都保存在一个变长的内存块中,多次事务保存的内存块按时间顺序串成链表,读取到这一行的时候,从最早的数据块开始遍历,将对同一列的更新记录合并,然后应用到基线数据上并生成最终数据.随着对同一行多次执行更新,上述链表会变得越来越长,读取时合并计算的代价也会越来越大,因此在链表超过配置的长度后,多个块将被合并为一个块(称为 RowCompaction),减少读取时遍历链表合并的

代价

OceanBase单节点内对于事务数据的维护以类似内存数据库的方式进行维护。这也是OB官方一直宣传的准内存数据库的特性。

接下来我们从如下几个方面来论述下OB事务模型的MVCC实现:

1. 事务版本号

2. 事务当前行与历史版本的存储

3. MVCC的读实现

 

事务版本号/时间戳(可能不是最新OB的实现)

首先事务版本,与OraclePGMySQLTIDBPolarDB都不同,OB虽然也在事务开始的时候获取了一个读快照时间戳,但OB的数据行存储没有存储这个时间戳,OB数据行的提交时间戳(提交版本号)仅在事务提交的时候获取并持久化到数据行中。

OB在每个分区的全局时间戳缓存服务(global timestamp cache)维护一个“最大提交事务时间戳”和一个“本地最大读时间戳”。

OB事务开始的时候从全局时间戳缓存服务获取事务读时间戳,这个值应该最终来自GTS。当一个读请求访问一个分区时,对应事务的读时间戳如果更大,则更新本分区的“本地最大读时间戳”。

OB事务提交的提交过程采用两阶段提交,先preparecommitPrepare过程会基于该分区的本地最大读时间戳和从GTS获取的时间戳的最大值为本地提交版本号,即max(本地最大读时间戳,GTS)Commit的时候,事务的 全局提交版本号(global commit version,即 commit version)是由所有分区本地提交版本号的最大值决定的,即每个分区prepare时获取的本地提交版本号中最大的。该事务 全局提交版本号即为这个事务的提交版本号。

OB每个分区的本地内存中都有一个“事务表”,用于存储该分区中事务的信息。事务表中事务的状态包括runningpreparecommit。除了状态,同时存储事务id和版本时间戳。提交时,将事务的全局提交版本号更新到事务表中,而后异步回填数据行上的提交版本信息。

 

事务commit阶段的最后,事务更新分区维护的“最大提交事务时间戳”。


常见关系库MVCC浅析2

对于读请求来说,在读取的时候,会使用读版本号来读取对应的数据,在真正读取时会用读版本号先更新本地 最大读时间戳。当读取到 RUNNING 状态的事务时,由于推高了 本地最大读时间戳,因此之后 RUNNING 状态的事务一定会以更大的 本地时间戳 来进入两阶段提交,根据保证和快照读的概念,可以安全地跳过这个数据。当读取到 PREPARE 状态的事务时,由于给予的保证,对于 本地时间戳 大于读时间戳的事务,和之前的分析一样,可以保证不用读到。但是若 本地时间戳 小于读时间戳,那么无法确认这个时间戳最后的 全局提交时间戳 和读时间戳的关系,因此解决方案是,优雅地等在这行的事务上(内部称为 lock for read,两阶段提交在 OceanBase 数据库的假设中应该会是很快完成的过程。

 

分区维护的“最大提交事务时间戳”的作用为:当一条语句可以明确其查询所在机器时,如果是一台机器,则直接使用该机器的 Global Committed Version 作为 Read Version,降低对于全局时间戳的请求压力。这感觉是对单语句单分片事务的优化。

 

事务当前行与历史版本的存储

OB内存中每个数据行有个行头结构RowValue,包含了几个关键的指针:

1. Undo list head: 指向历史RowCompaction时对应的数据块链表,该链表使用undo list node来连接下一次rowcompaction所涉及的事务版本。有点innodb回滚段指针的作用。

 

2. Data list head 和 data list tail 指向最近一次指向最近一次执行rowcompaction之后的数据库链表。

常见关系库MVCC浅析2

MVCC的读实现

 

 

全局维护一个publish_trans_id,表示最后一个成功提交的事务ID。每个事务开始时,获取publish_trans_id,相当于一个快照点,从而这个事务只能读取在这个事务之前开始的事务提交的修改,在这个事务开始之后开始的事务提交的修改时读不到的。读事务拿着事务开始时获取到的publish_trans_id,找到相应的行后,顺着链表读(先Data list head找,是否有所需版本,如果没有,再从Undo list headrow compaction之前的版本,只能读取TransID小于等于publish_trans_id的增量修改。每次刷redo log到磁盘后,会将这批redo log对应的最新的事务ID更新到publish_trans_id



TDSQL


TDSQL分布式MySQL版的MVCC方案在MySQLMVCC基础上加上一个全局GTS模块(叫MC),其实和polardb-x类型,也是一个TSO的方案。MC模块负责的是全局递增时间戳的生成。

 

如下是事务执行过程中与MC的交互,分别为事务的第一条SQL语句时获取读快照时间戳,事务提交的prepare完成后获取提交时间戳。

常见关系库MVCC浅析2

 

        不同于polardb-xtdsql并没有将事务时间戳直接存储到innodb的数据行,而是额外记录一个t_log系统表,来维护全局提交时间戳与mysql事务id的映射关系。对t_log进行GTS的映射访问是纯内存的,即GTS修改直接在内存中操作,Tlog在加载以及扩展都是映射到Innodb的缓冲池中。对于映射关系的修改,往往是事务提交的时候,此时直接在内存中修改映射关系,内存中Tlog关联的数据页变为脏页,同时在redo日志里增加对GTS的映射操作,定期通过刷脏来维护磁盘和内存中映射关系的一致性。由于内存修改的开销较小,而在redo中也仅仅增加几十字节,所以整体的写开销可以忽略不计。


常见关系库MVCC浅析2


 

对于MVCC的读逻辑,基于数据行对应的GTS时间戳来判断是否可见,如下是3类场景的总结:

常见关系库MVCC浅析2

 

可以看到如果查询的行属于prepare状态,还未分配时间戳,那么就必须等待,直到分配时间戳,来判定可见还是不可见,因为做prepare的事务从GTS获取的时间戳可能小于读事务分配的时间戳,这种情况下这个数据应该是可见的。如下是一个查询示例:

常见关系库MVCC浅析2

 

       TDSQLTIDB还有PolarDB-XMVCC和分布式事务GTS模式上的另一个主要差异是,TDSQL分布式事务查询强一致是可选功能,MC组件可以开启也可以不开启。官方给的性能影响是:对于写事务的影响不到3%,而对读事务的影响能够控制在10%以内。此外,还需要对undo页清理机制做改造,将原有的基于最老可见性视图的删除方式改为以最小活跃GTS的方式删除。



总结


产品

全局事务组件

/块级mvcc

历史数据存储方式

事务创建时分配id并存储

事务提交时获取scn并存储

事务提交后是否需要回填

是否有事务表要存储

MVCC是否可选

可见性判定简述

Oracle

回滚段

分配,但与数据行存储不直接相关,会被新事务覆盖

分配,但与数据行存储不直接相关,会被新事务覆盖

是,如果是快速提交和异步提交时

是,在回滚段头的事务表

比较块的scn,如果不可见,通过ITL找块的历史版本undo信息,直到可见,基于一路的undo信息还原出CR数据块。

PG

数据行

分配,随数据行存储

分配,随数据行存储

是,回填更新t_infomask

是,有pg_clog

判定一行记录的xminxmax对应的事务是否对当前事务快照是否已完成来判定一个数据行是否可见。如果xmin已经完成,但xmax还未完成,那么这个行就可见。

InnoDB

回滚段

分配,随数据行存储

不分配

基于活跃事务列表快照,查看已经提交数据,当前行不可见就通过回滚段找历史版本。

PolarDB-X

TSO

回滚段

分配,随数据行存储

分配,存储于事务表和数据行(依赖回填)

是,回填更新行scn

是,类似oracle存储于回滚段

比较行scn,不可见通过回滚段找历史版本

TIDB

TSO

数据行

分配,在不同的CF中作为key的一部分或value

分配,作为write CFkey的一部分

是,回填更新lock CF

基于write CF找小于读时间戳最大的commit_ts,然后找对应数据行

OB

TSO

行变更

Compation+增量变更链表

不分配

分配,并随行事务块一起存储

是,回填将行的本地提交时间戳更新为行的全局提交时间戳。

找到小于读时间戳的最大提交事务版本,如果对应版本已被compaction,找历史compaction前的版本,而后合并所找到版本的compaction和相关增量变更事务信息,配合基线数据提供数据行。

TDSQL

GTS

回滚段

分配,随数据行存储

分配,存储于系统clog

是,有clog系统表

基于clog找到数据行对应的提交时间戳,基于提交时间戳判定当前行是否可见,不可见通过回滚段找历史版本。



参考

常见关系库MVCC浅析2




转载请注明转自高孝鑫的博客!

0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有