当前位置: 首页 > news >正文

[LevelDB]关于LevelDB存储架构到底怎么设计的?

本文内容组织形式

  • LevelDB 存储架构重要特点
  • 总体概括
  • LevelDB中内存模型
  • MemTable
    • MemTable的数据结构
      • 背景:
        • SkipList
          • Skiplist的数据结构
        • Skiplist的数据访问细节
      • SkipList的核心方法
        • Node细节
          • 源代码
    • MemTable的数据加速方式
      • Iterator 的核心方法
    • MemTable 的读取&写入细节
    • MemTable的内存分配器 --Arena
      • Arena 核心方法
      • Arena 核心方法源代码
  • Table
    • Table 对外提供服务场景
      • 写入场景
      • 压缩场景
      • 读取场景
    • Table写入逻辑-TableBuilder
      • 源代码
    • Table读取逻辑-Table
      • 源代码
  • Block
    • Block 的读取流程
      • 源代码
    • Block 的写入流程
      • 源代码
  • 猜你喜欢
  • PS

LevelDB 存储架构重要特点

  1. LSM树结构:本质上就是分层存储+WAL+后台进程异步写入,将原本需要任意写入(random write: 我觉得翻译成任意写入更合理,随机写入是传统叫法)
    如下图架构
// LSM整体架构
+-------------------------+
|       Write Path        |
+-------------------------++-------------------------+
|    Write Ahead Log      |  // WAL日志
+-------------------------++-------------------------+
|      MemTable (L0)      |  // 活跃内存表(跳表)
+-------------------------++-------------------------+
|  Immutable MemTable     |  // 不可变内存表
+-------------------------++-------------------------+
|   SSTable Files (L0)    |  // Level 0:文件可能重叠
+-------------------------++-------------------------+
|   SSTable Files (L1)    |  // Level 1:文件有序不重叠
+-------------------------++-------------------------+
|   SSTable Files (L2)    |  // Level 2:容量是L1的10倍
+-------------------------++-------------------------+
|   SSTable Files (L3)    |  // Level 3:容量是L2的10倍
+-------------------------+
WAL日志:指的是操作日志,提前写入操作日志,是为了保证操作的原子化,如果在写入过程中出现问题,能够根据WAL日志重新运行操作
MemTable:使用跳表数据结构来实现内存结构
SSTable Files: 在磁盘上的文件,如果在实际的运行中,本质上指的是运行LevelDB的磁盘日志
todo: 缺个图
  1. 压缩策略: 使用可选择的压缩算法,对数据流进行可控改造,本质上是平衡计算机CPU和IO,因为压缩是相当于用一种编码信息的方式对原有数据进行精简表达。
  2. 索引加速: 使用布隆过滤对数据进行快速索引,就是相当于用多个hash来算同一个值,从而避免
  3. 自主控制的内存: C++相比Java来说最重要的变化就是,Java所有的代码都是run在JVM上的,就是相当于会把Java代码再转化成.class文件,然后再被JVM进行解释,这样JVM就会代替app层来进行内存管理,但是坏处是本质上会多消耗一些资源。C++则主要通过自己来进行内存的管理和控制,本质上就是通过创建对象并通过delete来进行控制对象的创建和销毁,虽然现在出现智能指针能够自己来控制,但是自己来控制的代价本质上和JVM一样会增大内存的消耗,但是如果忘记销毁会出现潜在内存泄漏,程序会run着run着就挂了,隔一段时间要重启一次(别问我怎么知道的)
    PS: LevelDB实际实现的时候有挺多特点,这里只挑选最重要几个

总体概括

本文准备从自上而下来解释LevelDB的存储设计结构(后续可能会补充和其他存储结构的异同todo,现在还不会)
在这里插入图片描述
首先我们要明白上图中主要提出了这样几个概念和实体,分别一句话解释

  1. USER:用户
  2. MemTable:能够被写入的可变内存
  3. Immutable:MemTable如果写满就不可写入,后续异步写入磁盘
  4. table: 本质上就是在磁盘上的一个sstable文件,就是运行了leveldb程序后,写入数据后生成的日志文件
  5. block: LevelDB的最小存储单元,多个block组成了table
  6. block Cache/Table Cache:对表(table)和块(block)进行缓存

LevelDB中内存模型

首先从内存的视角来看
主要分为下面这些模型

  1. Arena:高效的小对象分配
  2. Block Cache:读取性能优化
  3. TableCache:文件句柄管理
  4. MemTable:写入性能优化

PS:MemTable&Immutable 这两个实体的本质上是一样的,只是Immutable是Memtable的不可变的版本,接下来准备从下面两个方面来解释这两个实体

MemTable

MemTable的数据结构

背景:

MemTable使用Skiplist(跳表)数据结构来加速在内存结构对key的检索,本质上是通过Skiplist这种跳表数据结构来优化kv索引中k的遍历。

SkipList
Skiplist的数据结构

本质上SkipList 由 Node元素组成,具体的组成方式如下图所示,HEAD蓝块和黄色块都是Node, 这里的L3-L1指的是 Skiplist中的层级,因为SkipList是一个跳表数据结构,会根据不同的key来跳过一些node,来加速索引,

  1. key:当前
    在这里插入图片描述
Skiplist的数据访问细节

在这里插入图片描述
具体查找过程如下:

  1. 从头节点(HEAD)的最高层(L3)开始查找
  2. 在L3层向右移动到key=30的节点(发现下一个节点会超过目标值)
  3. 从key=30的L3层下降到L2层继续查找
  4. 从key=30节点降至L2层
  5. 从L3层降至L2层继续查找
  6. 从L3层降到L2层 (30 < 40,但30的右节点值会 > 40)
  7. 在L2层向右找到目标节点key=40”

参考源代码

SkipList<Key, Comparator>::FindGreaterOrEqual(const Key& key,Node** prev) const {// 从头节点开始Node* x = head_;// 从最高层开始int level = GetMaxHeight() - 1;while (true) {// 获取当前层的下一个节点Node* next = x->Next(level);// 如果当前层的下一个节点大于key, 则继续向下层查找if (KeyIsAfterNode(key, next)) {// Keep searching in this listx = next;} else {// 如果prev不为空, 则将当前节点赋值给prev[level]if (prev != nullptr) prev[level] = x;// 如果当前层为0层, 则返回下一个节点if (level == 0) {return next;} else {// 如果当前层不是0层, 则继续向下层查找  level--;}}}
}

SkipList的核心方法

在添加新节点时,会通过如下的方法确定当前node节点的高度,即height
概率分布:

  1. 所有节点(100%)至少有高度1
  2. 约25%的节点高度≥2
  3. 约6.25%的节点高度≥3
  4. 约1.56%的节点高度≥4
  5. 以此类推…
template <typename Key, class Comparator>
int SkipList<Key, Comparator>::RandomHeight() {static const unsigned int kBranching = 4;// kBranching = 4 表示概率因子int height = 1;while (height < kMaxHeight && rnd_.OneIn(kBranching)) {// 相当于有1/4 概率为trueheight++;}assert(height > 0);assert(height <= kMaxHeight);return height;
}
Node细节

Node主要有两个关键变量

  1. key: KV数据库里面的key
  2. next_: 表示当前的节点(Node)的下一个节点的地址

Next和NoBarrier_Next 这两个方法的区别就是,一个的next_变量的访问是需要等到所有的内存写操作结束之后才会进行读取,NoBarrier_Next 直接会读取原子变量 next_ 不会有任何等待

源代码
// 接下来 是具体的实现细节:  嵌套结构体 Node 的实现
template <typename Key, class Comparator>
struct SkipList<Key, Comparator>::Node {explicit Node(const Key& k) : key(k) {}Key const key;// 链接的访问器/修改器。封装在方法中,以便我们可以// 根据需要添加适当的内存屏障。Node* Next(int n) {assert(n >= 0);// 使用“获取加载”操作,以便我们观察返回的Node的完全初始化版本。// 表示所有写操作都在更新指针前执行完return next_[n].load(std::memory_order_acquire);}void SetNext(int n, Node* x) {assert(n >= 0);// 使用“释放存储”,以便任何通过此指针读取的线程都会看到一个完全初始化的版本。// 表示所有写操作都在更新指针后执行完next_[n].store(x, std::memory_order_release);}// 无内存屏障的变体,可以在少数位置安全使用。Node* NoBarrier_Next(int n) {assert(n >= 0);return next_[n].load(std::memory_order_relaxed);// 使用std::memory_order_relaxed 不会}void NoBarrier_SetNext(int n, Node* x) {assert(n >= 0);next_[n].store(x, std::memory_order_relaxed);}private:// Array of length equal to the node height.  next_[0] is lowest level link.std::atomic<Node*> next_[1];
};

MemTable的数据加速方式

MemTable的数据访问加速通过Iterator这个特性来进行实现, 为什么需要Iterator,而不是通过Node._next直接实现数据访问,主要是使用Iterator可以相比直接使用Node的方法有以下的额外特性,本质上就是对SKiplist的数据结构扩展一些能够方便遍历的特性。

  1. 双向遍历
  2. 状态管理, Valid()方法

Iterator 的核心方法

  1. Next: 向后遍历,本质上使用node_.Next(0) 。
  2. Prev: 向前遍历, 本质上相当于 list_->FindLessThan(node_->key)。
template <typename Key, class Comparator>
// 判断当前的迭代器是否有效,如果有效就调用next方法
inline void SkipList<Key, Comparator>::Iterator::Next() {assert(Valid());node_ = node_->Next(0);
}template <typename Key, class Comparator>
// 判断当前的迭代器是否有效,如果有效就调用prev方法
inline void SkipList<Key, Comparator>::Iterator::Prev() {// Instead of using explicit "prev" links, we just search for the// last node that falls before key.assert(Valid());// 调用 findLessThan 方法,找到当前节点的前一个节点node_ = list_->FindLessThan(node_->key);if (node_ == list_->head_) {node_ = nullptr;}
}

MemTable 的读取&写入细节

主要的方法有以下

  1. Add: 写入MemTable的方法
    在这里插入图片描述

  2. 计算大小

  3. 计算总编码长度

  4. 内存分配

  5. 编码与复制数据

  6. 验证与插入

  7. Get: 读取MemTable的方法
    在这里插入图片描述
    流程

  8. 迭代器是否找到有效位置

  9. 键前缀是否匹配

  10. 根据类型标记确定是返回值还是返回已删除状态

