mini-bitcask学习笔记
代码仓库:mini-bitcask
type KeyDir = std::collections::BTreeMap<Vec<u8>, (u64, u32)>;
pub type Result<T> = std::result::Result<T, std::io::Error>;
-
keyDir的值:(文件偏移量, 值长度)
-
pub type Result<T>是错误处理惯用法,这里统一使用std::io::Error
选择BTreeMap:自动按键排序,支持范围查询
pub struct MiniBitcask {log: Log,keydir: KeyDir,
}pub struct ScanIterator<'a> {inner: btree_map::Range<'a, Vec<u8>, (u64, u32)>,log: &'a mut Log,
}
MiniBitcask:对外提供 API(set/get/delete/scan),内部持有 Log 和 KeyDir。
Log:负责文件IO操作KeyDir:内存中的索引,快速定位键的位置
ScanIterator 是迭代器,引用 MiniBitcask 里的资源,必须用生命周期 'a 约束:
inner: btree_map::Range<'a, ...>:Range是BTreeMap的范围迭代器,是对KeyDir(MiniBitcask的字段)的不可变引用。'a表示这个引用和KeyDir的存活时间一致。log: &'a mut Log:是对MiniBitcask里log字段的可变引用。'a同样约束这个可变引用的必须和Log的存活时间一致。
效果:
fn bad_example() {let mut iter: ScanIterator;{let mut db = MiniBitcask::new(path).unwrap(); // db 是 MiniBitcask 实例iter = db.scan(...); // iter 引用了 db 的 keydir 和 log} // db 在这里被销毁,它的 keydir 和 log 也没了iter.next(); // 错误:iter 引用的资源已经不存在(悬垂引用)
}
impl Drop for MiniBitcask {fn drop(&mut self) {if let Err(error) = self.flush() {log::error!("failed to flush file: {:?}", error)}}
}
-
Drop trait类似于其他语言的析构函数 -
在
MiniBitcask离开作用于时自动调用
if let模式匹配:只处理Err情况,成功时不作任何操作
fn new(path: PathBuf) -> Result<Self> {let file = std::fs::OpenOptions::new().read(true).write(true).create(true).open(&path)?;file.try_lock_exclusive()?;Ok(Self { path, file })
}
OpenOptions构建器模式:链式调用配置文件打开方式?操作符:错误传播,如果失败直接返回Errtry_lock_exclusive()文件锁防止多进程同时写入
fn load_index(&mut self) -> Result<KeyDir> {while pos < file_len {let read_one = || -> Result<(Vec<u8>, u64, Option<u32>)> {// 读取操作...}();match read_one {Ok((key, value_pos, Some(value_len))) => {keydir.insert(key, (value_pos, value_len));}Ok((key, value_pos, None)) => {keydir.remove(&key);pos = value_pos;}Err(err) => return Err(err.into()),}}Ok(keydir)
}
- 缓冲区设计:
len_buf: [0u8; 4]: 复用缓冲区读取长度字段BufReader: 缓冲读取,减少系统调用
- 立即执行闭包:
let read_one = || { ... }(): 定义并立即执行
- 删除标记处理:
i32::from_be_bytes(len_buf): 值长度用有符号整数存储l >= 0: 正常数据l < 0: 删除标记(tombstone)
为什么需要这个方法:
日志文件是追加写入的(新的键值对/删除操作 都往文件末尾加),程序重启后,必须知道哪些键有效、值存在哪里。load_index 就是干这个的:通过一次完整的文件扫描,把所有键的最新状态(存在 / 删除)记录到 KeyDir 里,后续操作(get/set 等)直接查 KeyDir 就能快速定位,不用再扫整个文件。
impl<'a> Iterator for ScanIterator<'a> {type Item = Result<(Vec<u8>, Vec<u8>)>;fn next(&mut self) -> Option<Self::Item> {self.inner.next().map(|item| self.map(item))}
}
-
type Item = Result<(Vec<u8>, Vec<u8>)>是Iterator trait的关联类型,定义了迭代器每次迭代返回的元素类型 -
fn next(&mut self) -> Option<Self::Item>是Iterator trait的核心方法,定义了如何获取下一个元素- 返回值
Option<Self::Item>:迭代未结束时返回Some(Result<...>),迭代结束时返回None - 实现逻辑分两步: ①
self.inner.next():self.inner是BTreeMap的范围迭代器(btree_map::Range),它的next方法会返回下一个“键和(值位置,值长度)”的引用(类型是Option<(&Vec<u8>, &(u64, u32))>)。简单说:先从内存索引KeyDir中拿到下一个符合条件的key,以及它的value在文件中的位置和长度。 ②.map(|item| self.map(item)):对第一步拿到的结果进行转换。self.map(item)是ScanIterator自己的方法,作用是:根据item中的“值位置”和“值长度”,从日志文件(self.log)中读取实际的value,最终返回Result<(Vec<u8>, Vec<u8>)>(即键值对)
- 返回值
pub struct ScanIterator<'a> {inner: btree_map::Range<'a, Vec<u8>, (u64, u32)>, // 引用了 'a 生命周期的资源log: &'a mut Log, // 引用了 'a 生命周期的资源
}
ScanIterator 结构体本身是带生命周期的
ScanIterator<'a> 中的 'a 表示:它包含的两个字段(inner 和 log)都是引用,且这些引用的 存活时间被约束在 'a 这个范围内。
当你为 ScanIterator<'a> 实现 Iterator trait 时,必须在 impl 后面加上 <'a>,原因是:
要告诉编译器:当前 impl 块是针对 ScanIterator<'a> 这个带生命周期的类型的,块里的方法(比如 next)会用到 'a 约束的引用,需要保持生命周期一致。
'a 在这里绑定了三个东西的存活时间:
ScanIterator结构体本身的存活时间;- 它的字段
inner(BTreeMap的范围迭代器,本质是对KeyDir的引用)的存活时间; - 它的字段
log(对Log的可变引用)的存活时间。
编译器通过这个标签会强制检查:ScanIterator 实例的存活时间,不能超过它引用的 KeyDir 和 Log 的存活时间。
