mini-lsm通关笔记Week3Day1

项目地址:https://github.com/skyzh/mini-lsm

个人实现地址:https://gitee.com/cnyuyang/mini-lsm

在本章中,您将:

  • 重构你的实现以使用key+ts(时间戳)表示。
  • 使您的代码使用新的键(key)表示形式进行编译。

要运行测试用例,请执行以下操作:

cargo x copy-test --week 3 --day 1
cargo x scheck

注意:本章没有完整的单元测试。你需要做的就是让你的代码编译通过。

Task 0-Use MVCC Key Encoding

您需要将键(key)编码模块替换为MVCC编码方式。我们从原来的key模块中删除了一些接口,并为keys实现了新的比较器。如果您遵循了前面章节中的说明,并且没有使用into_inner方法,则应在所有重构后的第3天通过所有测试用例。否则,您将需要仔细查看仅比较键而不查看时间戳的地方。

具体而言,键类型定义已从:

pub struct Key<T: AsRef<[u8]>>(T);

...修改为:

pub struct Key<T: AsRef<[u8]>>(T /* user key */, u64 /* timestamp */);

...其中,我们有一个与keys关联的时间戳。我们只在系统内部使用此键(key)表示形式。在用户接口方面,我们不要求用户提供时间戳,因此在引擎中某些结构仍使用&[u8]而不是KeySlice。我们稍后将介绍需要更改函数签名的地方。现在,您只需要运行

cp mini-lsm-mvcc/src/key.rs mini-lsm-starter/src/

还有其他方法可以存储时间戳。例如,我们仍然可以使用pub struct Key<T: AsRef<[u8]>>(T);表示形式,但假设键(key)的最后8个字节是时间戳。您可以将其作为奖励任务的一部分来实现。

Alternative key representation: | user_key (varlen) | ts (8 bytes) | in a single slice
Our key representation: | user_key slice | ts (u64) |

在key+ts编码中,排序逻辑为key较小、时间戳较大的key会排前面。例如

("a", 233) < ("a", 0) < ("b", 233) < ("b", 0)

这里只要执行:

cp mini-lsm-mvcc/src/key.rs mini-lsm-starter/src/

Task 1-Encode Timestamps in Blocks

您将注意到的第一件事是,在替换key模块后,您的代码可能无法编译。在本章中,您需要做的就是使其编译。在此任务中,您将需要修改:

src/block.rs
src/block/builder.rs
src/block/iterator.rs

您将注意到raw_ref()len()从键对象的API中删除。作为替换,我们使用key_ref获取用户键的切片,使用key_len获取用户键的长度。您需要使用新的API重构block构造器和解码实现。此外,您还需要更改块编码以对时间戳进行编码。在BlockBuilder::add中,您应该这样做。新的区块输入记录将如下所示:

key_overlap_len (u16) | remaining_key_len (u16) | key (remaining_key_len) | timestamp (u64)

您可以使用raw_len来估计键所需的空间,并将时间戳存储在用户键之后。

更改块编码后,您需要相应地更改block.rsiterator.rs的解码。

首先可以使用cargo x scheck尝试编译项目,搜索报错信息中的block,将raw_ref()方法替换为key_ref()len()方法替换为key_len()

然后就需要修改编解码方式。

编码(builder.rs),在写入键(key)后写入时间戳:

// Encode key content.
self.data.put(&key.key_ref()[overlap..]);
// 【新增】Encode TS.
self.data.put_u64(key.ts());
// Encode value length.
self.data.put_u16(value.len() as u16);

解码(iterator.rs),在读取键(key)后读取,存在两个地方需要修改:

  1. get_first_key解析第一个键(key)
fn get_first_key(&self) -> KeyVec {
    let mut buf = &self.data[..];
    buf.get_u16();
    let key_len = buf.get_u16();
    // 读取key
    let key = &buf[..key_len as usize];

    // 【新增】推进buf的下标
    buf.advance(key_len as usize);
    // 【新增】读取时间戳
    let ts = buf.get_u64();
    // 【新增】构建KeyVec
    KeyVec::from_vec_with_ts(key.to_vec(), ts)
}
  1. seek_to_index解析后续的键(key),同样的在读取键(key)的内容后读取
let key = &entry[..key_len];
self.key.clear();
self.key.append(&self.first_key.key_ref()[..overlap_len]);
self.key.append(key);
entry.advance(key_len);

// 【新增】读取时间戳
let ts = entry.get_u64();
self.key.set_ts(ts);

Task 2-Encoding Timestamps in SSTs

然后,您可以继续修改table编码格式:

