Git Product home page Git Product logo

skiplist-cpp's Introduction

👉 本项目为C++实现,想系统学习本项目,推荐 卡码网【kv存储引擎-CPP】实战课 👉 想了解Java版本,推荐 卡码网【kv存储引擎-Java】实战课

版权申明: 本项目为我(程序员Carl)的原创。引用本项目文章请注明出处,例如:转自 https://github.com/youngyangyang04/Skiplist-CPP。 发现恶意抄袭或搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境!

KV存储引擎

众所周知,非关系型数据库redis,以及levedb,rockdb其核心存储引擎的数据结构就是跳表。

本项目就是基于跳表实现的轻量级键值型存储引擎,使用C++实现。插入数据、删除数据、查询数据、数据展示、数据落盘、文件加载数据,以及数据库大小显示。

在随机写读情况下,该项目每秒可处理啊请求数(QPS): 24.39w,每秒可处理读请求数(QPS): 18.41w

项目中文件

  • main.cpp 包含skiplist.h使用跳表进行数据操作
  • skiplist.h 跳表核心实现
  • README.md 中文介绍
  • README-en.md 英文介绍
  • bin 生成可执行文件目录
  • makefile 编译脚本
  • store 数据落盘的文件存放在这个文件夹
  • stress_test_start.sh 压力测试脚本
  • LICENSE 使用协议

提供接口

  • insertElement(插入数据)
  • deleteElement(删除数据)
  • searchElement(查询数据)
  • displayList(展示已存数据)
  • dumpFile(数据落盘)
  • loadFile(加载数据)
  • size(返回数据规模)

存储引擎数据表现

插入操作

跳表树高:18

采用随机插入数据测试:

插入数据规模(万条) 耗时(秒)
10 0.316763
50 1.86778
100 4.10648

每秒可处理写请求数(QPS): 24.39w

取数据操作

取数据规模(万条) 耗时(秒)
10 0.47148
50 2.56373
100 5.43204

每秒可处理读请求数(QPS): 18.41w

项目运行方式

make            // complie demo main.cpp
./bin/main      // run 

如果想自己写程序使用这个kv存储引擎,只需要在你的CPP文件中include skiplist.h 就可以了。

可以运行如下脚本测试kv存储引擎的性能(当然你可以根据自己的需求进行修改)

sh stress_test_start.sh 

待优化

  • delete的时候没有释放内存 (我这里进行了优化,更改SkipList析构函数的代码,使得析构完全,还请各路大佬来指正)
  • 压力测试并不是全自动的
  • 跳表的key用int型,如果使用其他类型需要自定义比较函数,当然把这块抽象出来更好
  • 如果再加上一致性协议,例如raft就构成了分布式存储,再启动一个http server就可以对外提供分布式存储服务了

关于作者

大家好,我是程序员Carl,《代码随想录》作者,哈工大师兄,先后在腾讯和百度从事分布式技术研发。

skiplist-cpp's People

Contributors

mayuan6 avatar xhh0608 avatar youngyangyang04 avatar zashjie avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

skiplist-cpp's Issues

load_file() 中也有遗漏 delete 处

load_file() 里的 key 和 value 都是采用定义为指针然后 new 一个对象空间给它俩,但是函数最后都没有对这两个 string 指针进行 delete 操作,也是存在内存泄露的。

void SkipList<K, V>::load_file() {

    _file_reader.open(STORE_FILE);
    std::cout << "load_file-----------------" << std::endl;
    std::string line;
    std::string* key = new std::string();
    std::string* value = new std::string();
   ...
}

访问空跳表时,search会出错

感谢您的项目!我在调试的过程中,如果直接对空跳表进行search,程序会崩溃,调试发现可能的问题在这里:

template<typename K, typename V> 
bool SkipList<K, V>::search_element(K key) {

    std::cout << "search_element-----------------" << std::endl;
    Node<K, V> *current = _header;

    // start from highest level of skip list
    for (int i = _skip_list_level; i >= 0; i--) {
        while (current->forward[i] && current->forward[i]->get_key() < key) {
            current = current->forward[i];
        }
    }

    //reached level 0 and advance pointer to right node, which we search
    current = current->forward[0];

    // if current node have key equal to searched key, we get it
    if (current and current->get_key() == key) {
        std::cout << "Found key: " << key << ", value: " << current->get_value() << std::endl;
        return true;
    }

    std::cout << "Not Found Key:" << key << std::endl;
    return false;
}

其中

current = current->forward[0];

使得getvalue会对空指针操作从而崩溃,修改一下这里应该就好啦


更新:非常抱歉,是我之前修改search代码时改动了返回值导致的错误,抱歉因为粗心打扰了。。。我的改动如下

template<typename K, typename V> 
V SkipList<K, V>::search_element(K key) {

    std::cout << "search_element-----------------" << std::endl;
    Node<K, V> *current = _header;

    // start from highest level of skip list
    for (int i = _skip_list_level; i >= 0; i--) {
        while (current->forward[i] && current->forward[i]->get_key() < key) {
            current = current->forward[i];
        }
    }

    //reached level 0 and advance pointer to right node, which we search
    current = current->forward[0];

    // if current node have key equal to searched key, we get it
    if (current and current->get_key() == key) {
        std::cout << "Found key: " << key << ", value: " << current->get_value() << std::endl;
        return current->get_value();
    }

    std::cout << "Not Found Key:" << key << std::endl;
    return current->get_value();
}

所以卡哥的原代码是没有问题的。。是我修改的疏忽。。。抱歉抱歉!

/

