实战案例6:数据库存储引擎插件(可插拔存储后端)
说实话,数据库存储引擎这个题目,我一开始觉得挺唬人的。但后来想通了——说白了,就是让上层SQL逻辑和底层数据存储方式解耦。你写一条INSERT语句,数据是写到B+树里,还是写到LSM树里,甚至写到个文本文件里,对上层来说应该是透明的。
我在做嵌入式数据库项目时,就遇到过这种需求。客户说:你们默认的B+树引擎太慢了,我要用内存哈希表。好嘛,那就得把存储引擎做成插件。今天咱们就聊聊这个实战案例。
1. 核心设计思路
先想清楚我们要什么。一个可插拔的存储引擎,需要满足三点:
- 统一接口:所有引擎必须实现相同的操作函数
- 动态加载:运行时按需加载.so或.dll文件
- 生命周期管理:初始化、读写、关闭,每一步都要可控
我个人习惯把接口定义放在一个头文件里,所有引擎插件都按这个契约来写。这样主程序根本不需要知道底层是啥引擎。
核心原则:主程序只认函数指针表,不认具体实现。
2. 接口定义
先定义存储引擎的操作接口。我把它设计成一个结构体,里面全是函数指针:
// storage_engine.h
#ifndef STORAGE_ENGINE_H
#define STORAGE_ENGINE_H
#include <stdint.h>
#include <stdbool.h>
// 引擎操作结果
typedef enum {
SE_OK = 0,
SE_ERROR,
SE_NOT_FOUND,
SE_FULL
} se_status_t;
// 键值对结构
typedef struct {
uint64_t key;
void* value;
size_t value_len;
} se_kv_pair_t;
// 引擎操作接口
typedef struct {
// 引擎元信息
const char* engine_name;
const char* engine_version;
// 生命周期
se_status_t (*init)(const char* config);
se_status_t (*destroy)(void);
// 核心操作
se_status_t (*put)(uint64_t key, const void* value, size_t len);
se_status_t (*get)(uint64_t key, void* value, size_t* len);
se_status_t (*delete)(uint64_t key);
// 统计信息
size_t (*count)(void);
void (*stats)(char* buf, size_t buf_size);
} se_engine_t;
// 插件导出符号 - 每个引擎必须实现
typedef se_engine_t* (*get_engine_func)(void);
#endif // STORAGE_ENGINE_H
嗯,这里要注意:get_engine_func 这个函数指针类型,是插件和主程序之间的桥梁。每个插件必须导出一个同名函数,返回自己的引擎实例。
3. 插件加载器
接下来是动态加载的核心代码。我用的是POSIX的dlopen系列函数,Windows下用LoadLibrary,原理一样。
// plugin_loader.c
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "storage_engine.h"
typedef struct plugin_handle {
void* handle; // dlopen返回的句柄
se_engine_t* engine; // 引擎实例
char path[256]; // 插件路径
} plugin_handle_t;
// 加载插件
plugin_handle_t* load_engine_plugin(const char* so_path) {
plugin_handle_t* ph = malloc(sizeof(plugin_handle_t));
if (!ph) return NULL;
// 打开动态库
ph->handle = dlopen(so_path, RTLD_NOW | RTLD_LOCAL);
if (!ph->handle) {
fprintf(stderr, "dlopen failed: %s\n", dlerror());
free(ph);
return NULL;
}
// 获取引擎工厂函数
get_engine_func get_engine = (get_engine_func)dlsym(ph->handle, "get_engine");
if (!get_engine) {
fprintf(stderr, "dlsym failed: %s\n", dlerror());
dlclose(ph->handle);
free(ph);
return NULL;
}
// 创建引擎实例
ph->engine = get_engine();
if (!ph->engine) {
fprintf(stderr, "get_engine returned NULL\n");
dlclose(ph->handle);
free(ph);
return NULL;
}
strncpy(ph->path, so_path, sizeof(ph->path) - 1);
printf("[Loader] Loaded engine: %s v%s from %s\n",
ph->engine->engine_name,
ph->engine->engine_version,
so_path);
return ph;
}
// 卸载插件
void unload_engine_plugin(plugin_handle_t* ph) {
if (!ph) return;
// 先销毁引擎
if (ph->engine && ph->engine->destroy) {
ph->engine->destroy();
}
// 关闭动态库
if (ph->handle) {
dlclose(ph->handle);
}
free(ph);
printf("[Loader] Engine unloaded\n");
}
我曾经踩过一个坑:dlclose之后,引擎结构体里的函数指针就变成悬空指针了。所以一定要先调用destroy,再关库。顺序反了就是段错误。
4. 实现一个内存哈希引擎
咱们写个最简单的内存哈希表引擎,作为示例插件:
// engine_memhash.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "storage_engine.h"
#define HASH_SIZE 1024
typedef struct hash_entry {
uint64_t key;
void* value;
size_t value_len;
struct hash_entry* next;
} hash_entry_t;
static hash_entry_t* table[HASH_SIZE];
static size_t entry_count = 0;
static uint32_t hash_key(uint64_t key) {
return (uint32_t)(key ^ (key >> 32)) % HASH_SIZE;
}
static se_status_t mem_init(const char* config) {
memset(table, 0, sizeof(table));
entry_count = 0;
printf("[MemHash] Initialized (config: %s)\n", config ? config : "none");
return SE_OK;
}
static se_status_t mem_destroy(void) {
for (int i = 0; i < HASH_SIZE; i++) {
hash_entry_t* e = table[i];
while (e) {
hash_entry_t* next = e->next;
free(e->value);
free(e);
e = next;
}
table[i] = NULL;
}
entry_count = 0;
printf("[MemHash] Destroyed\n");
return SE_OK;
}
static se_status_t mem_put(uint64_t key, const void* value, size_t len) {
uint32_t idx = hash_key(key);
hash_entry_t* e = table[idx];
// 查找是否已存在
while (e) {
if (e->key == key) {
// 更新
free(e->value);
e->value = malloc(len);
memcpy(e->value, value, len);
e->value_len = len;
return SE_OK;
}
e = e->next;
}
// 新建节点
e = malloc(sizeof(hash_entry_t));
e->key = key;
e->value = malloc(len);
memcpy(e->value, value, len);
e->value_len = len;
e->next = table[idx];
table[idx] = e;
entry_count++;
return SE_OK;
}
static se_status_t mem_get(uint64_t key, void* value, size_t* len) {
uint32_t idx = hash_key(key);
hash_entry_t* e = table[idx];
while (e) {
if (e->key == key) {
if (*len < e->value_len) {
return SE_ERROR; // 缓冲区太小
}
memcpy(value, e->value, e->value_len);
*len = e->value_len;
return SE_OK;
}
e = e->next;
}
return SE_NOT_FOUND;
}
static se_status_t mem_delete(uint64_t key) {
uint32_t idx = hash_key(key);
hash_entry_t* e = table[idx];
hash_entry_t* prev = NULL;
while (e) {
if (e->key == key) {
if (prev) {
prev->next = e->next;
} else {
table[idx] = e->next;
}
free(e->value);
free(e);
entry_count--;
return SE_OK;
}
prev = e;
e = e->next;
}
return SE_NOT_FOUND;
}
static size_t mem_count(void) {
return entry_count;
}
static void mem_stats(char* buf, size_t buf_size) {
snprintf(buf, buf_size,
"MemHash Engine\n"
" Entries: %zu\n"
" Buckets: %d\n",
entry_count, HASH_SIZE);
}
// 导出引擎实例 - 这是插件必须的符号
se_engine_t* get_engine(void) {
static se_engine_t engine = {
.engine_name = "Memory Hash",
.engine_version = "1.0.0",
.init = mem_init,
.destroy = mem_destroy,
.put = mem_put,
.get = mem_get,
.delete = mem_delete,
.count = mem_count,
.stats = mem_stats
};
return &engine;
}
注意看最后那个get_engine函数。这就是插件导出的入口点。主程序通过dlsym找到它,拿到引擎实例。
小技巧:引擎实例用static变量,这样每个插件只创建一个实例。如果你需要多实例,可以改成动态分配,但记得在init里传个实例ID。
5. 主程序如何使用
主程序就简单了,加载插件,然后统一调用接口:
// main.c
#include <stdio.h>
#include "storage_engine.h"
#include "plugin_loader.h"
int main(int argc, char* argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <engine.so>\n", argv[0]);
return 1;
}
// 加载插件
plugin_handle_t* ph = load_engine_plugin(argv[1]);
if (!ph) {
fprintf(stderr, "Failed to load engine\n");
return 1;
}
se_engine_t* eng = ph->engine;
// 初始化
eng->init("max_entries=10000");
// 写入数据
eng->put(42, "Hello, Plugin!", 14);
eng->put(100, "Database Engine", 15);
// 读取数据
char buf[64];
size_t len = sizeof(buf);
if (eng->get(42, buf, &len) == SE_OK) {
printf("Got: %.*s\n", (int)len, buf);
}
// 统计信息
char stats[256];
eng->stats(stats, sizeof(stats));
printf("Stats:\n%s\n", stats);
// 清理
unload_engine_plugin(ph);
return 0;
}
编译插件时,要编译成动态库:
gcc -shared -fPIC -o engine_memhash.so engine_memhash.c -ldl
gcc -o main main.c plugin_loader.c -ldl
运行:
./main ./engine_memhash.so
6. 架构图
下面这张图展示了整个可插拔存储引擎的架构:
7. 避坑指南
做这种插件架构,有几个坑我替你们踩过了:
- 符号冲突:多个插件可能有同名全局变量。编译插件时一定要加
-fvisibility=hidden,只显式导出get_engine。 - 内存管理:插件里 malloc 的内存,最好在插件里 free。跨模块的 malloc/free 可能因为不同CRT而崩溃。
- 版本兼容:接口结构体一旦发布,就别轻易改字段顺序。我建议在结构体开头加个
version字段做校验。 - 线程安全:上面的示例没加锁,生产环境记得加读写锁或互斥量。
警告:千万不要在插件里使用全局构造函数(__attribute__((constructor)))来做初始化。因为dlopen时构造函数执行顺序不可控,容易出诡异问题。老老实实用init函数。
8. 扩展思路
这个框架还可以继续扩展:
- 支持引擎配置热加载:运行时切换引擎,不停机
- 增加引擎优先级:主引擎挂了,自动切换到备引擎
- 做引擎性能统计:记录每次操作的耗时,方便调优
说白了,插件化架构的核心就一句话:定义好契约,剩下的交给运行时。你想想看,主程序根本不需要知道底层是哈希表还是B+树,它只认那七个函数指针。这就是架构的魅力。
好,这一章就到这儿。代码都在上面了,你可以自己动手写个文件存储引擎试试,就当练手。