@TOC
LevelDB 读取有以下特点
- 多层查找策略:使用memory 和多级别的sstable来进行分层缓存来进行查询
- 版本控制: 使用快照来保证查询操作数据一致性
- 锁优化:多线程来进行数据查询
过程详解
1. 调用LevelDB的读取接口
leveldb::DB *dbptr = nullptr;
// 创建数据库
leveldb::Status s = leveldb::DB::Open(opts, "/tmp/leveldb_test01", &dbptr);
// 数据写入
s = db->Put(write_opts, "name", "leveldb");
s = db->Put(write_opts, "time", "20240319");
// 数据查询
std::string value;
leveldb::ReadOptions read_opts;
s = db->Get(read_opts, "", &value);
2. 读取快照
if (options.snapshot != nullptr) {
snapshot =
static_cast<const SnapshotImpl*>(options.snapshot)->sequence_number();// 隐式转换
} else {
snapshot = versions_->LastSequence();
}
}
读取快照,如果存在指定快照则读取指定快照,如果不存在就读取最新的快照 快照的目的是为了实现MVCC 主要有以下的特点
- 提供读取历史能力的能力
- 保证读取的一致性
- 支持多版本的并发控制
3. 初始化可变内存和不可变内存
// 代码地址:db/db_impl.cc
MemTable* mem = mem_;// 可变内存
MemTable* imm = imm_;// 不可变内存
这里的可变内存和不可变内存,是因为levelDB的LSM-Tree机制,简单来说,就是多级缓存来提升效率,数据在写入时会先写入可变内存,可变内存满了随后变成不可变内存,不可变内存会异步写入磁盘中的level 1层,多个level1会被压缩变成level2,以及往上递增,默认最多时7层
具体可以参考博客:XXXX
4. 核心逻辑:分级别读取数据
mutex_.Unlock();// 解锁
LookupKey lkey(key, snapshot);// 构建读取辅助类
if (mem->Get(lkey, value, &s)) {// 从内存中读取锁
// Done
} else if (imm != nullptr && imm->Get(lkey, value, &s)) {// 查找不可变内存表
// Done
} else {
s = current->Get(options, lkey, value, &stats);// 查找持久化存储
have_stat_update = true;
}
mutex_.Lock();// 重新上锁
4.1 内存读取方法
Slice memkey = key.memtable_key();
Table::Iterator iter(&table_);
iter.Seek(memkey.data()); // 使用跳表的seek操作读取对应的数据
这里对内存的检索做了优化 简单来说就是 使用多层索引组织了在内存中的形式, 表现是实现了一个跳表,相当于在kv这种使用有序key做排列的组织中,对key进行跳跃式的检索。
Slice memkey = key.memtable_key();
Table::Iterator iter(&table_);
iter.Seek(memkey.data()); // 使用跳表的seek操作读取对应的数据
核心的内存检索逻辑
// 从最高层开始
int level = GetMaxHeight() - 1;
while (true) {
// 获取当前层的下一个节点
Node* next = x->Next(level);
// 如果当前层的下一个节点大于key, 则继续向下层查找
if (KeyIsAfterNode(key, next)) {
// Keep searching in this list
x = next;
} else {
// 如果prev不为空, 则将当前节点赋值给prev[level]
if (prev != nullptr) prev[level] = x;
// 如果当前层为0层, 则返回下一个节点
if (level == 0) {
return next;
} else {
// 如果当前层不是0层, 则继续向下层查找
level--;
}
}
}
举个简单的例子
// 跳表的金字塔结构
Level 3: head ----------------> 20 -------------------------> 50
| |
Level 2: head --------> 15 --> 20 -----------> 35 ---------> 50
| | | |
Level 1: head --> 10 -> 15 -> 20 --> 25 ----> 35 --> 45 -> 50
| | | | | | |
Level 0: head -> 10 -> 15 -> 20 -> 25 -> 30 -> 35 -> 45 -> 50
// 查找值为27的节点
Level 3: head ------> 20 --------| // 快速跳到20
v
Level 2: head -> 15 -> 20 -----> 35 // 发现35太大,向下
v
Level 1: head -> ... -> 25 -> 35 // 找到25,继续
v
Level 0: head -> ... -> 25 -> 30 // 最终定位
具体可以参考博客: todo: XXXX
5. 在磁盘中sstable中进行检索
mutex_.Unlock(); // 先拿锁
status = log_->AddRecord(WriteBatchInternal::Contents(write_batch));// 调用日志文件 写入 信息
// 可选择的同步操作
status = logfile_->Sync(); // 将内存中日志数据刷写到磁盘中
6. 写入数据到内存中
s = current->Get(options, lkey, value, &stats);// 查找持久化存储
简单来说 这个函数调用的检索是检索磁盘上的sstable,首先会读取磁盘上文件的缓存数据,如果没有就检索更下层的文件,读取操作系统中的文件
// 创建索引块的迭代器
Iterator* iiter = rep_->index_block->NewIterator(rep_->options.comparator);
iiter->Seek(k);//在索引块中查找
if (iiter->Valid()) {
// 找到可能包含目标key的位置
Slice handle_value = iiter->value();
// 布隆过滤器检查
FilterBlockReader* filter = rep_->filter;
BlockHandle handle;
if (filter != nullptr && handle.DecodeFrom(&handle_value).ok() &&
!filter->KeyMayMatch(handle.offset(), k)) {// 如果布隆过滤器表示key不存在,直接返回
// Not found
} else {
// 读取实际的数据块
Iterator* block_iter = BlockReader(this, options, iiter->value());
// 在数据块中查找
block_iter->Seek(k);
if (block_iter->Valid()) {
// 找到数据,调用回调函数处理结果
(*handle_result)(arg, block_iter->key(), block_iter->value());
}
s = block_iter->status();
delete block_iter;
}
}
7. 接下来在后台调用压缩层级的方法
MaybeScheduleCompaction(); // 压缩层级的方法 主要是为了提升读取的性能
通过环境变量 env_ 在后台调用压缩方法
mutex_.AssertHeld();// 确保 互斥锁已经持有:主要是为了确保能拿到 background_compaction_scheduled_
if (background_compaction_scheduled_) {// 确保当前没有正在运行的压缩任务: 查看当前的后台压缩任务标识 是否为true, 如果为true
} else if (shutting_down_.load(std::memory_order_acquire)) {// 确保数据库正常运行: 查看数据库, 是否已经关闭, 如果关闭, 不需要进行压缩
} else if (!bg_error_.ok()) {// 检查是否有后台错误
} else if (imm_ == nullptr && manual_compaction_ == nullptr &&
!versions_->NeedsCompaction()) {// 判断是否需要压缩
// 检查
/**
* 1. imm 是否为 nullptr, 表示没有不可变内存表需要处理
* 2. manual_compaction_ 是否为 nullptr,表示没有手动压缩任务。
* 3. versions_->NeedsCompaction() 是否返回 false,表示当前版本不需要压缩。
*/
} else {
/**
* 后台运行 方法
*/
background_compaction_scheduled_ = true;
env_->Schedule(&DBImpl::BGWork, this);//
}
参考源码&项目链接
github 链接: git@github.com:luogaiyu/leveldb.git
Status DBImpl::Get(const ReadOptions& options, const Slice& key,
std::string* value) {
Status s;
MutexLock l(&mutex_);
SequenceNumber snapshot;
/**
* 读取快照,如果存在指定快照则读取指定快照,如果不存在就读取最新的快照
* 快照的目的是为了实现MVCC
* 主要有以下的特点
* 1. 提供读取历史能力的能力
* 2. 保证读取的一致性
* 3. 支持多版本的并发控制
*/
if (options.snapshot != nullptr) {
snapshot =
static_cast<const SnapshotImpl*>(options.snapshot)->sequence_number();// 隐式转换
} else {
snapshot = versions_->LastSequence();
}
MemTable* mem = mem_;// 可变内存
MemTable* imm = imm_;// 不可变内存
// 主要有两种 memtable
Version* current = versions_->current();
mem->Ref();// 增加引用
//
if (imm != nullptr) imm->Ref();
current->Ref();
bool have_stat_update = false;
Version::GetStats stats; // 查看版本
// Unlock while reading from files and memtables
{
mutex_.Unlock();// 查找操作之前 释放互斥锁, 减少锁竞争, 因为查找过程中, 读取操作是无害的, 不需要持有锁
LookupKey lkey(key, snapshot);
if (mem->Get(lkey, value, &s)) {// 从内存中读取锁
// Done
} else if (imm != nullptr && imm->Get(lkey, value, &s)) {// 查找不可变内存表
// Done
} else {
s = current->Get(options, lkey, value, &stats);// 查找持久化存储
have_stat_update = true;
}
mutex_.Lock();// 重新上锁
}
if (have_stat_update && current->UpdateStats(stats)) {
MaybeScheduleCompaction(); // 压缩层级的方法 主要是为了提升读取的性能
}
mem->Unref();
if (imm != nullptr) imm->Unref();
current->Unref();
return s;
}
猜你喜欢
C++多线程: blog.csdn.net/luog_aiyu/a…
PS
你的赞是我很大的鼓励 欢迎大家加我飞书扩列, 希望能认识一些新朋友~ 二维码见: www.cnblogs.com/DarkChink/p…