模块合并
通过前面的章节,我们已经将本 K-V 存储引擎拆分成的多个模块分别都介绍完了,但是会有一部份的内容没有涉及到,例如在跳表插入和删除数据的过程中,没有进行加锁,在多线程的情况下,可能会出现数据不一致的问题等。
现在我们将之前所有模块合并,同时补齐那些没有涉及到的内容。
不过因为篇幅原因,会有一部份的函数实现会省略,可以到 GitHub 上查看完整代码。
https://github.com/1412771048/skipList
合并过程:
- 创建一个 skiplist.h 文件
- 添加需要的头文件
#include <iostream>
#include <cstdlib> // 随机函数
#include <cmath>
#include <cstring>
#include <mutex> // 引入互斥锁
#include <fstream> // 引入文件操作
- 定义数据保存和加载时的文件路径
- 定义互斥锁
#define STORE_FILE "store/dumpFile" // 存储文件路径
std::mutex mtx; // 定义互斥锁
- 对插入节点成员函数和删除节点成员函数进行加锁
// 只有在插入和删除的时候,才会进行加锁
template <typename K, typename V>
int SkipList<K, V>::insert_element(const K key, const V value) {
mtx.lock(); // 在函数第一句加锁
// ... 算法过程(省略)
if (current != NULL && current->get_key() == key) {
std::cout << "key: " << key << ", exists" << std::endl;
// 在算法流程中有一个验证 key 是否存在的过程
// 在此处需要提前 return,所以提前解锁
mtx.unlock();
return 1;
}
// ...
mtx.unlock(); // 函数执行完毕后解锁
return 0;
}
template <typename K, typename V>
void SkipList<K, V>::delete_element(K key) {
mtx.lock(); // 加锁
// ... 算法过程(省略)
mtx.unlock(); // 解锁
return;
}
本章完整文件如下:
#include <iostream>
#include <cstdlib> // 随机函数
#include <cmath>
#include <cstring>
#include <mutex> // 引入互斥锁
#include <fstream> // 引入文件操作
#define STORE_FILE "store/dumpFile" // 存储文件路径
std::mutex mtx; // 定义互斥锁
std::string delimiter = ":";
template <typename K, typename V>
class Node {
public:
Node() {}
Node(K k, V v, int);
~Node();
K get_key() const;
V get_value() const;
void set_value(V);
Node<K, V> **forward;
int node_level;
private:
K key;
V value;
};
// ... Node 类的所有方法实现(省略)
template <typename K, typename V>
class SkipList {
public:
SkipList(int);
~SkipList();
int get_random_level();
Node<K, V> *create_node(K, V, int);
int insert_element(K, V);
void display_list();
bool search_element(K);
void delete_element(K);
void dump_file();
void load_file();
void clear(Node<K, V> *);
int size();
private:
void get_key_value_from_string(const std::string &str, std::string *key, std::string *value);
bool is_valid_string(const std::string &str);
private:
int _max_level;
int _skip_list_level;
Node<K, V> *_header;
std::ofstream _file_writer;
std::ifstream _file_reader;
int _element_count;
};
// ... SkipList 类的大部分方法实现(省略)
// 只有在插入和删除的时候,才会进行加锁
template <typename K, typename V>
int SkipList<K, V>::insert_element(const K key, const V value) {
mtx.lock(); // 在函数第一句加锁
// ... 算法过程(省略)
if (current != NULL && current->get_key() == key) {
std::cout << "key: " << key << ", exists" << std::endl;
// 在算法流程中有一个验证 key 是否存在的过程
// 在此处需要提前 return,所以提前解锁
mtx.unlock();
return 1;
}
// ...
mtx.unlock(); // 函数执行完毕后解锁
return 0;
}
template <typename K, typename V>
void SkipList<K, V>::delete_element(K key) {
mtx.lock(); // 加锁
// ... 算法过程(省略)
mtx.unlock(); // 解锁
return;
}
至此,我们已经将所有的分散的模块合并。
在将所有的模块合并了之后,可以将 skiplist.h include 到其他文件中,就可以使用该存储引擎了。
压力测试
将 “模块合并” 章节创建的 skiplist.h 包含到当前压力测试程序中。
测试程序主要的内容为编写在随机读写下,测试项目每秒可处理写请求数,和每秒可处理读请求数
具体可以通过多线程(pthread)以及计时(chrono)来执行插入和检索操作。
压力测试文件的内容如下:
// 引入必要的头文件
#include <iostream> // 用于输入输出流
#include <chrono> // 用于高精度时间测量
#include <cstdlib> // 包含一些通用的工具函数,如随机数生成
#include <pthread.h> // 用于多线程编程
#include <time.h> // 用于时间处理函数
#include "./skiplist.h" // 引入自定义的跳表实现
// 定义宏常量
#define NUM_THREADS 1 // 线程数量
#define TEST_COUNT 100000 // 测试用的数据量大小
SkipList<int, std::string> skipList(18); // 创建一个最大层级为18的跳表实例
// 插入元素的线程函数
void *insertElement(void* threadid) {
long tid; // 线程ID
tid = (long)threadid; // 将void*类型的线程ID转换为long型
std::cout << tid << std::endl; // 输出线程ID
int tmp = TEST_COUNT/NUM_THREADS; // 计算每个线程应该插入的元素数量
// 循环插入元素
for (int i=tid*tmp, count=0; count<tmp; i++) {
count++;
skipList.insert_element(rand() % TEST_COUNT, "a"); // 随机生成一个键,并插入带有"a"的元素
}
pthread_exit(NULL); // 退出线程
}
// 检索元素的线程函数
void *getElement(void* threadid) {
long tid; // 线程ID
tid = (long)threadid; // 将void*类型的线程ID转换为long型
std::cout << tid << std::endl; // 输出线程ID
int tmp = TEST_COUNT/NUM_THREADS; // 计算每个线程应该检索的元素数量
// 循环检索元素
for (int i=tid*tmp, count=0; count<tmp; i++) {
count++;
skipList.search_element(rand() % TEST_COUNT); // 随机生成一个键,并尝试检索
}
pthread_exit(NULL); // 退出线程
}
int main() {
srand(time(NULL)); // 初始化随机数生成器
{
pthread_t threads[NUM_THREADS]; // 定义线程数组
int rc; // 用于接收pthread_create的返回值
int i; // 循环计数器
auto start = std::chrono::high_resolution_clock::now(); // 开始计时
// 创建插入元素的线程
for( i = 0; i < NUM_THREADS; i++ ) {
std::cout << "main() : creating thread, " << i << std::endl;
rc = pthread_create(&threads[i], NULL, insertElement, (void *)i); // 创建线程
if (rc) {
std::cout << "Error:unable to create thread," << rc << std::endl;
exit(-1); // 如果线程创建失败,退出程序
}
}
void *ret; // 用于接收pthread_join的返回值
// 等待所有插入线程完成
for( i = 0; i < NUM_THREADS; i++ ) {
if (pthread_join(threads[i], &ret) != 0 ) {
perror("pthread_create() error");
exit(3); // 如果线程等待失败,退出程序
}
}
auto finish = std::chrono::high_resolution_clock::now(); // 结束计时
std::chrono::duration<double> elapsed = finish - start; // 计算耗时
std::cout << "insert elapsed:" << elapsed.count() << std::endl; // 输出插入操作耗时
}
// 下面的代码块与上面类似,用于创建并管理检索操作的线程
{
pthread_t threads[NUM_THREADS];
int rc;
int i;
auto start = std::chrono::high_resolution_clock::now();
for( i = 0; i < NUM_THREADS; i++ ) {
std::cout << "main() : creating thread, " << i << std::endl;
rc = pthread_create(&threads[i], NULL, getElement, (void *)i);
if (rc) {
std::cout << "Error:unable to create thread," << rc << std::endl;
exit(-1);
}
}
void *ret;
for( i = 0; i < NUM_THREADS; i++ ) {
if (pthread_join(threads[i], &ret) != 0 ) {
perror("pthread_create() error");
exit(3);
}
}
auto finish = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> elapsed = finish - start;
std::cout << "get elapsed:" << elapsed.count() << std::endl;
}
pthread_exit(NULL); // 主线程退出
return 0;
}
在测试文件中的 NUM_THREADS(执行线程数量) 和 TEST_COUNT(测试数量) 可自定义。
编译测试文件,执行测试文件,观察输出结果。
编译命令如下:
g++ --std=c++11 main.cpp -o stress -pthread