实战案例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. 架构图

下面这张图展示了整个可插拔存储引擎的架构:

可插拔存储引擎架构 上层应用 / SQL 层 统一接口:se_engine_t(函数指针表) 内存哈希引擎 engine_memhash.so get_engine() 导出 B+树引擎 engine_btree.so get_engine() 导出 LSM树引擎 engine_lsm.so get_engine() 导出 插件加载器:dlopen / dlsym / dlclose 运行时动态选择引擎,无需重新编译主程序

7. 避坑指南

做这种插件架构,有几个坑我替你们踩过了:

  • 符号冲突:多个插件可能有同名全局变量。编译插件时一定要加 -fvisibility=hidden,只显式导出 get_engine
  • 内存管理:插件里 malloc 的内存,最好在插件里 free。跨模块的 malloc/free 可能因为不同CRT而崩溃。
  • 版本兼容:接口结构体一旦发布,就别轻易改字段顺序。我建议在结构体开头加个 version 字段做校验。
  • 线程安全:上面的示例没加锁,生产环境记得加读写锁或互斥量。

警告:千万不要在插件里使用全局构造函数(__attribute__((constructor)))来做初始化。因为dlopen时构造函数执行顺序不可控,容易出诡异问题。老老实实用init函数。

8. 扩展思路

这个框架还可以继续扩展:

  • 支持引擎配置热加载:运行时切换引擎,不停机
  • 增加引擎优先级:主引擎挂了,自动切换到备引擎
  • 做引擎性能统计:记录每次操作的耗时,方便调优

说白了,插件化架构的核心就一句话:定义好契约,剩下的交给运行时。你想想看,主程序根本不需要知道底层是哈希表还是B+树,它只认那七个函数指针。这就是架构的魅力。

好,这一章就到这儿。代码都在上面了,你可以自己动手写个文件存储引擎试试,就当练手。


公众号:蓝海资料掘金营,微信deep3321