mini-lsm通关笔记Week3Day3

news/2025/2/15 10:51:39/文章来源:https://www.cnblogs.com/cnyuyang/p/18716617

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

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

在本章中,您将:

  • 在上一章的基础上完成读路径,以支持快照读。
  • 实现事务API,支持快照读。
  • 引擎恢复过程中能正确恢复已提交时间戳。

最后,您的引擎将能够为用户提供存储键(key)的一致视图。

在重构过程中,您可能需要根据需要将某些函数的签名从&self更改为self: &Arc<Self>

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

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

注意:在完成本章后,您还需要通过2.5和2.6的测试用例。

Task 1-LSM Iterator with Read Timestamp

本章的目标是:

let snapshot1 = engine.new_txn();
// 给引擎写点东西
let snapshot2 = engine.new_txn();
// 给引擎写点东西
snapshot1.get(/* ... */); // 我们可以检索引擎先前状态的一致快照

为了达到这个目的,我们可以在创建事务的时候记录下读取的时间戳(也就是最近一次提交的时间戳)。当我们对事务进行读操作时,只会读取低于或等于读取时间戳的所有版本的键(key)。

在此任务中,您需要修改:

src/lsm_iterator.rs

为此,您需要在LsmIterator中记录读取时间戳。

impl LsmIterator {pub(crate) fn new(iter: LsmIteratorInner,end_bound: Bound<Bytes>,read_ts: u64,) -> Result<Self> {// ...}
}

你需要改变你的LSM迭代器next逻辑来找到正确的键。

先在LsmIterator结构体中添加相关字段,并修改构造函数:

pub struct LsmIterator {inner: LsmIteratorInner,upper: Bound<Bytes>,prev_key: Vec<u8>,read_ts: u64, // 【新增】
}pub(crate) fn new(iter: LsmIteratorInner, upper: Bound<Bytes>, read_ts: u64) -> Result<Self> {let mut lsm = Self {inner: iter,upper,prev_key: Vec::new(),read_ts, // 【新增】};...
}

next函数的改造结合Task2完成

Task 2-Multi-Version Scan and Get

在此任务中,您需要修改:

src/mvcc.rs
src/mvcc/txn.rs
src/lsm_storage.rs

现在我们在LSM迭代器中有了read_ts,我们可以在事务结构上实现scanget,这样我们就可以在存储引擎中的给定点读取数据了。

如果需要,我们建议您在LsmStorageInner结构中创建像scan_with_ts(/*原始参数*/, read_ts: u64)get_with_ts这样的辅助函数。存储引擎上最初的get/scan应该实现为创建一个事务(快照),并在该事务上执行get/scan。调用路径类似于:

LsmStorageInner::scan -> new_txn and Transaction::scan -> LsmStorageInner::scan_with_ts

要在LsmStorageInner::scan中创建事务,我们需要向事务构造函数提供一个Arc<LsmStorageInner>。因此,我们可以将scan的签名更改为采取self: &Arc<Self>而不是简单的&self,这样我们就可以用let txn = self.mvcc().new_txn(self.clone(), /* ...*/)来创建一个事务。

您还需要更改scan函数以返回一个TxnIterator。我们必须确保用户迭代引擎时快照是活的,因此,TxnIterator存储快照对象。在TxnIterator内部,我们现在可以存储一个FusedIterator<LsmIterator>。我们稍后在实现OCC时将其更改为其他内容。

您暂时不需要实现Transaction::put/delete,所有修改仍将通过引擎(MiniLsm对象)。

LsmMvccInner::new_txn

先实现从mvcc对象申请一个事务,获取到最新的时间戳,用于构造Transaction对象

pub fn new_txn(&self, inner: Arc<LsmStorageInner>, serializable: bool) -> Arc<Transaction> {let ts = self.ts.lock();let read_ts = ts.0;Arc::new(Transaction {read_ts,inner,local_storage: Arc::new(SkipMap::new()),committed: Arc::new(AtomicBool::new(false)),key_hashes: None,})
}

Transaction

scan函数实现需要依赖TxnIteratorTxnLocalIteratorBuilder先简单完成这两部分内容,帮助流程继续往下走。

TxnLocalIterator最少需要实现is_valid函数:

fn is_valid(&self) -> bool {false
}

is_valid返回false表示这个迭代器中没有有效数据。

TxnIterator最少需要实现他的构造函数以及next函数

// 构造函数
pub fn create(txn: Arc<Transaction>,iter: TwoMergeIterator<TxnLocalIterator, FusedIterator<LsmIterator>>,
) -> Result<Self> {Ok(Self { _txn: txn, iter })
}// next函数
fn next(&mut self) -> Result<()> {self.iter.next()?;Ok(())
}

get&scan,分别调用LsmStorageInnerget_with_tsscan_with_ts函数:

// get函数实现
pub fn get(&self, key: &[u8]) -> Result<Option<Bytes>> {self.inner.get_with_ts(key, self.read_ts)
}// scan函数实现
pub fn scan(self: &Arc<Self>, lower: Bound<&[u8]>, upper: Bound<&[u8]>) -> Result<TxnIterator> {let local_iter = TxnLocalIteratorBuilder {map: self.local_storage.clone(),iter_builder: |map| map.range((map_bound(lower), map_bound(upper))),item: (Bytes::new(), Bytes::new()),}.build();TxnIterator::create(self.clone(),TwoMergeIterator::create(local_iter,self.inner.scan_with_ts(lower, upper, self.read_ts)?,)?,)
}

get_with_ts、scan_with_ts

开始之前先实现new_txn,以及实现新的getscan函数,将原来的函数签名修改为get_with_tsscan_with_ts

pub fn new_txn(self: &Arc<Self>) -> Result<Arc<Transaction>> {Ok(self.mvcc().new_txn(self.clone(), self.options.serializable))
}// 新实现
pub fn scan<'a>(self: &'a Arc<Self>,_lower: Bound<&[u8]>,_upper: Bound<&[u8]>,
) -> Result<TxnIterator> {let txn = self.mvcc().new_txn(self.clone(), self.options.serializable);txn.scan(_lower, _upper)
}// 历史代码,修改函数签名
pub fn scan_with_ts(&self,_lower: Bound<&[u8]>,_upper: Bound<&[u8]>,read_ts: u64, // 新加参数
) -> Result<FusedIterator<LsmIterator>> {...// 历史代码
}// 新实现
pub fn get<'a>(self: &'a Arc<Self>, key: &[u8]) -> Result<Option<Bytes>> {let txn = self.mvcc().new_txn(self.clone(), self.options.serializable);txn.get(key)
}// 历史代码,修改函数签名
pub fn get_with_ts(&self, key: &[u8], read_ts: u64) -> Result<Option<Bytes>> {...// 历史代码
}

