常见关系库MVCC浅析2
标签:
mvcc |
分类: 工作学习 |
PolarDB-x引入Lizard SCN事务的主要原因是2个:
1.
2.
不是类似mysql那样基于回滚段,而是在tikv层把所有历史版本数据一起存储,key是原本key+mvcc版本号。版本高的在前面。如下是官网的描述。
这里的Version存储的是什么呢?是事务的编号或时间戳,由TIDB的PD模块分配。TIDB的事务模型是基于google的percolater,PD模块作为TSO为事务分配时间戳。TIDB事务的start_ts是事务begin开始的时候申请的,而commit_ts是在事务提交阶段申请的。和percolater一样,数据行中存在2个系统列:lock和write。
事务操作的第一行数据为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/。如下是主要实现的一个说明:
如下是TIDB中数据行存储信息的key与value的示例,使用了3个rocksdb column family(CF):
所以从write CF是可以查到数据行对应事务提交版本号commit_ts。对于MVCC读而言,一个事务的读快照(事务的启动版本号start_ts)版本号>=对应行的事务提交版本号commit_ts,即这个行可读。否则看它的历史版本。由于TIDB的数据组织是高版本放前面,低版本放后面,所以可以方便地查找历史版本。
所以MVCC查询的一个基本流程就是:
1.
2.
除了最新行外,其他的行就可以通过遍历write CF的key(key中包含了commit_ts),来确认一个可读的版本,即第一个write CF
commit_ts<</font>事务read_view_ts的行。Write CF信息为${key}_${commit_ts}-->${start_ts},
OceanBase采用基线数据+内存增量数据的分层
所有的事务操作产生的增量数据在内存中据以稀疏格式存储,仅仅保存被修改的列的值(或修改操作如“+1”),如下是一个针对一行数据的多个事务历史版本的存储示例:
每次事务对行数据的更新记录都保存在一个变长的内存块中,多次事务保存的内存块按时间顺序串成链表,读取到这一行的时候,从最早的数据块开始遍历,将对同一列的更新记录合并,然后应用到基线数据上并生成最终数据.随着对同一行多次执行更新,上述链表会变得越来越长,读取时合并计算的代价也会越来越大,因此在链表超过配置的长度后,多个块将被合并为一个块(称为
代价。
OceanBase单节点内对于事务数据的维护以类似内存数据库的方式进行维护。这也是OB官方一直宣传的准内存数据库的特性。
接下来我们从如下几个方面来论述下OB事务模型的MVCC实现:
1.
2.
3.
事务版本号/时间戳(可能不是最新OB的实现)
首先事务版本,与Oracle、PG、MySQL、TIDB、PolarDB都不同,OB虽然也在事务开始的时候获取了一个读快照时间戳,但OB的数据行存储没有存储这个时间戳,OB数据行的提交时间戳(提交版本号)仅在事务提交的时候获取并持久化到数据行中。
OB在每个分区的全局时间戳缓存服务(global timestamp cache)维护一个“最大提交事务时间戳”和一个“本地最大读时间戳”。
OB事务开始的时候从全局时间戳缓存服务获取事务读时间戳,这个值应该最终来自GTS。当一个读请求访问一个分区时,对应事务的读时间戳如果更大,则更新本分区的“本地最大读时间戳”。
OB事务提交的提交过程采用两阶段提交,先prepare后commit。Prepare过程会基于该分区的本地最大读时间戳和从GTS获取的时间戳的最大值为本地提交版本号,即max(本地最大读时间戳,GTS)。Commit的时候,事务的
全局提交版本号(global commit
version,即
OB每个分区的本地内存中都有一个“事务表”,用于存储该分区中事务的信息。事务表中事务的状态包括running、prepare和commit。除了状态,同时存储事务id和版本时间戳。提交时,将事务的全局提交版本号更新到事务表中,而后异步回填数据行上的提交版本信息。
事务commit阶段的最后,事务更新分区维护的“最大提交事务时间戳”。
对于读请求来说,在读取的时候,会使用读版本号来读取对应的数据,在真正读取时会用读版本号先更新本地
分区维护的“最大提交事务时间戳”的作用为:当一条语句可以明确其查询所在机器时,如果是一台机器,则直接使用该机器的
事务当前行与历史版本的存储
OB内存中每个数据行有个行头结构RowValue,包含了几个关键的指针:
1.
2.
MVCC的读实现
全局维护一个publish_trans_id,表示最后一个成功提交的事务ID。每个事务开始时,获取publish_trans_id,相当于一个快照点,从而这个事务只能读取在这个事务之前开始的事务提交的修改,在这个事务开始之后开始的事务提交的修改时读不到的。读事务拿着事务开始时获取到的publish_trans_id,找到相应的行后,顺着链表读(先Data list head找,是否有所需版本,如果没有,再从Undo list head找row compaction之前的版本),只能读取TransID小于等于publish_trans_id的增量修改。每次刷redo log到磁盘后,会将这批redo log对应的最新的事务ID更新到publish_trans_id。
TDSQL
TDSQL分布式MySQL版的MVCC方案在MySQL的MVCC基础上加上一个全局GTS模块(叫MC),其实和polardb-x类型,也是一个TSO的方案。MC模块负责的是全局递增时间戳的生成。
如下是事务执行过程中与MC的交互,分别为事务的第一条SQL语句时获取读快照时间戳,事务提交的prepare完成后获取提交时间戳。
对于MVCC的读逻辑,基于数据行对应的GTS时间戳来判断是否可见,如下是3类场景的总结:
可以看到如果查询的行属于prepare状态,还未分配时间戳,那么就必须等待,直到分配时间戳,来判定可见还是不可见,因为做prepare的事务从GTS获取的时间戳可能小于读事务分配的时间戳,这种情况下这个数据应该是可见的。如下是一个查询示例:
总结
|
产品 |
全局事务组件 |
行/块级mvcc |
历史数据存储方式 |
事务创建时分配id并存储 |
事务提交时获取scn并存储 |
事务提交后是否需要回填 |
是否有事务表要存储 |
MVCC是否可选 |
可见性判定简述 |
|
Oracle |
否 |
块 |
回滚段 |
分配,但与数据行存储不直接相关,会被新事务覆盖 |
分配,但与数据行存储不直接相关,会被新事务覆盖 |
是,如果是快速提交和异步提交时 |
是,在回滚段头的事务表 |
否 |
比较块的scn,如果不可见,通过ITL找块的历史版本undo信息,直到可见,基于一路的undo信息还原出CR数据块。 |
|
PG |
否 |
行 |
数据行 |
分配,随数据行存储 |
分配,随数据行存储 |
是,回填更新t_infomask |
是,有pg_clog |
否 |
判定一行记录的xmin和xmax对应的事务是否对当前事务快照是否已完成来判定一个数据行是否可见。如果xmin已经完成,但xmax还未完成,那么这个行就可见。 |
|
InnoDB |
否 |
行 |
回滚段 |
分配,随数据行存储 |
不分配 |
否 |
否 |
否 |
基于活跃事务列表快照,查看已经提交数据,当前行不可见就通过回滚段找历史版本。 |
|
PolarDB-X |
TSO |
行 |
回滚段 |
分配,随数据行存储 |
分配,存储于事务表和数据行(依赖回填) |
是,回填更新行scn |
是,类似oracle存储于回滚段 |
否 |
比较行scn,不可见通过回滚段找历史版本 |
|
TIDB |
TSO |
行 |
数据行 |
分配,在不同的CF中作为key的一部分或value |
分配,作为write CF的key的一部分 |
是,回填更新lock CF |
否 |
否 |
基于write CF找小于读时间戳最大的commit_ts,然后找对应数据行 |
|
OB |
TSO |
行变更 |
Compation+增量变更链表 |
不分配 |
分配,并随行事务块一起存储 |
是,回填将行的本地提交时间戳更新为行的全局提交时间戳。 |
否 |
否 |
找到小于读时间戳的最大提交事务版本,如果对应版本已被compaction,找历史compaction前的版本,而后合并所找到版本的compaction和相关增量变更事务信息,配合基线数据提供数据行。 |
|
TDSQL |
GTS |
行 |
回滚段 |
分配,随数据行存储 |
分配,存储于系统clog表 |
否 |
是,有clog系统表 |
是 |
基于clog找到数据行对应的提交时间戳,基于提交时间戳判定当前行是否可见,不可见通过回滚段找历史版本。 |

加载中…