src/table.rs
src/table/builder.rs
src/table/iterator.rs

具体来说,您需要更改数据块元编码以包含键的时间戳。所有其他代码保持不变。正如我们在所有函数的签名(即 seek、add)中使用的KeySlice,新的key比较器应该自动按用户key和timestamp对key进行排序。

在您的table构建器中,您可以直接使用key_ref()来构建bloom过滤器。这自然会为您的 SST 创建一个前缀布隆过滤器。

src/table/builder.rs文件:

首先需要将SsTableBuilder结构中的first_keylast_key两个类型修改一下。由Vec<u8>修改为KeyVec。因为Vec<u8>无法存储时间戳信息。

再将self.first_key = Bytes::copy_from_slice(key.raw_ref()).into();赋值方法修改为 self.first_key.set_from_slice(key);

最后就是bloom过滤器的构建:self.key_hashes.push(farmhash::fingerprint32(key.key_ref()));

src/table.rs文件:

Block块的偏移量、最小的key(first_key)、最大的key(last_key)需要编码进文件中。

编码BlockMeta::encode_block_meta

buf.put_u32(meta.offset as u32);
buf.put_u16(meta.first_key.key_len() as u16);
buf.put(meta.first_key.key_ref());
// 【新增】编码时间戳
buf.put_u64(meta.first_key.ts());
buf.put_u16(meta.last_key.key_len() as u16);
buf.put(meta.last_key.key_ref());
// 【新增】编码时间戳
buf.put_u64(meta.last_key.ts());

解码BlockMeta::encode_block_meta

let offset: usize = buf.get_u32() as usize;
let first_key_len = buf.get_u16() as usize;
// 【修改】构建方式
let first_key =
    KeyBytes::from_bytes_with_ts(buf.copy_to_bytes(first_key_len), buf.get_u64());
let last_key_len: usize = buf.get_u16() as usize;
// 【修改】构建方式
let last_key =
    KeyBytes::from_bytes_with_ts(buf.copy_to_bytes(last_key_len), buf.get_u64());

Task 3-LSM Iterators

由于我们使用关联的泛型类型来使大多数迭代器适用于不同的键类型(即 &[u8]KeySlice<'_>),因此如果正确实现,我们不需要修改merge迭代器和concat迭代器。LsmIterator我们在内部剥离时间戳并将最新版本的键(key)返回给用户的地方。在此任务中,您将需要修改:

src/lsm_iterator.rs

目前,我们不会修改LsmIterator的逻辑,只保留键(key)的最新版本。我们只通过在将键(key)传递给内部迭代器时将时间戳附加到用户输入的键(key)中,并在返回时从键(key)中剥离时间戳来使其编译。目前,您的LSM迭代器的行为应该是向用户返回同一密钥的多个版本。

按题目要求不修改逻辑,只需要将本文件中的raw_ref()方法替换为key_ref()方法。

Task 4-Memtable

现在,我们保留 memtable 的逻辑。我们将KeySlice返回给用户,并使用TS_DEFAULT。我们将在下一章中将 memtable 更改为 MVCC。在此任务中,您将需要修改:

src/mem_table.rs

可以通过编译,找到报错的地方就是KeySlice::from_slice函数需要多一个入参:TS_DEFAULT,存在两处。

KeySlice::from_slice(&self.borrow_item().0[..], TS_DEFAULT)

Task 5-Engine Read Path

在此任务中,您需要修改

src/lsm_storage.rs

现在,我们在键中有一个时间戳,在创建迭代器时,我们需要查找具有时间戳的键(key),而不仅仅是用户键(key)。您可以使用TS_RANGE_BEGIN创建键切片,这是最大的 ts。

当您检查键(key)是否在表中时,您可以简单地比较用户密钥,而无需比较时间戳。

此时,您应该构建您的实施并通过所有第 1 周的测试用例。存储在系统中的所有密钥都将使用TS_DEFAULT(即时间戳 0)。我们将使引擎完全多版本,并通过接下来两章中的所有测试用例。

from_slice函数中添加TS_RANGE_BEGINraw_ref()函数替换为key_ref()即可编译通过。

week3day1的新增测试用例,往一个SST中写入:

key:(key00000, 5) value: value0001
key:(key00000, 4) value: value0002
key:(key00000, 3) value: value0003
key:(key00000, 2) value: value0004
key:(key00000, 1) value: value0005
key:(key00001, 5) value: value0006
...

然后能够还原回来。因为第二章的用例还不能通过,可以先将tests.rs文件中的第二章的测试用例先注释掉,进行测试。

posted @   余为民同志  阅读(11)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示