首先我们分析一下引入时间戳后的键值对&时间戳的布局:

有以下两点特点:

  1. 单个SST中,存在重复的key,重复key是连续排布,但是时间戳ts不同(取决于比较规则)

  2. 整体看,key依旧是从小到大排布,先出现所有的a再出现b

get_with_ts

因为get操作为点查,所以只需要将所有查询的起始时间设置为read_ts,这里各举一个例子。

// Memtable
let memtable = snapshot.memtable.scan(Bound::Included(KeySlice::from_slice(key, read_ts)), // 修改Bound::Included(KeySlice::from_slice(key, TS_RANGE_END)),
);// SST
SsTableIterator::create_and_seek_to_key(table, KeySlice::from_slice(key, read_ts))?;

scan_with_ts的实现方式有很多,这里仅仅展示我的代码逻辑,对于scan扫描范围不做修改,SsTableIterator返回的是ab所有的

还是用SST1SST2举例,如果read_ts为99,那么需要返回给用户的应该为其中绿色部分:

SsTableIterator则需要实现将灰色部分过滤的功能。实现判断逻辑为:

  • key:a ts:100:通过时间戳判断
  • key:b ts:100:通过时间戳判断
  • key:b ts:96:通过prev_key判断
  • key:c ts:99:通过is_empty()判断
  • key:c ts:97:通过prev_key判断

首先对于上层函数来说SsTableIterator当前的值是否有效不能仅仅通过key是否在搜索范围内判断,还要加上时间戳的判断。同时我们发现,有效的数据不是连续的,所以SsTableIterator在碰到第一个无效数据时不能停止搜索,只有key不在范围内才能停止。,为达成以上目的,需要作如下修改:

fn is_valid(&self) -> bool {self.is_key_valid() && self.read_ts >= self.inner.key().ts()
}fn is_key_valid(&self) -> bool {if !self.inner.is_valid() {return false;}let mut is_valid = true;match self.upper.as_ref() {Bound::Included(upper) => is_valid = self.inner.key().key_ref() <= upper.as_ref(),Bound::Excluded(upper) => is_valid = self.inner.key().key_ref() < upper.as_ref(),Bound::Unbounded => {}}is_valid
}

