常見的 Key-Value 緩存如 Memcached、Redis 皆以獨立服務形式運行。但在某些場景下(如邊緣計算、輕量級微服務、避免網絡開銷),我們需要一個**嵌入式、本地化、無外部依賴**的 KV 緩存。現有的 LevelDB、RocksDB 雖強大,但對於簡單場景顯得過於重量級(依賴多、編譯複雜、資源佔用大)。
整個緩存通過 MemFile 映射一塊連續內存區域,劃分為三段:
m_Head, m_Tail)。m_Next, m_Prev)。ThreadMutex (RAII 鎖 AutoLock) 保護。m_RecycleHead) 獲取節點,不足時從未使用區 (m_FreeBase) 分配。MoveToHead),淘汰時回收表尾 (GetTailNodeId + RecycleNode)。K 需提供 HashCode() 和 Equals() 方法;V 需支持拷貝構造/賦值。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;
}
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。