大白话解析LevelDB:MemTable

Memtable

LevelDB在写入一个key-value时, 不会直接将该key-value落盘。而是先将该key-value写入内存中的memtable, 当memtable的大小达到一定阈值时, 再将memtable整个落盘成一个sstable文件。

我们先来看下memtable的接口, 都提供了哪些功能:

class MemTable {
   public:
    
    explicit MemTable(const InternalKeyComparator& comparator);

    // 不允许拷贝和赋值
    MemTable(const MemTable&) = delete;
    MemTable& operator=(const MemTable&) = delete;

    // Increase reference count.
    void Ref() { ++refs_; }

    // Drop reference count.  Delete if no more references exist.
    void Unref() {
        --refs_;
        assert(refs_ >= 0);
        if (refs_ <= 0) {
            delete this;
        }
    }

    void Add(SequenceNumber seq, ValueType type, const Slice& key,
             const Slice& value);

    bool Get(const LookupKey& key, std::string* value, Status* s);


    size_t ApproximateMemoryUsage();

    Iterator* NewIterator();

   private:
    // ...
};

memtable有两类接口

  • 数据读写接口
    • Add: 添加一个key-valuememtable
    • Get: 从memtable中查找一个key对应的value
    • NewIterator: 生成一个memtable的迭代器
    • ApproximateMemoryUsage: 获取memtable的内存占用
  • 内存管理接口
    • Ref: 增加memtable的引用计数
    • Unref: 减少memtable的引用计数, 当引用计数为0时, 释放memtable的内存

先从简单的看起, 来看下Memtable的构造函数.

MemTable的构造函数

explicit MemTable(const InternalKeyComparator& comparator) \
    : comparator_(comparator), refs_(0), table_(comparator_, &arena_) {}

MemTable是一个有序集合, 数据始终是按照key的顺序排列的, 所以MemTable的构造函数中, 传入了一个InternalKeyComparator对象, 用于比较两个key的大小。
至于为什么是InternalKeyComparator, 而不是KeyComparator, 是因为Memtable需要确保每个Key的唯一性, 即使用户Add了多个相同Key, 但也会结合SequenceNumberValueType来确保每个Key的唯一性。

InternalKey = Sequence + Type + UserKey

除了comparator_, 构造函数中还会初始化refs_table_.
refs_代表该memtable的引用次数, 当refs_为0时, 该memtable将会被销毁。
table_是一个SkipList, 用于实际存储memtable中的key-value数据。
之所以为什么要将SkipList封装在memtable中, 而不是直接使用SkipList, 是为了灵活性. 抽象出一个Memtable, SkipList是其中一种实现. 用户如果有特殊需求, 可以将SkipList替换成其他的数据结构, 比如B+Tree等. SkipList的内容比较多,详情移步大白话解析LevelDB:SkipList(跳表)

explicit关键字的作用

至于为什么要将构造函数声明为explicit, 是为了防止隐式转换, 保证Memtable只能通过显式的方式来构造.
我们看个例子, 如果没有将构造函数声明为explicit, 有些错误就会因为隐式转换而变成合法的, 编译阶段无法发现错误.

class InternalKeyComparator {
  // ...
};

class MemTable {
 public:
  // 没有使用 explicit,允许隐式转换
  MemTable(const InternalKeyComparator& comparator) {
    // 构造 MemTable
  }
  // ...
};

void ProcessMemTable(MemTable mtable) {
  // 处理 MemTable
}

int main() {
  InternalKeyComparator comparator;
  // 不小心写错了, 直接把comparator传给了ProcessMemTable,
  // 但由于没有 explicit, 会隐式创建 MemTable 对象
  ProcessMemTable(comparator);  // 隐式转换
}

为什么MemTable不允许拷贝

还有两个MemTable的构造函数, 被delete禁用了, 也就是告诉其他人MemTable不允许拷贝和赋值.
为什么不允许拷贝MemTable对象呢? 是因为MemTable的内存空间是通过内部的Arena arena_来进行管理的.
Arena是一个内存池, 如果MemTable对象被拷贝了, 那么两个MemTable对象就会共享同一个Arena对象, 造成混乱。
MemTable若要支持拷贝, 需要将arena_深拷贝, 大大增加了实现的复杂度.

MemTable::Ref 和 MemTable::Unref

MemTable的构造讲完了, 现在来看下RefUnref这两个接口.
MemTable的内部有一个refs_变量, 用于记录当前MemTable的引用计数.
Ref方法就是将refs_加1, Unref方法就是将refs_减1, 并且当refs_为0时, 销毁MemTable对象本身.

class MemTable {
   public:
    // ...

    void Ref() { ++refs_; }

    void Unref() {
        --refs_;
        assert(refs_ >= 0);
        if (refs_ <= 0) {
            delete this;
        }
    }

    // ...
};

MemTable::Add 和 MemTable::Get

看下AddGet这两个接口分别用于往MemTable中添加和查找key-value.

MemTable::Add

Add接口接受4个参数, 分别是sequence, type, keyvalue.
将这4个参数编码为一个entry, 然后调用SkipList::Insert插入到skiplist中.
SkipList::insert的实现见SkipList::Insert

void MemTable::Add(SequenceNumber s, ValueType type, const Slice& key,
                   const Slice& value) {
    // MemTable::Add会将{key, value, sequence, type}编码为一个Entry, 然后插入到SkipList中 
    // MemTable Entry的格式如下:
    // |----------------------|----------------------------------|
    // | Field                | Description                      |
    // |----------------------|----------------------------------|
    // | key_size             | varint32 of internal_key.size()  |   <--- head
    // | internal_key bytes   | char[internal_key.size()]        |
    // | value_size           | varint32 of value.size()         |
    // | value bytes          | char[value.size()]               |
    // |----------------------|----------------------------------|
    // 
    // 其中, internal_key的格式如下:
    // |----------------------|----------------------------------|
    // | Field                | Description                      |
    // |----------------------|----------------------------------|
    // | user_key bytes       | char[user_key.size()]            |   <--- head
    // | sequence             | 7 Byte                           |
    // | type                 | 1 Byte                           |
    // |----------------------|----------------------------------|

    // 计算 key 和 value 的大小
    size_t key_size = key.size();
    size_t val_size = value.size();

    // InternalKey = Key + SequenceNumber(7B) + Type(1B)
    // 所以 InternalKey 的大小为 key_size + 8
    size_t internal_key_size = key_size + 8;
    
    // encoded_len是整个entry的大小
    const size_t encoded_len = VarintLength(internal_key_size) +
                               internal_key_size + VarintLength(val_size) +
                               val_size;
    
    // 从arena_中分配内存, 开辟entry的空间, 即buf
    char* buf = arena_.Allocate(encoded_len);

    // 在entry中写入internal_key_size
    char* p = EncodeVarint32(buf, internal_key_size);

    // 在entry中写入key
    std::memcpy(p, key.data(), key_size);
    p += key_size;

    // 在entry中写入sequence与type
    EncodeFixed64(p, (s << 8) | type);
    p += 8;

    // 在entry中写入value_size
    p = EncodeVarint32(p, val_size);

    // 在entry中写入value
    std::memcpy(p, value.data(), val_size);

    // 检查是否刚好将entry填满
    assert(p + val_size == buf + encoded_len);

    // 将entry插入skiplist
    table_.Insert(buf);
}

MemTable::Get

Get接口接受一个LookupKey对象, 用于查找key对应的value.

// LookupKey格式如下:
// |----------------------|-------------------------------------------------|
// | Field                | Description                                     |
// |----------------------|-------------------------------------------------|
// | internal_key_size    | varint32 of internal_key.size()                 |   <--- head
// | user_key bytes       | char[user_key.size()]                           |
// | sequence and type    | 8 bytes (SequenceNumber(7B) and ValueType(1B))  |
// |----------------------|-------------------------------------------------|
bool MemTable::Get(const LookupKey& key, std::string* value, Status* s) {
    // memkey = internal_key_size + user_key + sequence&&type
    Slice memkey = key.memtable_key();

    // typedef SkipList<const char*, KeyComparator> Table;
    // iter是一个SkipList::Iterator.
    // 创建一个skiplist的iterator.
    Table::Iterator iter(&table_);

    // 把iter移动到memkey所在的位置.
    iter.Seek(memkey.data());

    // 如果找不到对应的memkey, 则返回false.
    // 这里其实可以写的简洁些:
    // if (!iter.Valid()) { return false; }
    if (iter.Valid()) {

        // entry的format在MemTable::Add()中有详细的描述.
        // 取出entry
        const char* entry = iter.key();

        // 取出entry的interal_key_size(key_length), 与user_key的指针(key_ptr)
        uint32_t key_length;
        const char* key_ptr = GetVarint32Ptr(entry, entry + 5, &key_length);

        // 检查一下iter seek到的key和lookupkey是不是同一个user_key.
        if (comparator_.comparator.user_comparator()->Compare(
                Slice(key_ptr, key_length - 8), key.user_key()) == 0) {
            
            // 把type(tag)取出来
            const uint64_t tag = DecodeFixed64(key_ptr + key_length - 8);
            switch (static_cast<ValueType>(tag & 0xff)) {

                // iter seek到的key是一个插入或者更新的key,
                // 把value取出来, return true
                case kTypeValue: {
                    Slice v = GetLengthPrefixedSlice(key_ptr + key_length);
                    value->assign(v.data(), v.size());
                    return true;
                }

                // iter seek到的key已经被标记为删除了
                // 将status设为NotFound, return true
                case kTypeDeletion:
                    *s = Status::NotFound(Slice());
                    return true;
            }
        }
    }
    return false;
}

为什么没有MemTable::Delete

小朋友, 你是否有个问号, 为什么MemTable只实现了AddGet, 没有Delete呢?

这是因为 LevelDB 使用的是基于日志结构合并树(Log-Structured Merge-tree,简称 LSM-tree)的存储机制,其对删除操作的处理与传统的键值存储系统有所不同。

  1. 删除操作的实现:

    • 在 LSM-tree 中,删除操作被视为一种特殊的插入操作。当你想删除一个键时,LevelDB 实际上会在 MemTable 中插入一个带有该键的特殊标记(称为墓碑标记,tombstone)。这个标记表示该键已被删除。也就是通过MemTable::Add插入一条kTypeDeletion类型的记录。
  2. 合并过程中的删除处理:

    • MemTable 被转换到 SSTable(排序字符串表)并进行合并(Compaction)时,带有墓碑标记的键会被用来从较低层的 SSTable 中删除相应的键值对。这意味着删除操作实际上是在合并过程中延迟处理的。
  3. 效率和空间考虑:

    • 通过这种方式处理删除操作,LevelDB 能够在不立即清理数据的情况下快速响应删除请求,同时在后台合并过程中有效地处理实际的数据删除,这样既提高了效率,又节省了存储空间。

MemTable除了AddGet, 还有一个NewIteratorApproximateMemoryUsage两个与数据读写相关的接口.

MemTable::NewIterator

NewIterator用于生成一个MemTable的迭代器, 用于遍历MemTable中的所有key-value.
实现非常简单, 构造一个MemTableIterator对象即可.
MemTableIterator其实就是把SkipList包装了一层. 具体实现见SkipList::Iterator的实现

Iterator* MemTable::NewIterator() { return new MemTableIterator(&table_); }

class MemTableIterator : public Iterator {
   public:
    explicit MemTableIterator(MemTable::Table* table) : iter_(table) {}

    MemTableIterator(const MemTableIterator&) = delete;
    MemTableIterator& operator=(const MemTableIterator&) = delete;

    ~MemTableIterator() override = default;

    bool Valid() const override { return iter_.Valid(); }
    void Seek(const Slice& k) override { iter_.Seek(EncodeKey(&tmp_, k)); }
    void SeekToFirst() override { iter_.SeekToFirst(); }
    void SeekToLast() override { iter_.SeekToLast(); }
    void Next() override { iter_.Next(); }
    void Prev() override { iter_.Prev(); }
    Slice key() const override { return GetLengthPrefixedSlice(iter_.key()); }
    Slice value() const override {
        Slice key_slice = GetLengthPrefixedSlice(iter_.key());
        return GetLengthPrefixedSlice(key_slice.data() + key_slice.size());
    }

    Status status() const override { return Status::OK(); }

   private:
    // MemTable::Table::Iterator = SkipList::Iterator
    MemTable::Table::Iterator iter_;
    std::string tmp_;  // For passing to EncodeKey
};

MemTable::ApproximateMemoryUsage

ApproximateMemoryUsage用于获取MemTable的内存占用, 是个预估值.
实际返回的是arena_的内存占用, 因为MemTable的所有内存都是通过arena_来分配的,
具体实现见Arena

size_t MemTable::ApproximateMemoryUsage() { return arena_.MemoryUsage(); }

至此, MemTable的实现就过完了.

  • 24
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
XGBoost(eXtreme Gradient Boosting)是一种非常流行的机器学习算法,它是一种梯度提升树模型。它的设计目标是提高其前身GBDT(Gradient Boosting Decision Tree)算法的性能和鲁棒性。 XGBoost使用的是一种特殊的决策树模型,称为CART(Classification and Regression Trees)。与传统的决策树不同,CART决策树在每个节点上进行分裂时,会使用一种称为泰勒展开的方法,来近似地找到最优分裂点。通过这种方法,XGBoost能够更精确地构建决策树模型,并提高预测的准确性。 XGBoost还通过引入正则化技术,如L1和L2正则化,来避免模型过拟合。正则化可以限制模型的复杂性,提高模型的泛化能力,并使得模型对噪音数据不敏感。 在训练过程中,XGBoost使用梯度提升算法,该算法通过迭代地训练多个决策树,并使用梯度下降法来优化模型的损失函数。在每一轮迭代中,XGBoost会根据之前模型的预测结果和真实标签之间的误差,调整每个样本的权重,并生成一个新的决策树。通过这种迭代优化的方式,XGBoost能够逐步提升模型的准确性。 此外,XGBoost还具备优化性能的功能。它使用一种称为并行化的技术,通过同时在多个处理器上训练多个决策树,来加快训练速度。另外,XGBoost还支持特征重要性评估,可以通过计算每个特征对模型的贡献度来帮助我们理解数据的特征重要性。 总之,XGBoost是一种非常强大的机器学习算法,它通过使用特殊的决策树模型、正则化技术、梯度提升算法和优化性能等方法,提高了模型的预测准确性和鲁棒性。它在很多数据竞赛和实际应用中都取得了出色的结果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值