将原来is_valid内容移至is_key_valid中,is_valid新增时间戳判断,is_valid供外部调用,is_key_valid迭代器内部使用。

构造函数new

pub(crate) fn new(iter: LsmIteratorInner, upper: Bound<Bytes>, read_ts: u64) -> Result<Self> {let mut lsm = Self {inner: iter,upper,prev_key: Vec::new(),read_ts,};while lsm.is_key_valid() && (lsm.inner.key().ts() > read_ts || lsm.value().is_empty()) {if lsm.value().is_empty() {lsm.prev_key = lsm.key().to_vec();}lsm.next();}if lsm.is_key_valid() {lsm.prev_key = lsm.key().to_vec();}Ok(lsm)
}

next函数:

fn next(&mut self) -> Result<()> {self.inner.next();if self.inner.is_valid() {if self.inner.key().ts() > self.read_ts {return self.next();}if self.inner.value().is_empty() {self.prev_key = self.key().to_vec();return self.next();}if self.prev_key == self.key().to_vec() {return self.next();}self.prev_key = self.key().to_vec();}Ok(())
}

除此之外还需要修改scan_with_ts中处理Excluded的逻辑,在scan中会通过SsTableIterator::create_and_seek_to_key找到左边界,然后通过:

if iter.is_valid() && iter.key().key_ref() == key {iter.next()?;
}

跳过当前值。这个操作在非mvcc版本是生效的,因为非mvcc版本不存在相同的键。然后我们此前修改的逻辑是LsmIterator中的,也在SstConcatIterator不生效。为适配mvcc,只要把if改成while就能完整的跳过这个字段的所有版本:

while iter.is_valid() && iter.key().key_ref() == key {iter.next()?;
}

Task 3-Store Largest Timestamp in SST

在此任务中,您需要修改:

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

在SST编码中,您应该在块元数据之后存储最大的时间戳,并在加载SST时恢复它。这将有助于系统在恢复系统时确定最新的提交时间戳。

编码encode_block_meta

pub fn encode_block_meta(block_meta: &[BlockMeta],max_ts: u64, // 【新增】最大的时间戳#[allow(clippy::ptr_arg)] buf: &mut Vec<u8>,
) {let original_len = buf.len();buf.put_u32(block_meta.len() as u32);for meta in block_meta {... }buf.put_u64(max_ts); // 【新增】写入最大的时间戳buf.put_u32(crc32fast::hash(&buf[original_len + 4..]));
}

解码decode_block_meta

pub fn decode_block_meta(mut buf: &[u8]) -> Result<(Vec<BlockMeta>, u64)> { //【修改】修改返回值类型let num = buf.get_u32();let checksum = crc32fast::hash(&buf[..buf.remaining() - 4]);let mut block_meta: Vec<BlockMeta> = Vec::with_capacity(num as usize);for i in 0..num {...}let max_ts = buf.get_u64(); // 【新增】读取最大的时间戳if buf.get_u32() != checksum {bail!("meta checksum mismatched");}Ok((block_meta, max_ts)) // 【修改】返回最大时间戳
}

然后同步修改调用点。需要在SsTableBuilder结构体中新增成员变量max_ts,调用add自动更新该变量,最后在build函数中传入encode_block_meta

Task 4-Recover Commit Timestamp

现在我们有了SST中的最大时间戳信息和WAL中的时间戳信息,我们可以获取引擎启动前提交的最大时间戳,并在创建mvcc对象时使用该时间戳作为最新提交的时间戳。

如果没有启用WAL,您可以简单地通过找到SST中最大的时间戳来计算最新提交的时间戳。如果启用了WAL,您应该进一步迭代所有恢复的memtable,并找到最大的时间戳。

在此任务中,您需要修改:

src/lsm_storage.rs

我们没有此部分的测试用例。在完成本节之后,您应该通过前几章(包括2.5和2.6)的所有持久性测试。

修改LsmStorageInner::open函数:

let mut last_commit_ts = 0; // 声明last_commit_ts
if !manifest_path.exists() {manifest = Some(Manifest::create(manifest_path)?);
} else {... for table_id in state.l0_sstables.iter().chain(state.levels.iter().map(|(_, files)| files).flatten()){let table_id = *table_id;let sst = SsTable::open(table_id,Some(block_cache.clone()),FileObject::open(&Self::path_of_sst_static(path, table_id)).context("failed to open SST")?,)?;last_commit_ts = last_commit_ts.max(sst.max_ts()); // 读取SST,更新last_commit_tsstate.sstables.insert(table_id, Arc::new(sst));}if options.enable_wal { // 打开WAL日志场景...let max_ts = memtable.map.iter().map(|x| x.key().ts()).max().unwrap_or_default();last_commit_ts = last_commit_ts.max(max_ts); // 读取Memtable,更新last_commit_tsif !memtable.is_empty() {state.imm_memtables.insert(0, Arc::new(memtable));}}
...
let storage = Self {...mvcc: Some(LsmMvccInner::new(last_commit_ts)), // 使用恢复出来的ast_commit_ts创建mvcc对象...
};Ok(storage)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/884175.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

软件测试流程——H模型

1、产品开需求澄清会议 2、开发和测试拿到需求 3、分析需求,编写测试计划 4、编写测试用例 5、评审测试用例 6、评审通过,将用例导入用例管理工具 7、搭建环境 8、开发提交项目代码包,达到准入条件 9、测试将项目代码包部署到环境中 10、开始冒烟测试 (冒烟测试通过,则进入…

请求被中止: 未能创建 SSL/TLS 安全通道_微信红包接口_iis_windows

开发微信红包接口过程中出现:请求被中止: 未能创建 SSL/TLS 安全通道。本地调试正常,但是服务器上不行。 通用方法:指定使用的协议tls(https://myssl.com,这里可以测试对应接口服务器支持的协议) 证书导入赋予权限,参考如下文章: 两种方式彻底解决请求被中止- 未能创建…

SpringBoot环境搭建

JDK 在Oracle官网下载JDK11需注册Oracle账户,较麻烦。 在https://jdk.java.net/archive/找到11 GA (build 11+28)下载Windows版本的压缩包解压后将jdk-11文件夹放到合适位置(多个版本可同时存在),IDE可自动检测到该JDK 如果存在问题可能需要进一步配置环境变量,先创建一个…

C语言静态查找表的查找算法(顺序查找、折半查找)

顺序查找的实现#include <stdio.h>#define SIZE 10 // 静态查找表的大小// 顺序查找函数 int sequentialSearch(int arr[], int size, int target) {for (int i = 0; i < size; i++) {if (arr[i] == target) {return i; // 查找成功,返回目标的索引}}return -1; /…

游戏陪玩接单平台源码,如何实现按钮悬浮功能

游戏陪玩接单平台源码,如何实现按钮悬浮功能实现可视区域底部悬浮编辑提交等功能的悬浮框: 并当滚动到某处时, 固定在该处<template><div><slot></slot><divv-show="show"style="height: 80px"></div></div>…

11. Docker 微服务实战(将项目打包生成镜像,在 Docker 当中作为容器实例运行)

11. Docker 微服务实战(将项目打包生成镜像,在 Docker 当中作为容器实例运行) @目录11. Docker 微服务实战(将项目打包生成镜像,在 Docker 当中作为容器实例运行)2. 最后:建 Module - docker_boot编辑 pom<?xml version="1.0" encoding="UTF-8"?&g…

raylib U1S02 - raylib程序的基本结构

在写raylib的时候,代码有一个基本的结构,不同功能的代码按照这个机构去填充。 一般来说,我们可以把一个raylib的程序分成三个模块:创建窗口和元素 主循环,实时计算和渲染 结束程序,释放空间下面是基本框架的代码。以后大家写raylib,可以直接先把这段代码复制上 #include…

Regulex:这款正则表达式可视化神器,让复杂正则一目了然!

引言 大家好,今天分享一个可视化正则表达式的网站,可以更加清晰的调试正则表达式,方便大家应对那种没有注释并且看不懂的正则。 正则可视化 地址:https://jex.im/regulex/#!flags=&re= Github 地址:https://github.com/CJex/regulex 来看看效果,网站自带的正则可视化…

Windows 10/11 安装paraview

下载: https://www.paraview.org/download/安装打开 paraview 之后报错,如下: paraview.exe -系统错误 paraview 由于找不到 msmpi.dll, ,无法继续执行代码。重新安装程序可能会解决此向题 安装 MPI v10.1.3 https://www.microsoft.com/en-us/download/details.aspx?id…

Float、Double 浮点数的二进制表示方法

转载请标明原文地址:https://segmentfault.com/a/1190000041768195 Float二进制表示法 IEEE754标准中规定,单精度浮点数float占4字节32位 Sign(符号1位)|Exponent(指数8位 偏移127)|Mantissa(尾数23位) Sign(符号):表示浮点数的正负(大于等于0为0,小于0为1) Exponent(指数):…