首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫

MySQL 實現本地 Key-Value 緩存:輕量級嵌入式方案完整代碼解析

     編輯:MySQL 資料庫相關知識

項目背景與動機

常見的 Key-Value 緩存如 Memcached、Redis 皆以獨立服務形式運行。但在某些場景下(如邊緣計算、輕量級微服務、避免網絡開銷),我們需要一個**嵌入式、本地化、無外部依賴**的 KV 緩存。現有的 LevelDB、RocksDB 雖強大,但對於簡單場景顯得過於重量級(依賴多、編譯複雜、資源佔用大)。

本方案核心特性

  1. 極致輕量:核心代碼僅約 400 行 C++,無第三方依賴。
  2. 高性能:Value 長度 1K 時,單線程讀寫速度可達 200 萬 ops/秒。
  3. 零拷貝/內存映射:數據直接映射到文件(mmap),避免 malloc/free 開銷,杜絕內存洩漏與碎片。
  4. 持久化與崩潰恢復:服務重啟甚至機器掉電後,緩存內容依然存在(映射到磁盤文件時)。
  5. 近似 LRU 淘汰:在哈希桶鏈表層面實現 LRU,雖非全局嚴格 LRU,但滿足大多數場景。
  6. 生產驗證:已在數十台線上機器穩定運行半年無故障。
  7. 泛型支持:Key/Value 支持任意 C++ 類型,不限於字串,使用更靈活。

核心架構設計

數據結構佈局

整個緩存通過 MemFile 映射一塊連續內存區域,劃分為三段:

  • TableHead (固定 1KB):元數據(表長、節點總數、空閒列表頭、回收列表頭、已用計數、表名、擴展區)。
  • Array[] (哈希桶陣列):每個桶存儲雙向鏈表的頭尾節點 ID (m_Head, m_Tail)。
  • Entry[] (節點池):存儲實際 KV 數據、哈希碼、前後節點指針 (m_Next, m_Prev)。

關鍵機制

  • 線程安全:所有公共操作均由 ThreadMutex (RAII 鎖 AutoLock) 保護。
  • 雙層空閒管理:優先從回收鏈表 (m_RecycleHead) 獲取節點,不足時從未使用區 (m_FreeBase) 分配。
  • LRU 實現:每個哈希桶維護一條雙向鏈表,訪問節點移至表頭 (MoveToHead),淘汰時回收表尾 (GetTailNodeId + RecycleNode)。
  • Key 概念要求:模板參數 K 需提供 HashCode() 和 Equals() 方法;V 需支持拷貝構造/賦值。

核心代碼實現

HashTable 類定義

template
class HashTable {
public:
    HashTable(const char* tablename, uint32_t tableLen, uint32_t nodeTotal);
    virtual ~HashTable();

    // 基礎 CRUD
    bool Add(K& key, V& value);
    bool Del(K& key);
    bool Set(K& key, V& value);
    bool Get(K& key, V& value);
    bool Exist(K& key);
    uint32_t Count();
    bool Replace(K& key, V& value); // 存在則更新,否則新增

    // LRU 相關操作
    bool LruAdd(K& key, V& value, K& recyKey, V& recyValue, bool& recycled);
    bool LruSet(K& key, V& value);
    bool LruGet(K& key, V& value);
    bool LruReplace(K& key, V& value, K& recyKey, V& recyValue, bool& recycled);

    void Clear();
    int GetRowKeys(vector& keys, uint32_t index);
    void* Padding(uint32_t size);

private:
    // 內部結構體、私有方法、成員變量...
    static const uint32_t m_InvalidId = 0xffffffff;
    static const uint32_t m_HeadSize = 1024;
    struct TableHead { ... };
    struct Array { uint32_t m_Head, m_Tail; };
    struct Entry { V m_Value; K m_Key; uint32_t m_Code, m_Next, m_Prev; };

    size_t m_MemSize;
    uint8_t* m_MemAddr;
    TableHead* m_HeadAddr;
    Array* m_ArrayAddr;
    Entry* m_EntryAddr;
    ThreadMutex m_MutexLock;

    // 私有輔助方法
    bool MoveToHead(K& key);
    uint32_t GetIdByKey(K& key);
    void AddNodeToHead(uint32_t index, uint32_t nodeId);
    bool MoveNodeToHead(uint32_t index, uint32_t nodeId);
    bool RecycleNode(uint32_t index, uint32_t nodeId);
    uint32_t GetTailNodeId(uint32_t index);
    uint32_t GetFreeNode();
    DISABLE_COPY_AND_ASSIGN(HashTable);
};

關鍵邏輯詳解

構造函數:內存映射與初始化

