LSM引擎:SkipList的初步实现

SkipList介绍

  1. 跳表是一个多级的链表,在最低一级是完整的链表,级数越高每一级的节点数相应减少,同一个节点可以在上下级之间转换,前提是不超出该节点能表示的层级数,这样遍历时通过高级跳表一次就可以跳过多个节点从而减少查找某个节点所需要的时间,此外跳表节点又可以按照一定的顺序排序,这样也便于后续实现范围查询。
  2. 在LSM Tree中跳表作为内存存储数据的数据结构,每个跳表节点将存储一个键值对,其中当value为空字符串时认为对应的key在逻辑上被删除,区别于key在物理上不存在于跳表,为了方便前后遍历和管理跳表,我们实现为带表头的双向跳表。

初步实现

  • 实现跳表节点
    • 跳表节点SkipListNode是实际的存储单元,其中包含键值对和前向指针forward与后向指针backward的数组,这种设计便于后续在同一节点的不同层级间进行跳转,为防止出现智能指针的循环引用,后向指针用std::weak_ptr实现,需要说明的是这里的forward相当于next而backward相当于prev
struct SkipListNode {
  std::string key_;
  std::string value_;
  std::vector<std::shared_ptr<SkipListNode>> forward;
  std::vector<std::weak_ptr<SkipListNode>> backward;
  // 其他代码
};
  • 实现跳表结构
    • 跳表SkipList用一个std::shared_ptr指针管理表头节点,用三个变量分别记录跳表的最大层数,当前层数以及字节数,并拥有c++随机数生成的成员用于后续跳表节点的插入
class SkipList {
public:
  // 其他代码

private:
  std::shared_ptr<SkipListNode> head_;
  int curr_level_;
  int max_level_;
  size_t size_bytes_;

  int random_level();
  std::mt19937 gen_;
  std::uniform_int_distribution<int> dist_;
};
  • 实现主要API
    • 实现Put
    1. 在跳表中插入一个节点需要首先找到该节点的插入位置,由于链表的插入特性,我们在实现中选择用插入位置前的第一个节点指示节点的插入,由于跳表具有多层级,我们需要用一个update数组来指向所有需要插入层级的前驱节点。
    2. 新节点需要通过随机方式获取一个层级数,层级数越大获取到的概率越小,这样是为了让跳表高层级能跨过节点尽量多以提高遍历效率,新节点层数大于跳表当前节点层数时需要更新跳表层数。
    3. 查找过程从表头节点开始,比较key与节点key的大小关系,在key大于节点key时在同一级向后继续遍历,否则如果当前级遍历完或者key不大于节点key,则需要向下缩小步长进入更低层级搜索。
    4. 查找结束后如果存在目标值只需要更新其value并修改size_bytse,若不存在该目标值则需要依照update数组的指示将待插入节点层层插入到跳表中,同样需要更新size_bytes。
void SkipList::Put(const std::string &key, const std::string &value) {
  if (head_ == nullptr) {
    throw std::runtime_error("SkipList not initialized: head_ is null");
  }
  int new_level = random_level();
  auto new_node = std::make_shared<SkipListNode>(key, value, new_level);
  std::vector<std::shared_ptr<SkipListNode>> update(max_level_, nullptr);

  // 查找元素是否存在同时记录插入位置
  auto curr = head_;
  for (int idx = curr_level_ - 1; idx >= 0; --idx) {
    while (curr->forward[idx] != nullptr && curr->forward[idx]->key_ < key) {
      curr = curr->forward[idx];
    }
    if (idx < new_level) {
      update[idx] = curr;
    }
  }

  // 找是不是目标节点
  curr = curr->forward[0];
  if (curr != nullptr && curr->key_ == key) {
    size_bytes_ += value.size() - curr->value_.size();
    curr->value_ = value;
    return;
  }

  // 没有目标节点,需要插入
  if (new_level > curr_level_) {
    // 超出curr_level_的部分前驱节点是表头节点
    for (int idx = curr_level_; idx < new_level; ++idx) {
      update[idx] = head_;
    }
    curr_level_ = new_level;
  }

  // 按照层次从低到高插入
  for (int idx = 0; idx < new_level; ++idx) {
    new_node->forward[idx] = update[idx]->forward[idx];
    update[idx]->forward[idx] = new_node;
    new_node->SetBackWard(update[idx], idx);
    if (new_node->forward[idx] != nullptr) {
      new_node->forward[idx]->SetBackWard(new_node, idx);
    }
  }

  size_bytes_ += key.size() + value.size();
}
  • 实现Remove(函数在Engine中不被调用,主要用于验证跳表实现的正确性)
  1. 在跳表中删除一个节点和插入类似,同样需要记录前驱节点数组update用于节点删除,搜索逻辑和插入中一致,需要注意的是在删除过程可能需要减小跳表层数。