如果要保证线程安全,那么所有的读写方法都应该加锁。可使用rwlock来保证线程安全,同时减少竞争锁的成本。更加细粒度的方法则是在每一个level的linkedlist head上加锁,减少一个线程持有锁的时间。以上为个人看法。

对于优化项目中内存无法delete的情况的修改

template<typename K, typename V>
Node<K, V>::~Node() {
delete []forward;
};
开始之前先叠个甲,我比较菜,如果所言有差错,还望各路大神指正。
这里这个析构函数中delete这个二维数组的操作在我个人看来是会有内存泄漏的风险的。我查阅了一些底层的原理,发现delete并不会将这个二维数组彻底删除。下面是我个人在vs上的调试过程:
屏幕截图 2023-10-12 214757
这里我使用同样的方法定义了一个二维数组,在删除时设置断点,首先我们来查看二维数组的地址。
image
在进行delete[]Array之后,我进行了对内存地址的搜索,发现,被删除的只有*Array之中的内容,但是原先分配给**Array之中的内存并没有被删除:
在进行删除之前,查阅内存:
image
image
在进行删除之后,再进行查阅:
image
image
image
我们会发现确实有内存是没有删干净的。
根据我个人的理解,我认为这个过程可以使用下面的图来表示:
image
image
综上,我认为这边一段代码应该修改为:
template<typename K, typename V>
Node<K, V>::~Node() {
for(int i=0;i<level+1;i++){
delete []forward[i];
}
delete[] forward;
};
另外上面的图片有一张搜索的内存上面运行的图片里没有,那是因为我运行了两次,都是选的Array[][]中的内存地址来查询,是没有问题的。
如有错误希望大神可以耐心指正,让我知道我是哪里知识有欠缺>_<

c++11重构

先自我介绍,我是去年转行学习c++,做的第一个项目就是学习的Carl师兄的skip_list,一晃眼已经毕业半年,如今在某中厂搬砖。回头载看到github上做的项目感慨万千,这一路走来是异常艰辛,感谢Carl师兄的项目,感谢互联网的开放和资源。

这是渊源。

搬砖间隙,用c++11重构了当初的项目,希望在这段寒冷的时间里大家都能生活得好。地址附如下:

https://github.com/xiao-yang25/MySkipList

压测问题

请问为什么写操作反而会比读操作快呢

师兄,发现几个问题

  1. skiplist.h 中的 get_random_level() 函数中的 n 的初始值应该是 0 不应该是 1。
template<typename K, typename V>
int SkipList<K, V>::get_random_level(){

    int k = 1;   // 这个地方应该是 0 
    while (rand() % 2) {
        k++;
    }
    k = (k < _max_level) ? k : _max_level;
    return k;
};

如果 k 的初始值是1的话,那么插入的节点的 level 都是大于等于1的,那么在整个跳表中,第0层的索引和第1层的索引就完全一样没有区分度了。
2. skiplist.h 中的 delete_element() 中,只是把节点从指针链表中删除了,但实际的节点没有删除,会造成内存泄漏

    delete current;    // 应该有删除本节点的语句
    _element_count --;
  1. skiplist.h 中的 ~SkipList() 中,也只删除了一下头节点,本跳表中的其他节点也都没有进行删除
template<typename K, typename V> 
SkipList<K, V>::~SkipList() {

    if (_file_writer.is_open()) {
        _file_writer.close();
    }
    if (_file_reader.is_open()) {
        _file_reader.close();
    }

    // 此处为所添加的部分 循环删除所有节点
    Node<K, V>* current = _header->forward[0];
    while(current) {
        Node<K, V>* tmp = current->forward[0];
        delete current;
        current = tmp;
    }

    delete _header;
}
  1. 另外有一处小优化的地方,在 delete_element() 里,进行索引数组更新的地方
        // start for lowest level and delete the current node of each level
        for (int i = 0; i <= _skip_list_level; i++) {

            // if at level i, next node is not target node, break the loop.
            if (update[i]->forward[i] != current) 
                break;

            update[i]->forward[i] = current->forward[i];
        }

可以改为

    for (int i = current->node_level; i >= 0; --i) {
        update[i]->forward[i] = current->forward[i];
    }

因为从被删除节点的 node_level 可以得到它的层级,那么在 i<=node_level 的层级上,一定满足原来代码中的 update[i]->forward[i] == current 的条件。

师兄,这些是我个人见解,有不对的地方帮我指正出来吧

细节疑问

  1. insert_element函数中,此处第二个if是否可以直接修改为else
    if (current != NULL && current->get_key() == key) { //... }
    if (current == NULL || current->get_key() != key) { //... }
  2. delete_element处没有释放节点内存
  3. skiplist的析构函数也没有释放头节点外其它节点的内存

关于压力测试的疑问

程序中很大一部分时间浪费在了多余的控制台输出,如果把这些注释掉,测出的结果要好上不少。

%forward 数组越界访问

Node 节点的 %forward 变量的内存是构造函数中分配的,且大小并不是最大值 %_max_level。
插入节点时,特别是当 %random_level 大于 %_skip_list_level,更新路径上各节点 %forward 时,并没有重新分配内存,而是直接下标操作,看起来有越界的问题?
(各节点 %forward 数组长度一定是小于等于 %_skip_list_level 的)

您好,我想请教一下代码有关代码线程安全相关的内容

在class SkipList 中的 insert_elementdelete_element中,开头的一行使用 mtx.lock(); 尝试给数据结构上了锁,不过作为查找的 search_element 却没有上锁。这么做有个问题,虽然它避免了写-写数据竞争,但是没有避免读-写数据竞争,我并不确定在多线程环境下这样依然是安全的,例如某一线程在free的的时候另一线程仍然在读以及使用指针。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.