void MemTable::Add(SequenceNumber s, ValueType type, const Slice& key,const Slice& value) {// 这里可以看到 具体内存中数据的实现, 这里相当于做了 key 和 value的一层转换/*** 这里的重点就是 arena_.Allocate 分配内存* std::memcpy: 内存复制*/// Format of an entry is concatenation of://  key_size     : varint32 of internal_key.size()//  key bytes    : char[internal_key.size()]//  tag          : uint64((sequence << 8) | type)//  value_size   : varint32 of value.size()//  value bytes  : char[value.size()]size_t key_size = key.size();size_t val_size = value.size();size_t internal_key_size = key_size + 8;const size_t encoded_len = VarintLength(internal_key_size) +internal_key_size + VarintLength(val_size) +val_size;char* buf = arena_.Allocate(encoded_len);// 使用arena 来进行内存分配char* p = EncodeVarint32(buf, internal_key_size);std::memcpy(p, key.data(), key_size);p += key_size;EncodeFixed64(p, (s << 8) | type);// 这个是为了内存对齐, todo, 确认下p += 8;p = EncodeVarint32(p, val_size);std::memcpy(p, value.data(), val_size);assert(p + val_size == buf + encoded_len);table_.Insert(buf); // 这里还是使用跳表来进行存储, 使用跳表来组织结构
}bool MemTable::Get(const LookupKey& key, std::string* value, Status* s) {// Memtable 的get 方法, 相当于内存进行了一层缓存Slice memkey = key.memtable_key();Table::Iterator iter(&table_);iter.Seek(memkey.data());if (iter.Valid()) {// entry format is://    klength  varint32//    userkey  char[klength]//    tag      uint64//    vlength  varint32//    value    char[vlength]const char* entry = iter.key();uint32_t key_length;const char* key_ptr = GetVarint32Ptr(entry, entry + 5, &key_length);if (comparator_.comparator.user_comparator()->Compare(Slice(key_ptr, key_length - 8), key.user_key()) == 0) {// Correct user keyconst uint64_t tag = DecodeFixed64(key_ptr + key_length - 8);switch (static_cast<ValueType>(tag & 0xff)) {case kTypeValue: {Slice v = GetLengthPrefixedSlice(key_ptr + key_length);value->assign(v.data(), v.size());return true;}case kTypeDeletion:*s = Status::NotFound(Slice());return true;}}}return false

MemTable的内存分配器 --Arena

作用: 为memtable这种对象,比如说频繁插入内存的情况下,提前申请一批内存来防止直接使用 malloc 和new调用系统调用,这样的话开销非常高,并且标准实现中内存会存在全局锁,会有内存竞争的问题,这里需要对比一下 ,本质上是使用 池化的计数来对内存进行管理

Arena 核心方法

  1. Allocate: 从当前内存块中快速分配内存,空间不足时调用AllocateFallback。
  2. AllocateFallback: 处理内存不足情况,根据请求大小决定分配专用块或新标准块。
  3. AllocateAligned: 分配内存时确保地址对齐,计算额外填充并处理对齐要求。

Arena 核心方法源代码

// 使用内联防止内存展开, 主要的作用是降低内存开销
inline char* Arena::Allocate(size_t bytes) {// The semantics of what to return are a bit messy if we allow// 0-byte allocations, so we disallow them here (we don't need// them for our internal use).assert(bytes > 0);if (bytes <= alloc_bytes_remaining_) {char* result = alloc_ptr_;alloc_ptr_ += bytes;alloc_bytes_remaining_ -= bytes;return result;}return AllocateFallback(bytes);
}// 当 当前块内存不足的时候
char* Arena::AllocateFallback(size_t bytes) {if (bytes > kBlockSize / 4) {// Object is more than a quarter of our block size.  Allocate it separately// to avoid wasting too much space in leftover bytes.char* result = AllocateNewBlock(bytes);return result;}// We waste the remaining space in the current block.alloc_ptr_ = AllocateNewBlock(kBlockSize);alloc_bytes_remaining_ = kBlockSize;char* result = alloc_ptr_;alloc_ptr_ += bytes;alloc_bytes_remaining_ -= bytes;return result;
}char* Arena::AllocateAligned(size_t bytes) {const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;// 判断一个指针需要多少字节? 从而在不同的机器上进行内存对齐static_assert((align & (align - 1)) == 0,"Pointer size should be a power of 2");size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align - 1);size_t slop = (current_mod == 0 ? 0 : align - current_mod);size_t needed = bytes + slop;char* result;if (needed <= alloc_bytes_remaining_) {result = alloc_ptr_ + slop;alloc_ptr_ += needed;alloc_bytes_remaining_ -= needed;} else {// AllocateFallback always returned aligned memoryresult = AllocateFallback(bytes);}assert((reinterpret_cast<uintptr_t>(result) & (align - 1)) == 0);return result;
}

Table

Table的本质就是对应磁盘文件中的sstable,是由多个 Block组成,组成的Block主要有以下部分

  1. 数据块(Data blocks)- 存储实际的键值对
  2. 索引
  3. 元数据块
  4. 过滤器块
  5. 页脚

Table的交互逻辑主要有两个部分

  1. TableBuilder:写入逻辑
  2. Table:读取逻辑

Table 对外提供服务场景

Table 抽象主要有三种场景会调用当前的Table,写入压缩读取

写入场景

  • 用户写请求 → DBImpl::Write → MemTable填充 → CompactMemTable → BuildTable → TableBuilder
/*** 压缩内存表,将不可变内存表压缩为新的表文件,并更新版本集。*/
void DBImpl::CompactMemTable() {....//***************************************************************************************************Status s = WriteLevel0Table(imm_, &edit, base);// 写入一个新的 Level0表文件, 更新edit 将内存表转换成SSTable
// ***************************************************************************************************...
} 
/** 这个方法用来写入Level0等级的表*/
Status DBImpl::WriteLevel0Table(MemTable* mem, VersionEdit* edit,Version* base) {// ...
// ************************************************************************************************s = BuildTable(dbname_, env_, options_, table_cache_, iter, &meta);// 通过BuildTable方法构建 Table对象
// ************************************************************************************************
// ...
}

如上代码 先调用CompactMemTable方法,然后调用WriteLevel0Table方法,接着调用BuildTable方法

压缩场景

  • 后台Compaction → DoCompactionWork → BuildTable → TableBuilder

读取场景

读取场景主要被用在TableCache中的Table::InternalGet方法

Status TableCache::Get(const ReadOptions& options, uint64_t file_number,uint64_t file_size, const Slice& k, void* arg,void (*handle_result)(void*, const Slice&,const Slice&)) {Cache::Handle* handle = nullptr;Status s = FindTable(file_number, file_size, &handle);if (s.ok()) {Table* t = reinterpret_cast<TableAndFile*>(cache_->Value(handle))->table;// **************************************************************************************************s = t->InternalGet(options, k, arg, handle_result); // Table对外暴露方法// **************************************************************************************************cache_->Release(handle);}return s;
}
  • 用户读请求 → DBImpl::Get → Version::Get → TableCache → Table::InternalGet

Table写入逻辑-TableBuilder

在这里插入图片描述
写入细节

  1. 输入处理:方法接收一对 key-value 作为输入
  2. 索引块处理:
  • 检查是否有待处理的索引条目(pending_index_entry)
  • 如果有,使用比较器找到上一个 key 和当前 key 的最短分隔符
  • 将分隔符和对应的 handle 编码后添加到索引块中
  1. 过滤块处理:
  • 如果过滤块存在,则调用 filter_block->AddKey(key) 将 key 添加到过滤块
  1. 数据块处理:
  • 更新 last_key 和条目计数 num_entries
  • 将 key-value 对添加到数据块
  • 估计当前数据块大小,如果超过设定阈值,调用 Flush() 方法
  1. Flush() 会将当前数据块写入 SSTable 文件

源代码

void TableBuilder::Add(const Slice& key, const Slice& value) {Rep* r = rep_;// rep 是否close了assert(!r->closed);if (!ok()) return;if (r->num_entries > 0) {assert(r->options.comparator->Compare(key, Slice(r->last_key)) > 0);}// 处理 待处理的索引条目if (r->pending_index_entry) {assert(r->data_block.empty());// 数据块 是空的r->options.comparator->FindShortestSeparator(&r->last_key, key);// 找到 最后一个key 和当前key的最短分隔符std::string handle_encoding;r->pending_handle.EncodeTo(&handle_encoding);r->index_block.Add(r->last_key, Slice(handle_encoding));// 索引块添加 r->pending_index_entry = false;}// if (r->filter_block != nullptr) {r->filter_block->AddKey(key);// 添加过滤块}// 将string类型的数据 重新设置r->last_key.assign(key.data(), key.size());r->num_entries++;// r->data_block.Add(key, value);//  数据块添加const size_t estimated_block_size = r->data_block.CurrentSizeEstimate();if (estimated_block_size >= r->options.block_size) {//判断块大小Flush();// 调用Flush方法 把数据写入到内存中}
}

Table读取逻辑-Table

Table.InternalGet 方法,调用Get方法来读取Table的内容
在这里插入图片描述
查询细节

  1. 输入的 Key 首先进入索引块,通过 iiter->Seek(k) 定位可能包含目标键的数据块
  2. 通过布隆过滤器的 KeyMayMatch 快速判断键是否可能存在
  3. 最后在数据块中通过 block_iter->Seek(k) 精确查找键值对
  4. 如果找到目标键值对,调用 handle_result(arg, key, value) 处理结果

源代码

Status Table::InternalGet(const ReadOptions& options, const Slice& k, void* arg,void (*handle_result)(void*, const Slice&,const Slice&)) {Status s;// 创建索引块的迭代器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;}}if (s.ok()) {s = iiter->status();}delete iiter;return s;
}

Block

主要就是为了支撑 Table的检索,本质上 Table是对外提供了一个sstable的可用抽象,而block是对sstable做更细粒度的管理。

Block从写入写出流程来看,分为两个部分

  1. Block: 读取流程
  2. BlockBuilder: 写入流程

Block 的读取流程

Block 数据结构和查找流程:
┌─────────────────────────────────── Block ────────────────────────────────────┐
│                                                                              │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐                 │
│  │ Restart │ │ Restart │ │ Restart │ │ Restart │ │ Restart │   数据区域       │
│  │ Point 0 │ │ Point 1 │ │ Point 2 │ │ Point 3 │ │ Point 4 │             │
│  └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘             │
│       │           │           │           │           │                     │
│       ▼           ▼           ▼           ▼           ▼                     │
│   ┌─────────┬─────────┬─────────┬─────────┬─────────┐                     │
│   │  数据1  │  数据2  │  数据3  │  数据4  │  数据5  │                     │
│   └─────────┴─────────┴─────────┴─────────┴─────────┘                     │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘查找过程示意 (假设查找 "apple"):1. 初始化搜索范围:
left = 0                right = 4│                       │▼                       ▼
┌─────┬─────┬─────┬─────┬─────┐
│  01234  │
└─────┴─────┴─────┴─────┴─────┘2. 二分查找过程:
第一次迭代:
mid = (0 + 4 + 1) / 2 = 2┌────── mid ──────┐│                 ▼
┌─────┬─────┬─────┬─────┬─────┐
│  01234  │
└─────┴─────┴─────┴─────┴─────┘比较 mid_key 与 target:
如果 mid_key < target:  left = mid
如果 mid_key >= target: right = mid - 13. 定位到重启点后的线性查找:
┌─────────────── 重启点区域 ───────────────┐
│     ┌────► 线性查找                      │
│     │                                    │
├─────▼───┬──────────┬──────────┬─────────┤
│ entry 1 │ entry 2  │ entry 3  │ entry 4 │
└─────────┴──────────┴──────────┴─────────┘每个 Entry 的格式:
┌──────────┬───────────┬────────────┬──────┬───────┐
│  shared  │non_shared │value_length│  key │ value │
└──────────┴───────────┴────────────┴──────┴───────┘优化说明:
1. 当前位置优化:
┌─────────────────────┐
│ if (Valid()) {      │
│   使用当前位置作为  │ 
│   查找起点          │
└─────────────────────┘2. 跳过重复查找优化:
┌─────────────────────┐
│ skip_seek 条件:     │
│ - 目标在当前区块    │
│ - 当前key较小       │
└─────────────────────┘

查找流程说明

  • 首先检查当前位置,如果已经有效,可能用作查找起点
  • 在重启点数组中进行二分查找,找到最后一个小于目标的重启点
  • 从找到的重启点开始,进行线性查找直到找到第一个大于等于目标的键
  • 使用优化机制避免不必要的查找操作
    关键优化点
  • 利用当前位置作为可能的起点
  • 二分查找快速定位重启区间
  • 通过 skip_seek 优化避免重复查找
  • 结合重启点和线性查找平衡查询效率

源代码

  void Seek(const Slice& target) override {// Binary search in restart array to find the last restart point// with a key < targetuint32_t left = 0;uint32_t right = num_restarts_ - 1;int current_key_compare = 0;if (Valid()) {// If we're already scanning, use the current position as a starting// point. This is beneficial if the key we're seeking to is ahead of the// current position.current_key_compare = Compare(key_, target);if (current_key_compare < 0) {// key_ is smaller than targetleft = restart_index_;} else if (current_key_compare > 0) {right = restart_index_;} else {// We're seeking to the key we're already at.return;}}while (left < right) {uint32_t mid = (left + right + 1) / 2;uint32_t region_offset = GetRestartPoint(mid);uint32_t shared, non_shared, value_length;const char* key_ptr =DecodeEntry(data_ + region_offset, data_ + restarts_, &shared,&non_shared, &value_length);if (key_ptr == nullptr || (shared != 0)) {CorruptionError();return;}Slice mid_key(key_ptr, non_shared);if (Compare(mid_key, target) < 0) {// Key at "mid" is smaller than "target".  Therefore all// blocks before "mid" are uninteresting.left = mid;} else {// Key at "mid" is >= "target".  Therefore all blocks at or// after "mid" are uninteresting.right = mid - 1;}}// We might be able to use our current position within the restart block.// This is true if we determined the key we desire is in the current block// and is after than the current key.assert(current_key_compare == 0 || Valid());bool skip_seek = left == restart_index_ && current_key_compare < 0;if (!skip_seek) {SeekToRestartPoint(left);}// Linear search (within restart block) for first key >= targetwhile (true) {if (!ParseNextKey()) {return;}if (Compare(key_, target) >= 0) {return;}}}

Block 的写入流程

在这里插入图片描述
主要就是使用前缀压缩的方式减少存储的压力,具体的细节如下

键值对存储格式示意图:
┌──────────────────────────────────────────────────────────────────┐
│                        Block Buffer                              │
└──────────────────────────────────────────────────────────────────┘示例: 添加两个键值对
1. 添加 "apple" -> "red"
┌─────────┬────────────┬────────────┬───────┬───────┐
│shared=0 │non_shared=5│value_size=3"apple""red" │
└─────────┴────────────┴────────────┴───────┴───────┘2. 添加 "apply" -> "blue" ("apple"共享"ap")
┌─────────┬────────────┬────────────┬───────┬────────┐
│shared=2 │non_shared=3│value_size=4"ply""blue"  │
└─────────┴────────────┴────────────┴───────┴────────┘前缀压缩过程示意:
"apple" -> "red"
┌────────────────────┐
│ apple     -> red   │ (完整存储,因为是重启点)
└────────────────────┘"apply" -> "blue"
┌────────────────────┐
│ ap[共享] + ply     │ (存储时只需要存储非共享部分"ply")
└────────────────────┘数据结构状态:
counter_ : 计数器,到达 block_restart_interval 时重置
┌───────┐
│   1(添加"apple")
└───────┘
┌───────┐
│   2(添加"apply")
└───────┘重启点数组 (restarts_):
┌───────┐
│   0(第一个键的位置)
└───────┘buffer_ 实际存储格式:
┌──────┬───────┬──────┬───────┬────┬──────┬───────┬──────┬────┬────────┐
│shared│non_sh.│val_sz│ key   │value│shared│non_sh.│val_sz│key │ value  │
│  053"apple""red"234"ply""blue" │
└──────┴───────┴──────┴───────┴────┴──────┴───────┴──────┴────┴────────┘

源代码

void BlockBuilder::Add(const Slice& key, const Slice& value) {Slice last_key_piece(last_key_);// assert(!finished_);assert(counter_ <= options_->block_restart_interval);assert(buffer_.empty()  // No values yet?|| options_->comparator->Compare(key, last_key_piece) > 0);// size_t shared = 0;if (counter_ < options_->block_restart_interval) {const size_t min_length = std::min(last_key_piece.size(), key.size());while ((shared < min_length) && (last_key_piece[shared] == key[shared])) {shared++;}} else {// Restart compressionrestarts_.push_back(buffer_.size());counter_ = 0;}// const size_t non_shared = key.size() - shared;// Add "<shared><non_shared><value_size>" to buffer_PutVarint32(&buffer_, shared);PutVarint32(&buffer_, non_shared);PutVarint32(&buffer_, value.size());// Add string delta to buffer_ followed by valuebuffer_.append(key.data() + shared, non_shared);buffer_.append(value.data(), value.size());// Update statelast_key_.resize(shared);last_key_.append(key.data() + shared, non_shared);assert(Slice(last_key_) == key);counter_++;
}

猜你喜欢

C++多线程: https://blog.csdn.net/luog_aiyu/article/details/145548529
一文了解LevelDB数据库读取流程:https://blog.csdn.net/luog_aiyu/article/details/145946636
一文了解LevelDB数据库写入流程:https://blog.csdn.net/luog_aiyu/article/details/145917173

PS

你的赞是我很大的鼓励
欢迎大家加我飞书扩列, 希望能认识一些新朋友~
二维码见: https://www.cnblogs.com/DarkChink/p/18598402

相关文章:

[LevelDB]关于LevelDB存储架构到底怎么设计的?

本文内容组织形式 LevelDB 存储架构重要特点总体概括LevelDB中内存模型MemTableMemTable的数据结构背景&#xff1a;SkipListSkiplist的数据结构 Skiplist的数据访问细节 SkipList的核心方法Node细节源代码 MemTable的数据加速方式Iterator 的核心方法 MemTable 的读取&写入…...

深入解析 React Diff 算法:原理、优化与实践

深入解析 React Diff 算法&#xff1a;原理、优化与实践 1. 引言 React 作为前端领域的标杆框架&#xff0c;采用 虚拟 DOM&#xff08;Virtual DOM&#xff09; 来提升 UI 更新性能。React 的 Diff 算法&#xff08;Reconciliation&#xff09; 是虚拟 DOM 运行机制的核心&a…...

【从零开始】Air780EPM的LuatOS二次开发——OneWire协议调试注意事项!

当涉及到与传感器、执行器等外部设备交互时&#xff0c;OneWire协议的高效调试成为决定项目成败的关键环节。OneWire协议&#xff08;单总线协议&#xff09;以其仅需一根数据线即可实现设备通信的极简特性&#xff0c;被广泛应用于温度传感器、身份识别模块等场景。 一、LuatO…...

响应(Response)

在 Flask 中&#xff0c;视图函数可以返回多种类型的响应&#xff0c;例如字符串、HTML、JSON、文件等。Flask 提供了 make_response 函数&#xff0c;用于生成和自定义 HTTP 响应。 2.1 默认响应 默认情况下&#xff0c;视图函数返回的字符串会被 Flask 包装成一个 HTTP 响应…...

C++学习之云盘项目fastDFS

1.资料介绍 1.1 一些概念 1. 什么是服务器 硬件 : 一台配置高的电脑 软件 : 电脑必须有一个能够解析 http 协议的软件 2. 常见的 Web 服务器 tomcat 服务器 apache 组织产品 , 开源的免费服务器 weblogic 服务器 bea 公司 , 收费的服务器 不交费 , 访问量受限…...

使用vue3+el-form实现动态新增名称,值,并对名称进行必填校验

使用vue3el-form实现动态新增名称&#xff0c;值&#xff0c;并对名称进行必填校验 效果图 代码 <template><el-form :model"form" :rules"rules" ref"dynamicForm"><!-- 动态添加的名称和值 --><el-row v-for"(ite…...

Spring Boot 集成高德地图电子围栏

摘要&#xff1a;本文手把手教你通过 Spring Boot 调用高德地图 API 实现电子围栏功能&#xff0c;涵盖云端围栏创建、设备位置监控与本地算法校验&#xff0c;附带完整代码和避坑经验&#xff01; 一、电子围栏核心原理 1.1 什么是电子围栏&#xff1f; 虚拟地理边界&#x…...

3.JVM-内部结构

栈结构 动态链接 栈中的对象指向堆中的实际引用 符号引用: 比如一个类的名称 直接引用: 具体堆中数据信息 方法返回 栈中上一层的结果和下一层的指令 操作数栈 局部变量 该线程中需要的变量 PC计数器 程序计数器:存当前执行到那一步 操作数栈里面将计算完之后的结果推入局…...

Spring 框架中常用注解和使用方法

Spring 框架中常用注解的详细解释与应用场景&#xff0c;结合核心功能和实际开发需求进行分类说明&#xff1a; 1.组件定义注解 1.1 Component 作用&#xff1a;通用注解&#xff0c;将普通 Java 类标记为 Spring 管理的 Bean&#xff0c;由容器实例化和管理&#xff0c;相当…...

神策数据接入 DeepSeek,AI 赋能数据分析与智能运营

在 AI 技术迅猛发展的浪潮下&#xff0c;神策数据正在加速推进人工智能在数据分析和智能运营领域的深度应用。近日&#xff0c;神策数据宣布全面接入 DeepSeek&#xff0c;为企业客户带来更加智能化、高效的数据分析与智能运营服务。这一举措展现了神策数据在人工智能方向的探索…...

微软OneNote无法同步解决方案

目录 前言原因UWP特性 解决方案C***h注册表 参考链接 前言 假设有多台Windows电脑&#xff0c;最方便且免费的多设备笔记同步方案就是微软自家的OneNote&#xff0c;使用OneDrive自带的5G云存储。 但是在国内大陆的OneNote&#xff0c;经常会出现无法同步、同步失败&#xff1…...

一般机器学习有哪些算法?

传统的机器学习算法主要依赖统计学和优化方法&#xff0c;不依赖深层神经网络&#xff0c;通常具有较高的可解释性且适用于中小规模数据集。以下是经典的传统机器学习算法分类及代表性模型&#xff1a; 一、监督学习&#xff08;Supervised Learning&#xff09; 1. 回归&…...

RAGFlow部署与使用(开源本地知识库管理系统,包括kibana配置)

一、RAGFlow 简介 戳我访问RAGFlow RAGFlow 是一款基于深度文档理解构建的开源 RAG&#xff08;Retrieval-Augmented Generation&#xff09;引擎。它可以给我们搭建本地知识库&#xff0c;将用户的知识文档上传到RAGFlow后&#xff0c;通过文档切分、向量入库&#xff0c;在…...

STM32G070CBT6读写FLASH中的数据

向FLASH中写入数据函数 /*函数说明&#xff1a;向FLASH中写数据形参&#xff1a;addr-要写入数据的起始地址 data-准备写入数据 len-数据大小返回值&#xff1a;1-成功&#xff0c;0-失败 */ uint8_t FlashWriteData(uint64_t addr,uint8_t data[],size_t len) {uint32_t Fir…...

如何使用HACS一键集成米家与果家设备到HomeAssistant玩转智能家居

文章目录 前言1. 下载HACS源码2. 添加HACS商店3. 绑定米家设备 前言 各位科技潮人和智能家居发烧友们&#xff0c;是不是也梦想着把家里变成一个高科技的空间&#xff1f;有了群晖NAS这位得力助手&#xff0c;不仅存储空间大得吓人&#xff0c;还能通过Docker轻松安装各种应用…...

Flutter_学习记录_状态管理之GetX

1. 状态管理、Flutter Getx介绍 1.1 状态管理 通俗的讲&#xff1a;当我们想在多个页面&#xff08;组件/Widget&#xff09;之间共享状态&#xff08;数据&#xff09;&#xff0c;或者一个页面&#xff08;组件/Widget&#xff09;中的多个子组件之间共享状态&#xff08;数…...

DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加列宽调整功能,示例Table14_09自定义单元格的固定表头表格

前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之添加列宽调整功能,示例Table14_09自定义单元格…...

基于 Prometheus + Grafana 监控微服务和数据库

以下是基于 Prometheus Grafana 监控微服务和数据库的详细指南&#xff0c;包含架构设计、安装配置及验证步骤&#xff1a; 一、整体架构设计 二、监控微服务 1. 微服务指标暴露 Spring Boot 应用&#xff1a; xml <!-- 添加 Micrometer 依赖 --> <dependency>…...

文件解析漏洞

一&#xff0c;IIS解析漏洞 1&#xff0c;IIS6.X 目录解析 在iis的⽹站根⽬录新建⼀个名为q.asp的⽂件&#xff0c;在q.asp中新建⼀个txt⽂件 在外部浏览器中访问windows2003的iis⽹站中的1.txt 发现asp代码被执⾏ 2&#xff0c;IIS6.X 畸形文件解析 在iis的⽹站根⽬录新建⼀…...

C++学习笔记(二十一)——文件读写

一、文件读写 作用&#xff1a; 文件读写指的是将数据从程序存储到文件&#xff0c;或从文件读取数据&#xff0c;以实现数据的持久化存储。 C 提供了 fstream 头文件&#xff0c;用于文件操作&#xff0c;主要包括&#xff1a; ofstream&#xff08;输出文件流&#xff09;—…...

Ubuntu上部署Flask+MySQL项目

一、服务器安装python环境 1、安装gcc&#xff08;Ubuntu默认已安装&#xff09; 2、安装python源码 wget https://www.python.org/ftp/python/3.13.2/Python-3.13.2.tar.xz 3、安装Python依赖库 4、配置python豆瓣源 二、服务器安装虚拟环境 1、安装virtualenv pip3.10 ins…...

Unity WebGL项目访问时自动全屏

Unity WebGL项目访问时自动全屏 打开TemplateData/style.css文件 在文件最下方添加红色框内的两行代码 使用vscode或者其他编辑器打开index.html 将按钮注释掉&#xff0c;并且更改为默认全屏...

C#RTSP代理推流程序

将不支持rtsp的相机通过rtspserver实现推流 主要功能 1. rtsp交互 2. udp推流 3. Bitmap转H264,RTP打包 4. 支持多路播放...

Redis--渐进式遍历

目录 一、引言 二、介绍 三、命令 四、总结 一、引言 本篇文章将介绍Redis中的渐进式遍历 二、介绍 一般使用keys * 来获取所有的key&#xff0c;但这样的操作如果数据量很大的时候&#xff0c;会将服务器给卡死&#xff0c;所以通过渐进式遍历&#xff0c;就会避免这个问题…...

PyTorch深度学习框架60天进阶学习计划 - 第23天:Transformer架构解析

让我继续完成机器翻译示例的代码&#xff1a; PyTorch深度学习框架60天进阶学习计划&#xff1a;第23天 Transformer架构解析 学习目标 推导自注意力机制数学公式详解位置编码的傅里叶基函数设计对比编码器-解码器结构的信息流动差异 1. Transformer架构概述 Transformer架…...

《C#上位机开发从门外到门内》3-4:基于TCP/IP的远程监控系统设计与实现

文章目录 一、项目概述二、系统架构设计三、通信协议设计四、功能模块实现五、系统安全性与稳定性六、性能优化与测试七、实际应用案例八、结论 随着信息技术的飞速发展&#xff0c;远程监控系统在工业自动化、智能家居、环境监测等领域的应用日益广泛。基于TCP/IP协议的远程监…...

【MySQL】MySQL审计工具Audit Plugin安装使用

MySQL审计工具Audit Plugin安装使用 https://www.cnblogs.com/waynechou/p/mysql_audit.html MySQL 5.6 开启审计功能 https://blog.51cto.com/u_15127556/4344503 MySQL之添加日志审计功能 https://blog.csdn.net/weixin_43279032/article/details/105507170 MySQL开启日志记录…...

Flutter 按钮组件 ElevatedButton 详解

目录 1. 引言 2. ElevatedButton 的基本用法 3. 主要属性 4. 自定义按钮样式 4.1 修改背景颜色和文本颜色 4.2 修改按钮形状和边框 4.3 修改按钮大小 4.4 阴影控制 4.5 水波纹效果 5. 结论 相关推荐 1. 引言 在 Flutter 中&#xff0c;ElevatedButton 是一个常用的…...

AndroidStudio+Android8.0下的Launcher3 导入,编译,烧录,调试

文章目录 编译完成搜索输出文件Android.mk配置gradle编译环境报错一报错二报错三输出文件下载INSTALL_FAILED_TEST_ONLY查找系统签名查找签名工具开始签名查看签名签名问题重新生成秘钥解决方案生成成功挽救错误:重新刷机更换testkey秘钥keystore生成keystoreINSTALL_FAILED_S…...

【差分约束】P5590 赛车游戏|省选-

本文涉及知识点 【数学 线性代数】差分约束 P5590 赛车游戏 题目描述 R 君和小伙伴打算一起玩赛车。但他们被老司机 mocania 骗去了秋名山。 秋名山上有 n n n 个点和 m m m 条边&#xff0c;R 君和他的小伙伴要从点 1 1 1 出发开往点 n n n&#xff0c;每条边都有一个…...

咪咕MG101_晨星MSO9380芯片_安卓5.1.1_免拆卡刷固件包

咪咕MG101_晨星MSO9380芯片_安卓5.1.1_免拆卡刷固件包&#xff08;内有教程&#xff09; 刷机教程简单说明&#xff1a; 1、把下载好的刷机包&#xff0c;U盘里建立一个upgrade文件夹&#xff0c;固件放入此文件夹里&#xff0c;放入U盘中&#xff0c;注意升级包为压缩包不要对…...

【软件工程】06_软件设计

6.1 软件设计概述 1. 软件设计的目标 软件设计的最基本目标就是回答 “概括地描述系统如何实现用户所提出来的功能和性能等方面的需求?” 这个问题。 软件设计的目标是根据软件需求分析的结果,设想并设计软件,即根据目标系统的逻辑模型确定目标系统的物理模型。包括软件体系…...

在Flutter中使用Future读取一个大文件会导致线程阻塞吗

目录 一、Future 与文件读取的机制 1. Dart 的异步 I/O 原理 2. 代码示例 二、什么情况下会阻塞主线程? 1. I/O 操作本身不会阻塞 2. 数据处理可能阻塞 3. 示例对比 三、如何避免阻塞主线程? 1. 将耗时操作移到 Isolate 2. 使用 compute 函数(简化 Isolate 操作)…...

2025-03-17 Unity 网络基础1——网络基本概念

文章目录 1 网络1.1 局域网1.2 以太网1.3 城域网1.4 广域网1.5 互联网&#xff08;因特网&#xff09;1.6 万维网1.7 小结 2 IP 地址2.1 IP 地址2.2 端口号2.3 Mac 地址2.4 小结 3 客户端与服务端3.1 客户端3.2 服务端3.3 网络游戏中的客户端与服务端 1 网络 ​ 在没有网络之前…...

2025-03-17 学习记录--C/C++-PTA 习题4-8 高空坠球

合抱之木&#xff0c;生于毫末&#xff1b;九层之台&#xff0c;起于累土&#xff1b;千里之行&#xff0c;始于足下。&#x1f4aa;&#x1f3fb; 一、题目描述 ⭐️ 习题4-8 高空坠球 皮球从某给定高度自由落下&#xff0c;触地后反弹到原高度的一半&#xff0c;再落下&…...

Java网络编程socket

一、UDP 特点&#xff1a; ① 用户数据报协议&#xff08;User Datagram Protocol&#xff09; ​ ② UDP是面向无连接通信协议 ​③ 速度快&#xff0c;一次只能传输64KB数据&#xff0c;数据不安全&#xff0c;容易丢失 &#xff08;1&#xff09;单播 一对一 客户端&#xf…...

蓝桥杯备赛 Day0_移动零

&#x1f388; 个人主页&#x1f449;&#xff1a;tbRNA-CSDN博客tbRNA-CSDN博客tbRNA-CSDN博客 &#x1f4af; 个人简介&#xff1a;在校大学生一枚&#x1f48b;. &#x1f60d; 希望我的文章对大家有着不一样的帮助&#xff0c;欢迎大家关注我&#xff0c;感谢大家的多多支持…...

Razor C# 变量

Razor C# 变量 引言 在ASP.NET MVC和Razor视图引擎中,变量是构建动态网页的基础。理解Razor C#变量的使用对于开发者来说至关重要。本文将详细介绍Razor C#变量的概念、类型、作用域以及如何在实际项目中有效使用它们。 一、Razor C# 变量的概念 Razor C# 变量是存储在Raz…...

产品更新丨谷云科技ETLCloud 3月更新速递

本月&#xff0c;我们的数据集成产品ETLCloud继续迎来多项更新&#xff0c;进一步提升系统的兼容性和用户体验。以下是本月更新的亮点内容&#xff1a; 新增10项功能组件&#xff0c;持续丰富产品易用性 聚水潭-奇门通用组件 新增聚水潭-奇门通用组件&#xff0c;帮助企业更…...

如何高效定位网络丢包问题?

引言 本期分享一个比较常见的网络问题--丢包。例如我们去ping一个网站&#xff0c;如果能ping通&#xff0c;且网站返回信息全面&#xff0c;则说明与网站服务器的通信是畅通的&#xff0c;如果ping不通&#xff0c;或者网站返回的信息不全等&#xff0c;则很可能是数据被丢包…...

gitlab将本地项目提交到远程dev分支

获取Git路径 首先从远程获取到git路径&#xff0c;将给的git地址进行克隆到本地文件&#xff1b; git clone http:************.git 按照git地址的文件路径将本地项目&#xff0c;拷贝到目标文件中 在该路径中&#xff0c;初始化命令&#xff1b; # 初始化项目 git init #…...

Linux命令学习使用列表

Linux命令学习使用列表 1 系统启动相关2 系统网络相关3 系统磁盘相关4 系统定时任务5 系统进程监控 1 系统启动相关 1.1 麒麟V10 sp3修改选择默认启动项 2 系统网络相关 2.1 Linux IP 配置  2.2 ping监测网络通信情况 3 系统磁盘相关 4 系统定时任务 5 系统进程监控 5.1 L…...

分布式锁: 并发时,redis如何避免删别人的锁

在使用Redis实现分布式锁的时候&#xff0c;如何避免在并发情况下误删别人的锁。首先&#xff0c;分布式锁的基本概念&#xff1a;是多个客户端在访问共享资源时&#xff0c;通过某种机制来确保同一时间只有一个客户端能持有锁。 Redis通常用SET命令加上NX选项来创建锁&#xf…...

解决 Jupyter Notebook 中本地模块修改不生效的问题

解决 Jupyter Notebook 中本地模块修改不生效的问题 问题原因 当你在 Jupyter Notebook 中导入本地目录的库&#xff0c;修改后重新运行 import 语句却发现修改没有生效&#xff0c;这是因为 Python 的模块缓存机制。Python 解释器会将已导入的模块缓存在 sys.modules 字典中…...

蓝桥杯嵌入式赛道复习笔记2(按键控制LED灯,双击按键,单击按键,长按按键)

硬件原理解释 这张图展示了一个简单的按键电路原理图&#xff0c;其中包含四个按键&#xff08;PB0、PB1、PB2、PB3、PA0&#xff09;&#xff0c;每个按键通过一个10kΩ的上拉电阻连接到VDD&#xff08;电源电压&#xff09;&#xff0c;并接地&#xff08;GND&#xff09;。 …...

简单爬虫--框架

简单爬虫 import requests import re import chardet# 模拟浏览器的请求头 headers {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" }# 发送 HTTP 请求获取百…...

游戏引擎学习第163天

我们可以在资源处理器中使用库 因为我们的资源处理器并不是游戏的一部分&#xff0c;所以它可以使用库。我说过我不介意让它使用库&#xff0c;而我提到这个的原因是&#xff0c;今天我们确实有一个选择——可以使用库。 生成字体位图的两种方式&#xff1a;求助于 Windows 或…...

多模态模型Orpheus,基于病理图像的乳腺癌复发风险智能评估工具|顶刊解读·25-03-17

小罗碎碎念 在医学领域&#xff0c;尤其是乳腺癌治疗方面&#xff0c;准确评估患者的复发风险至关重要。对于占乳腺癌很大比例的 HR/HER2 - 亚型患者&#xff0c;目前主要依靠 Oncotype DX 的复发评分&#xff08;RS&#xff09;来指导治疗决策。 然而&#xff0c;该检测存在…...

基于MapReduce的气候数据分析

标题:基于MapReduce的气候数据分析 内容:1.摘要 本文聚焦于基于MapReduce的气候数据分析。背景在于随着全球气候变化问题日益严峻&#xff0c;海量气候数据的高效处理和分析成为关键。目的是利用MapReduce技术对气候数据进行有效挖掘&#xff0c;以揭示气候变化规律和趋势。方…...

Spring 原生启动过程

Spring&#xff08;Spring Framework&#xff09;的原生启动过程&#xff0c;它主要涉及 ApplicationContext 的初始化、BeanFactory 的加载、Bean 的创建与依赖注入。下面详细解析&#xff1a; Spring 原生启动过程 Spring 本身不依赖 SpringApplication&#xff0c;其核心在…...