// 删除节点,实际上LSM Engine的删除是通过Put(key, "")来实现的
void SkipList::Remove(const std::string &key) {
  if (head_ == nullptr) {
    throw std::runtime_error("SkipList not initialized: head_ is null");
  }

  std::vector<std::shared_ptr<SkipListNode>> update(max_level_, nullptr);

  auto curr = head_;
  for (int idx = curr_level_ - 1; idx >= 0; --idx) {
    while (curr->forward[idx] != nullptr && curr->forward[idx]->key_ < key) {
      curr = curr->forward[idx];
    }
    update[idx] = curr;
  }

  curr = curr->forward[0];
  if (curr == nullptr || curr->key_ != key) {
    return;
  }

  for (int idx = 0; idx < curr_level_; ++idx) {
    if (update[idx]->forward[idx] != curr) {
      break;
    }

    update[idx]->forward[idx] = curr->forward[idx];
    if (update[idx]->forward[idx] != nullptr) {
      update[idx]->forward[idx]->SetBackWard(update[idx], idx);
    }
  }

  size_bytes_ -= curr->key_.size() + curr->value_.size();
  while (curr_level_ > 1 && head_->forward[curr_level_ - 1] == nullptr) {
    --curr_level_;
  }
}
  • 实现Get
  1. 由于我们规定用value空字符作为逻辑上的删除标记,物理上不存在的值同样需要表示,因此这里用std::optional包装函数返回值,如果在表中没有发现key则返回std::nullopt,否则返回value即可。
  2. 其查询逻辑和Put,Remove一致,只需要找到理论上key节点存在的位置,然后判断节点key是否等于key即可知道是否物理上存在该节点。
// optional可同时处理key节点物理上不存在和key节点逻辑上被删除的两种情况
std::optional<std::string> SkipList::Get(const std::string &key) {
  if (head_ == nullptr) {
    throw std::runtime_error("SkipList not initialized: head_ is null");
  }

  auto curr = head_;
  for (int idx = curr_level_ - 1; idx >= 0; --idx) {
    while (curr->forward[idx] != nullptr && curr->forward[idx]->key_ < key) {
      curr = curr->forward[idx];
    }
  }

  curr = curr->forward[0];
  if (curr != nullptr && curr->key_ == key) {
    return std::make_optional(curr->value_);
  }
  return std::nullopt;
}
  • 实现随机层数
    这里通过C++ random提供的随机数生成器std::mt19337和随机数种子std::random_device生成随机数,用std::uniform_int_distribution (0,1)获取一个50%概率为1,50%概率为0的随机数,生成新层数时只需要在随机值为1时增加层数,为0时停止增加即可。
int SkipList::random_level() {
  int new_level = 1;
  while (new_level < max_level_ && dist_(gen_) == 1) {
    ++new_level;
  }
  return new_level;
}
  • 实现析构函数
    默认析构时首先回收表头结点的std::shared_ptr,由于引用计数为1,系统会回收头节点的内存空间,此时会销毁forward中的所有std::shared_ptr而这些std::shared_ptr又指向后继节点,会使后继节点的引用计数减为0,于是就需要继续调用节点的析构并回收空间,如果此时链表很长,并且在不考虑尾递归优化的情况下,跳表的回收可能会递归调用很多析构函数导致爆栈,因此需要将回收方式改为从表头节点开始逐节点释放。
SkipList::~SkipList() {
  while (head_ != nullptr) {
    auto next = head_->forward[0];
    head_->forward.clear();
    head_ = std::move(next);
  }
}
  • 实现跳表迭代器
    后续为了统一用迭代器处理各组件之间的交互,这里先初步实现一个跳表迭代器,后续可能进行优化。这里我的迭代器通过管理一个std::weak_ptr管理当前指向的跳表节点,这样设计的目的是符合跳表迭代器的特性,在节点被删除时迭代器失效,而节点插入时不会影响迭代器的正常使用。
// skiplist迭代器在指向的节点被删除后将失效
// 尽管在skiplist中应该不存在删除物理节点的情况
class SkipListIterator {
public:
  SkipListIterator(std::shared_ptr<SkipListNode> ptr = nullptr);

  SkipListIterator &operator++();
  bool operator==(const SkipListIterator &other) const;
  bool operator!=(const SkipListIterator &other) const;
  std::optional<std::pair<std::string, std::string>> Get() const;
  std::optional<std::string> GetKey() const;
  std::optional<std::string> GetValue() const;
  bool IsValid() const;
  bool IsEnd() const;

private:
  std::weak_ptr<SkipListNode> current_;
};

测试

#include <gtest/gtest.h>
#include <string>

#include "../include/skiplist/skiplist.h"
#include "../include/skiplist/skiplist_iterator.h"

TEST(SkipList, BasicOperation) {
  SkipList sl;
  // 验证正常插入
  sl.Put("key1", "value1");
  EXPECT_EQ("value1", sl.Get("key1"));
  // 验证正常覆盖
  sl.Put("key1", "value2");
  sl.Put("key1", "value3");
  EXPECT_EQ("value3", sl.Get("key1"));
  // 验证正常删除
  sl.Remove("key1");
  EXPECT_FALSE(sl.Get("key1").has_value());
}

TEST(SkipList, LargeScalePutAndGet) {
  int test_times = 100000;
  SkipList sl;

  // 测量大量数据
  for (int count = 0; count < test_times; ++count) {
    std::string key = "key" + std::to_string(count);
    std::string value = "value" + std::to_string(count);

    sl.Put(key, value);
  }

  for (int count = 0; count < test_times; ++count) {
    std::string key = "key" + std::to_string(count);
    std::string value = "value" + std::to_string(count);

    EXPECT_EQ(value, sl.Get(key));
  }
}

TEST(SkipList, LargeScaleRemove) {
  int test_times = 100000;
  SkipList sl;

  // 测量大量数据下删除正常
  for (int count = 0; count < test_times; ++count) {
    std::string key = "key" + std::to_string(count);
    std::string value = "value" + std::to_string(count);

    sl.Put(key, value);
  }

  for (int count = 0; count < test_times; ++count) {
    std::string key = "key" + std::to_string(count);

    sl.Remove(key);
  }

  for (int count = 0; count < test_times; ++count) {
    std::string key = "key" + std::to_string(count);

    EXPECT_FALSE(sl.Get(key).has_value());
  }
}

TEST(SkipList, BytesCount) {
  SkipList sl;

  std::string k1 = "key1", v1 = "value1";
  std::string k2 = "key2", v2 = "value2";

  sl.Put(k1, v1);
  sl.Put(k2, v2);
  int size = k1.size() + k2.size() + v1.size() + v2.size();
  EXPECT_EQ(size, sl.GetSize());

  sl.Put(k1, "value1.1");
  size += sizeof("value1.1") - 1 - v1.size();

  EXPECT_EQ(size, sl.GetSize());

  sl.Remove("key2");
  size -= k2.size() + v2.size();

  EXPECT_EQ(size, sl.GetSize());
}

TEST(SkipList, IteratorBasicOperation) {
  SkipList sl;

  auto begin = sl.Begin();
  auto end = sl.End();
  EXPECT_TRUE(begin == end);

  sl.Put("key1", "value1");
  begin = sl.Begin();
  end = sl.End();
  auto kv = begin.Get();
  EXPECT_EQ(kv.value().first, "key1");
  EXPECT_EQ(kv.value().second, "value1");
  EXPECT_FALSE(begin == end);
  EXPECT_TRUE(begin.IsValid());
  EXPECT_FALSE(begin.IsEnd());

  sl.Remove("key1");
  kv = begin.Get();
  EXPECT_EQ(kv, std::nullopt);
  EXPECT_TRUE(begin == end);
  EXPECT_FALSE(begin.IsValid());
  EXPECT_TRUE(begin.IsEnd());
}

TEST(SkipList, IteratorTraversal) {
  SkipList sl;
  int test_times = 10;

  for (int count = 0; count < test_times; ++count) {
    std::string key = "key" + std::to_string(count);
    std::string value = "value" + std::to_string(count);

    sl.Put(key, value);
  }

  auto iterator = sl.Begin();
  auto end = sl.End();

  int count = 0;
  while (iterator != end) {
    std::string key = "key" + std::to_string(count);
    std::string value = "value" + std::to_string(count);
    EXPECT_EQ(iterator.GetKey(), key);
    EXPECT_EQ(iterator.GetValue(), value);
    ++count;
    ++iterator;
  }

  EXPECT_TRUE(iterator == end);
}

int main(int argc, char **argv) {
  testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}
posted @ 2026-08-26 13:30  lf123z  阅读(11)  评论(0)    收藏  举报