MongoDB 事务,复制和分片的关系-3

举报
geminidb_fans 发表于 2020/06/30 20:10:28 2020/06/30
【摘要】 8.MongoDB rocks 4.0是如何支持时间戳的本章节内容介绍如何改造RocksDB来支持基于时间戳的事务。主要的接口UserKey与InternalKey为了在RocksDB中支持时间戳,通过不同的时间戳表示在数据库中数据的不同版本。在原InternalKey的基础上增加了一个新的内容CommitTimestamp,加上原来的UserKey,LSN和数据的类型三部分内容,共四部分,...

8.MongoDB rocks 4.0是如何支持时间戳的




本章节内容介绍如何改造RocksDB来支持基于时间戳的事务。

主要的接口
  • UserKey与InternalKey

    为了在RocksDB中支持时间戳,通过不同的时间戳表示在数据库中数据的不同版本。在原InternalKey的基础上增加了一个新的内容CommitTimestamp,加上原来的UserKey,LSN和数据的类型三部分内容,共四部分,如下所示。

UserKey + CommitTimestamp(8byte) + LSN + Type
  • 接口定义

    用于控制事务的时间戳,ToTransactionDB提供了两个主要的时间戳相关的接口与之配合,具体如下:

  virtual Status SetReadTimeStamp(const RocksTimeStamp& timestamp) = 0;
 virtual Status SetCommitTimeStamp(const RocksTimeStamp& timestamp) = 0;

SetReadTimeStamp用于设置事务的读时间戳,同一个事务只能设置一次,在分布式事务的场景下,即使在不同分区上执行的查询也需要使用同一个读时间戳,来保证各分区上看到的数据版本的一致性。

SetCommitTimeStamp用于设置事务的提交时间戳,提交时间戳的基本要求是要大于事务的读时间戳。此外,对于同一个事务来说,可以多次调用,并且设置的提交时间戳可以不同。对于调用该接口之后的更新操作都使用此时间戳作为asif_commit_timestamps,每个asif_commit_timestamps时间戳会作为对应更新操作的InternalKey的CommitTimestamp保存到Memtable中。

主要数据结构

RocksDB数据库同样需要维护下面几个数据结构,用来管理事务RocksDB实例级别的全局时间戳信息。

  • uncommitted_keys(未提交事务的UserKey列表)

    uncommitted_keys是保存未提交事务中发生更新的UserKey的容器,在写事务更新数据时,RocksDB将每个更新操作对应的Key插入到队列中,在事务提交时从队列中删除。当不同的事务并发执行更新操作时,则会通过此队列中保存的UserKey信息进行写冲突检测,如果已存在未提交的写事务更新了某个UserKey,则后执行更新操作的事务,会根据fisrt-update-win策略返回失败。

  • committed_keys(已提交事务的UserKeyKey列表)

    committed_keys是保存已提交的UserKey信息的容器。在事务提交后,Key信息从uncommitted_keys队列中清除,插入到此队列中。为了进行写冲突检测维护一个uncommitted_keys队列是显而易见的,但是为什么要维护一个committed_keys队列呢?这里通过一个简单的例子进行说明这个队列的必要性。

    例子:针对同一个Key的两个更新事务,事务A开始时间(也就是ReadTs)为t1,更新操作Put1(A)执行时间为t3,提交时间戳为t5。那么,事务B开始时间(也就是ReadTs)为t2。

    对于场景1,存在更新操作Put2(B)发生在t4,则通过uncommitted_keys则可以发现存在冲突,事务B返回失败。

    image.png

  • 对于场景2,存在操作Put2(B)发生在t6,这个场景刚好对应GSI中提交规则中snapshot(Ti) < commit(Tj) < commit(Ti)描述的场景,此时,事务B需要返回写冲突失败。如果在t6这个时间点,如果没有事务A的相关信息,那么则无法判断事务B是否应该成功提交还是失败。

    image.png


  • read_q(读时间戳队列)

    前面对于committed_keys只描述了插入的规则,但是这个队列什么时候进行清理呢?会不会不断变大?答案是“不会”。从上面的例子可知,事务B只关心CommitTs比ReadTs(B)大的事务,那么,我们就可以根据这一点,制定committed_keys队列的清理规则。数据库要维护一个读时间戳队列read_q,通过它找到最小的未提交事务的ReadTs,CommitTs小于此时间戳的事务,都可以从committed_keys中清理掉,从而解决此问题。

  • oldest_ts(最早可见时间戳)

    上面对于committed_keys的清理机制其实会有一个漏洞,就是如果事务A的信息在清理之后,如果存在一个新事务设置的ReadTs比事务A的CommitTs要早的化,则又会出现违背GSI提交规则的情况。为了解决这个问题,这里增加一个“最早的可见时间戳”oldest_ts,在调用SetReadTimeStamp接口设置ReadTs时,如果设置的时间戳小于oldest_ts,则返回失败。那么committed_keys队列清理的机制则修改为清理掉min(oldest_ts,read_q.begin().ts)之前的所有已提交事务的信息。“最早可见时间戳”的值由Mongo层进行设置,更新oldest_ts时,如果oldest_ts大于read_q.begin().ts,则取自动获取中read_q较小的为提交的ReadTs作为oldest_ts。

    通过增加上面几个接口和数据结构,使用RocksDB具备了基本的支持 基于时间戳事务的能力。

基本操作流程

由于引入了时间戳后,关系到数据可见性,数据清理的流程,都会涉及到对应代码逻辑的调整,在下面几个关键步骤中需要把InternalKey中的Timestamp取出进行逻辑判断。

  • 写操作

    每个写事务开始后,首先要设置ReadTs,然后通过接口TOTransaction::Put接口进行更新操作,在执行时通过对uncommitted_keys和committed_keys的检索,进行写冲突判断。如果无写冲突,则缓存在事务的writebatch中,在事务提交时,通过DBImpl::WriteImpl把每个更新操作的UserKey进行重写,在其中增加CommitTs作为InternalKey保存到Memtable中。

  • 读操作

    对于事务中包含更新操作的读事务,需要先从WriteBatch中write_batch_.GetFromBatchAndDB读最新数据,如果WriteBatch中无数据,则继续从Memtable和SST中继续查询,对于UserKey相同的数据,需要进行时间戳比对,当InternalKey中的时间戳大于ReadTs时,则继续遍历该UserKey的更早版本的数据,直到找到或者结束。

  • Compaction操作

    在Compaction的过程中,旧版本数据的清洗过滤:对于哪些key的数据需要保留,哪些数据需要清除的判断逻辑中,增加了对时间戳因素的处理,需要保证大于oldest_ts的数据不会被清理。


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。