當海量數據超過內容從大小需要落盤保存贏如何解決?如何對KV存儲進行封裝融合進redis?Redis編碼如何實現?Redis 是目前 NoSQL 領域的當紅炸子雞,本文涉及的Ardb就是一個完全兼容Redis協議的NoSQL的存儲服務。其存儲基於現有成熟的KV存儲引擎實現,理論上任何類似B-Tree/LSM Tree實現的KV存儲實現均可作為Ardb的底層存儲實現,目前Ardb支持LevelDB/RocksDB/LMDB.
本文以Ardb為例,介紹Redis與KV存儲之間融合時編解碼層的實現。
編碼方式
Redis與KV存儲的融合方案中, 編解碼層是一個很重要的環節。通過編解碼層,我們可以屏蔽了各種kv存儲實現的不同,可以在任意一個簡單的kv存儲引擎上,封裝實現Redis中string,hash,list,set,sorted set等復雜類型的數據結構。
對於String類型,很顯然可以與KV存儲中的一個KV對一一對應;
對於其它的容器類型,我們需要
- 一個KV來存儲其整個Key的元信息(比如List的成員個數,過期時間等);
- 每一個成員需要一個KV來保存成員的名稱和值;
對於sorted set,其每個成員有score和rank兩個屬性,所以需要:
- 一個KV保存整個Key的元信息
- 每一個成員需要一個KV保存 score信息
- 每一個成員需要一個KV保存每個成員對應 rank 信息
Key的編碼格式
對於所有的Key, 包含同樣的前綴,編碼格式定義如下:
[<namespace>] <key> <type> <element...>
namespace用於支持類似redis中的庫概念, 可以為任意字符串, 不限制必須為數字;
key則是一個變長二進制字符串
type用於定義一個簡單key-value的類型,此類型隱含表明key的數據結構類型;一個字節
meta信息的key中type固定為KEY_META;具體類型將在value中定義(參考下一節)
除以上三部分外,不同類型的key可能有附加字段;如Hash的key可能需要附加field字段
Value的編碼格式
內部Value則比較復雜,編碼均以type開始, type取值即上節定義的KeyType
<type> <element...>
后續格式根據各種類型定義不同.
各類型數據編碼方式
各類型數據的編碼方式如下: ns代表namespace
KeyObject ValueObject
String [<ns>] <key> KEY_META KEY_STRING <MetaObject> Hash [<ns>] <key> KEY_META KEY_HASH <MetaObject> [<ns>] <key> KEY_HASH_FIELD <field> KEY_HASH_FIELD <field-value> Set [<ns>] <key> KEY_META KEY_SET <MetaObject> [<ns>] <key> KEY_SET_MEMBER <member> KEY_SET_MEMBER List [<ns>] <key> KEY_META KEY_LIST <MetaObject> [<ns>] <key> KEY_LIST_ELEMENT <index> KEY_LIST_ELEMENT <element-value> Sorted Set [<ns>] <key> KEY_META KEY_ZSET <MetaObject> [<ns>] <key> KEY_ZSET_SCORE <member> KEY_ZSET_SCORE <score> [<ns>] <key> KEY_ZSET_SORT <score> <member> KEY_ZSET_SORT
ZSet編碼實例
這里以最復雜的Sorted Set來做實例。假設有個Sorted Set為 A: {member=frist, score=1}, {member=second, score=2}。其在Ardb中的存儲方式如下:
Key A的存儲編碼為:
// 偽代碼中的|代表域的分割,不代表實際存儲為"|"。實際序列化的時候每個域是按照特定位置序列化的. 鍵為:ns|1|A(1代表是KEY_META元信息類型) 值為:元信息編碼(redis數據類型/zset,過期時間,成員個數,最大最小score等)
成員first的score信息存儲編碼為:
鍵為:ns|11|A|first (11代表類型為KEY_ZSET_SCORE) 值為:11|1 (11代表類型KEY_ZSET_SCORE,1為該成員first的score)
成員first的rank信息存儲編碼為:
鍵為:ns|10|A|1|first (10代表類型為KEY_ZSET_SORT, 1為score) 值為:10 (代表類型KEY_ZSET_SORT,無意義。rocksdb中自動按key大小排序,所以很容易算出rank,不需要存儲和更新)
成員second的score信息存儲編碼略。
當用戶使用zcard A命令時,直接訪問namespace_1_A即可得到元信息中該有序集合的數目;
當用戶使用zscore A first時,直接訪問namespace_A_first即可得到first成員的score;
當用戶使用zrank A first時,先用zscore得到score,再查找namespace_10_A_1_first的序號;
具體的存儲方式代碼如下:
KeyObject meta_key(ctx.ns, KEY_META, key); ValueObject meta_value; for (each_member) { // KEY_ZSET_SORT 存儲rank信息 KeyObject zsort(ctx.ns, KEY_ZSET_SORT, key); zsort.SetZSetMember(str); zsort.SetZSetScore(score); ValueObject zsort_value; zsort_value.SetType(KEY_ZSET_SORT); GetDBWriter().Put(ctx, zsort, zsort_value); // 存儲score信息 KeyObject zscore(ctx.ns, KEY_ZSET_SCORE, key); zscore.SetZSetMember(str); ValueObject zscore_value; zscore_value.SetType(KEY_ZSET_SCORE); zscore_value.SetZSetScore(score); GetDBWriter().Put(ctx, zscore, zscore_value); } if (expiretime > 0) { meta_value.SetTTL(expiretime); } // 元信息 GetDBWriter().Put(ctx, meta_key, meta_value);
Del的實現
所有的數據結構都有保存meta的一個key-value,而meta信息的key編碼格式是統一的,因此不可能出現不同數據結構有相同名字的情況。(這就是為什么保存Key的KV對中,K固定為KEY_META類型,而對應redis類型信息存在META類型數據的Value中的原因)。
Del實現中會先查詢meta的key-value,得到具體數據結構類型,然后執行對應的刪除工作, 類似如下的步驟:
- 查詢指定key的meta信息,得到數據結構類型
- 根據具體類型,執行刪除工作
- 所以一次del至少需要 一次讀 + 后續刪除寫操作
具體代碼如下:
int Ardb::DelKey(Context& ctx, const KeyObject& meta_key, Iterator*& iter) { ValueObject meta_obj; if (0 == m_engine->Get(ctx, meta_key, meta_obj)) { // 如果是string類型直接刪除即可 if (meta_obj.GetType() == KEY_STRING) { int err = RemoveKey(ctx, meta_key); return err == 0 ? 1 : 0; } } else { return 0; } if (NULL == iter) { // 如果是復雜類型,需要按照namespace,key,類型前綴遍歷庫 // 搜索出所有前綴為 namespace|類型|Key的成員 iter = m_engine->Find(ctx, meta_key); } else { iter->Jump(meta_key); } while (NULL != iter && iter->Valid()) { KeyObject& k = iter->Key(); ... iter->Del(); iter->Next(); } }
前綴搜索代碼如下:
Iterator* RocksDBEngine::Find(Context& ctx, const KeyObject& key) { ... opt.prefix_same_as_start = true; if (!ctx.flags.iterate_no_upperbound) { KeyObject& upperbound_key = iter->IterateUpperBoundKey(); upperbound_key.SetNameSpace(key.GetNameSpace()); if (key.GetType() == KEY_META) { upperbound_key.SetType(KEY_END); } else { upperbound_key.SetType(key.GetType() + 1); } upperbound_key.SetKey(key.GetKey()); upperbound_key.CloneStringPart(); } ... }
Expire的實現
在一個key-value存儲引擎上支持復雜數據結構的expire過期數據的實現比較困難,ardb中則用幾個特殊技巧實現了對所有數據結構的過期(expire)的支持。
具體實現如下:
- meta的value中保存expire信息, 用絕對unix時間(ms)保存;
- 基於以上設計,ttl/pttl等查詢ttl的操作只需要一次讀meta即可完成;
- 基於以上設計,任何對meta信息的讀取,都會觸發expire的判斷,由於對meta信息的讀操作是必須的步驟,這里無需額外的讀操作(和Redis一樣訪問時會觸發)
- 創建一個namespace TTL_DB專門存放TTL排序信息。
- 保存設置expire時間到meta時, 當expire時間非0時,額外保存一個key-value, type為KEY_TTL_SORT; key的編碼格式為 [TTL_DB] "" KEY_TTL_SORT , value為空;所以類似expire 等設置過期時間的操作,在ardb的實現中將會多一次寫操作;
- 在自定義的comparator中,對KEY_TTL_SORT類型的key比較規則為先比較,這樣KEY_TTL_SORT數據將會以過期時間遠近保存在一起
- ardb中獨立啟動一個線程,每隔一定時間(100ms)順序掃描KEY_TTL_SORT類型數據;當過期時間小於當前時間,即可觸發刪除操作;當過期時間大於當前時間,即可終止本次掃描。(相當於是Redis中的定時任務serverCron中處理過期Key)。
結語
通過編碼層的轉換,我們可以很好的對KV存儲進行封裝從而和Redis進行融合。所有對Redis數據的操作,經過編碼層的轉換,最終會轉化為對KV存儲的n次讀寫(N>=1)。在符合Redis命令語義的情況下,編碼曾設計應當盡量的減少n的次數。
最重要的一點是,Redis與KV存儲的融合並不是為了替換Redis,而是尋求一種在性能可接受的情況下使得單機能支持遠超內存限制的數據量。在特定的場景中,也可以作為冷數據的存儲方案與Redis熱數據之間互聯互通。
源碼來源: minglisoft.cn/technology