template
HashTable::HashTable(const char* tablename, uint32_t tableLen, uint32_t nodeTotal) {
    AbortAssert(tablename != NULL);
    m_MemSize = m_HeadSize + tableLen * sizeof(Array) + nodeTotal * sizeof(Entry);
    // 關鍵:通過 MemFile::Realloc 實現文件映射/擴展
    m_MemAddr = (uint8_t*)MemFile::Realloc(tablename, m_MemSize);
    AbortAssert(m_MemAddr != NULL);

    m_HeadAddr = (TableHead*)m_MemAddr;
    m_ArrayAddr = (Array*)(m_MemAddr + m_HeadSize);
    m_EntryAddr = (Entry*)(m_MemAddr + m_HeadSize + tableLen * sizeof(Array));

    m_HeadAddr->m_TableLen = tableLen;
    m_HeadAddr->m_NodeTotal = nodeTotal;
    strncpy(m_HeadAddr->m_TableName, tablename, sizeof(m_HeadAddr->m_TableName));

    // 首次使用時初始化桶陣列
    if (m_HeadAddr->m_UsedCount == 0) {
        for (uint32_t i = 0; i < tableLen; ++i) {
            (m_ArrayAddr + i)->m_Head = m_InvalidId;
            (m_ArrayAddr + i)->m_Tail = m_InvalidId;
        }
        m_HeadAddr->m_FreeBase = 0;
        m_HeadAddr->m_RecycleHead = 0;
    }
}

獲取節點:回收優先、新建次之

template
uint32_t HashTable::GetFreeNode() {
    uint32_t nodeId = m_InvalidId;
    // 1. 優先從回收鏈表獲取
    if (m_HeadAddr->m_UsedCount < m_HeadAddr->m_FreeBase) {
        nodeId = m_HeadAddr->m_RecycleHead;
        m_HeadAddr->m_RecycleHead = (m_EntryAddr + nodeId)->m_Next;
        ++(m_HeadAddr->m_UsedCount);
    }
    // 2. 回收鏈表空,從未使用區分配
    else if (m_HeadAddr->m_UsedCount < m_HeadAddr->m_NodeTotal) {
        nodeId = m_HeadAddr->m_FreeBase;
        ++(m_HeadAddr->m_FreeBase);
        ++(m_HeadAddr->m_UsedCount);
    }
    // 3. 滿了返回無效 ID
    else {
        nodeId = m_InvalidId;
    }

    // 初始化節點
    if (nodeId < m_HeadAddr->m_NodeTotal) {
        Entry* tmpNode = m_EntryAddr + nodeId;
        memset(tmpNode, 0, sizeof(Entry));
        tmpNode->m_Next = m_InvalidId;
        tmpNode->m_Prev = m_InvalidId;
    }
    return nodeId;
}

LRU 核心:LruReplace 統一入口

template
bool HashTable::LruReplace(K& key, V& value, K& recyKey, V& recyValue, bool& recycled) {
    AutoLock autoLock(m_MutexLock);
    recycled = false;

    if (Exist(key)) return LruSet(key, value); // 存在:更新值並移至頭部
    else return LruAdd(key, value, recyKey, recyValue, recycled); // 不存在:嘗試新增,滿則淘汰尾部
}

使用示例

// 定義 Key 類型 (需實現 HashCode 和 Equals)
struct MyKey {
    uint64_t id;
    uint32_t HashCode() const { return (uint32_t)(id ^ (id >> 32)); }
    bool Equals(const MyKey& other) const { return id == other.id; }
};

struct MyValue {
    char data[1024];
    int len;
};

int main() {
    // 創建/打開緩存:表名、桶數量(建議質數)、最大節點數
    HashTable cache("my_cache.dat", 10007, 100000);

    MyKey key{12345};
    MyValue val{"hello", 5};

    // 普通模式
    cache.Add(key, val);
    MyValue got;
    if (cache.Get(key, got)) { /* ... */ }

    // LRU 模式 (緩存滿時自動淘汰最久未用)
    MyKey recyKey; MyValue recyVal; bool recycled;
    cache.LruReplace(key, val, recyKey, recyVal, recycled);
    if (recycled) { /* 通知業務層 recyKey 已被淘汰 */ }

    return 0; // 析構時自動 MemFile::Release,數據持久化到文件
}

適用場景與局限

適用場景不適用場景
單進程/多線程嵌入式緩存跨進程/分布式共享緩存 (需 Redis 等)
高性能讀寫、數據可放入單機內存/磁盤數據量遠超單機磁盤容量
需要持久化、崩潰恢復需要嚴格全局 LRU、事務、TTL 過期
Key/Value 為複雜 C++ 結構體需 SQL 查詢、二級索引、範圍查詢

總結

本方案通過 內存映射文件 + 定長哈希表 + 桶級 LRU 實現了一個極簡、高效、持久化的本地 KV 緩存。代碼精簡 (~400 行)、無依賴、易嵌入,非常適合對延遲敏感、不想引入重型中間件的 C++ 服務端場景。若需分布式、高可用、豐富數據結構,請轉向 Redis。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved