天天看點

Redis 子產品系統中的原生類型注冊一個新的資料類型

Redis 子產品系統可以通過調用 Redis 指令和低層次的直接通路資料結構,來深層次通路 Redis 内置的資料結構。

通過使用這些功能,以便在現有的 Redis 資料結構上建構新的抽象,或者通過使用字元串DMA将子產品的資料結構編碼為 Redis 字元串,建立子產品可能看起來就像導出了新的資料類型。但是,對于更複雜的問題,需要在子產品内部實作新的資料結構,這還不夠。

我們使用 Redis 子產品實作新的資料結構的功能,這就像本地的 Redis 執行個體中原生支援的類型。這個文檔旨在描述 Redis 子產品系統導出的 API ,這些 API 被用來建立新的資料類型、處理序列化的RDB檔案和在處理AOF的重寫過程中,通過<code>TYPE</code> 指令報告的類型等等。

一個導出原生類型的子產品由以下主要的部分組成:

實作某些新的資料結構和對新資料結構進行操作的指令。

一組回調方法用來處理:生成RDB,載入RDB,AOF重寫,釋放值對象關聯的鍵對象,計算和<code>DEBUG DIGEST</code>指令一起使用的哈希值。

每個子產品原生資料類型獨一無二的長度為9個字元的名字。

一種編碼版本,用來将RDB檔案持久化為指定子產品的資料版本,以便一個子產品能夠載入更老版本的RDB檔案。

當正在加載RDB檔案時,生成RDB操作和AOF重寫過程起初看起來可能很複雜,但是子產品的API提供一個非常進階的功能來處理所有這些問題,而不需要使用者來處理讀寫錯誤,是以實際情況中,為 Redis 寫一個新的資料結構是非常簡單的工作。

Redis 發行版的 <code>/modules/hellotype.c</code> 檔案中提供了一個非常容易了解并且完整的原生類型實作的例子。希望讀者通過浏覽這個示例實作來閱讀文檔,以了解實踐中的應用。

為了在 Redis 核心中注冊一個新的原生類型,子產品需要聲明一個全局變量,該變量将維護一個對資料類型的引用。注冊資料類型的API将會傳回一個被儲存在全局變量的資料類型。

從上面的例子可以看到,注冊一個新的類型要需要一次API調用。然而,一些函數指針作為參數被傳遞進去。一部分參數是可選的,而一部分參數是必須要指定的。上述例子中的方法集必須被傳遞,而 <code>.digest</code> 和<code>.mem_usage</code> 是可選的,并且這兩個選項在子產品内部并沒有被支援,是以你可以先忽略他們。

<code>ctx</code> 參數使我們在<code>OnLoad</code>函數中接收到的上下文,<code>name</code> 類型是一個由9個字元組成的名字,字元集包括 <code>A-Z</code>, <code>a-z</code>, <code>0-9</code> ,以及下劃線 <code>_</code> 和負号 <code>-</code>。

請注意,在 Redis 中的每一個資料類型的名字必須是獨一無二的,是以,如果其名稱有意義,請使用大小寫的組合,并且嘗試使用類型名稱和子產品作者名稱作為組合的習慣,建立一個9個字元長的獨一無二的名稱。

注意: 非常重要的是,名稱長度必須為9個字元長度,否則注冊類型将會失敗,繼續閱讀更多以了解原因。

例如,如果我建立了一個 b-tree 的資料類型并且我的名字是 antirez ,那麼我将其命名為 btree1-az 。該名字将會被轉換為64 bit的整數,當進行持久化時會被存儲在RDB檔案中,并且當加載RDB資料時,會被用來解析成對應的子產品資料。如果 Redis 沒有發現相比對的子產品,整數值會被轉換為名稱,以便為使用者提供一些關于加載資料時丢失的子產品的線索。

當執行 <code>TYPE</code> 指令調用了持有已注冊的類型的鍵,該類型名稱也被用來作為指令的回複。

<code>encver</code> 參數是所使用的子產品在RDB檔案中存儲的編碼版本。例如,我的編碼版本從0開始,但是當我釋出2.0版本的子產品時,我可以更好的切換編碼,新的子產品将被注冊為編碼版本1,是以,當當它生成新的RDB檔案時,新的版本将會被存儲在磁盤中。但是,當載入RDB檔案時,子產品的<code>rdb_load</code> 方法将會被調用,即使發現有不同的編碼版本(該編碼版本會被作為參數傳遞給<code>rdb_load</code>),以便該子產品仍然能夠加載舊的RDB檔案。

最後一個參數是一個結構體,用于傳遞類型方法給注冊函數: <code>rdb_load</code>, <code>rdb_save</code>, <code>aof_rewrite</code>, <code>digest</code> 和<code>free</code> 和<code>mem_usage</code>全都是回調函數,且具有以下的原型和用途:

當從RDB檔案中加載資料時,調用 <code>rdb_load</code> 函數,它以與<code>rdb_save</code> 所生成相同的格式加載資料。

當生成RDB檔案時,<code>rdb_save</code> 被調用。

當AOF正在被執行重寫操作時,<code>aof_rewrite</code> 被調用,并且子產品需要告訴 Redis 重新建立指定鍵的指令順序。

當執行 <code>DEBUG DIGEST</code> 并且持有子產品類型的鍵被找到時,<code>digest</code> 被調用。目前還沒有實作該功能,是以被留為空。

當<code>digest</code> 指令請求指定鍵總共消耗的記憶體時,<code>mem_usage</code> 被調用,并且被用于擷取子產品值使用的位元組數。

當一個具有子產品原生類型的鍵通過<code>DEL</code> 或者任何其他的方式被删除時,<code>free</code> 被調用,是為了讓子產品回收與此相關聯的記憶體。

哦,我明白你需要了解這一點,是以這裡有一個具體的解釋。

當 Redis 進行持久化生成 RDB 檔案時,子產品特定的資料類型也需要被持久化。目前 RDB 檔案是鍵值對的序列,如下所示:

1個位元組用來辨別字元串、清單或集合等等。就子產品資料而言,他被設定為<code>module data</code> 的特殊值,但是這當然還不夠,我們還需要将一個特定值和能夠加載和處理該特定值的特定子產品類型相連結的資訊。

是以,當我将一個子產品的 <code>type specific value</code> 進行持久化時,我們用一個64位整數來表示它的字首。64位足夠去存儲所需要的資訊,以便查找到可以處理指定類型的子產品,并且足夠的短,以至于可以在RDB檔案中存儲每一個子產品的字首,而最終不會使RDB檔案太大。同時,這個用64位簽名作為字首的解決方案不需要做奇怪的事,比如在RDB頭檔案中定義具體類型子產品的清單。一切都很簡單。

那麼,為了以可靠的方式辨別給定的子產品,那麼你可以在64位中存儲什麼呢?如果你建構了一個64個符号的字元集,你可以輕易的存儲9個字元,每個字元6位長度,并且還剩下10位,這10位用于存儲類型的編碼版本,以便在将來相同的類型能夠逐漸演變,并且能為RDB檔案提供不同的、更加有效或可更新的序列化格式。

是以每個子產品值被存儲的字首如下所示:

前9個元素是由6位組成的字元,最後10位是編碼版本。

當RDB檔案加載時,先會讀取64位值,然後屏蔽最後10位,并且在子產品類型的緩存中查找一個可以比對上的子產品。當找到一個比對項時,載入RDB檔案值的方法就被調用,10位的編碼版本作為該方法的參數,以便如果支援多個版本,子產品能夠了解要加載的資料布局的版本。

現在有趣的是,如果因為沒有被加載的子產品由這個簽名,而子產品類型不能被解決,我們可以将64位值轉換回9個字元的名字,并且将一個包括子產品類型名稱的錯誤列印給使用者!以便她或者他能夠立刻意識到錯誤。

在<code>RedisModule_OnLoad()</code> 函數中注冊了我們新的資料類型之後,我們還需要能夠設定具有原生類型值的 Redis 鍵。

這通常發生在将資料寫到鍵的指令的上下文中。原生類型API允許設定和擷取子產品原生資料類型,并且可以測試一個給定的鍵是否已經和一個指定的資料類型相關聯。

API使用正常子產品的<code>RedisModule_OnLoad()</code> 低層次的通路接口來處理此問題。這是一個将原生類型私有資料結構設定為Redis鍵的一個例子。

函數 <code>RedisModule_ModuleTypeSetValue()</code> 用于打開一個鍵的用于寫操作的句柄,并且獲得三個參數:鍵的句柄,原生類型的引用和一個在類型注冊期間擷取,最終包含實作原生類型的私有資料的指針<code>void*</code> 。

請注意,Redis沒有任何關于你的資料的線索。它隻是調用在注冊方法時你提供的回調函數,以便對該類型執行操作。

類似地,我們可以從一個鍵中找回私有的資料,使用下面的函數:

我們還可以測試一個鍵是否含有原生類型作為其值。

但是,為了讓調用能做正确的事,我們需要檢查是否鍵為空,是否包含正确類型的值等等。是以一貫的實作寫原生類型指令的代碼如下面這些行所示。

然後,如果我們成功的驗證了該鍵不是一個錯誤的類型,并且我們準備要寫入值,如果鍵為空,那麼我們通常是想要建立一個新的資料結構,或者如果鍵不為空,則傳回一個關聯該鍵的值的引用。

如之前所述,當 Redis 需要去釋放一個持有原生類型值的鍵時,它需要從子產品的幫助才能釋放記憶體。這就是我們為什麼在類型注冊期間傳遞一個<code>free</code> 回調函數的原因。

假如我們的資料結構隻由單個配置設定項組成,那麼這個釋放方法的一個最簡單的實作如下所示

然而一個更加現實的實作會調用一些執行更加複雜記憶體回收的函數,通過将void指針轉換為一些結構并且釋放構成該值的所有資源。

生成和加載RDB的回調方法需要建立(并加載)磁盤上的資料類型的表示。Redis 提供進階的API能夠自動在RDB檔案中存儲以下類型:

無符号64位整數

有符号64位整數

雙精度浮點數(double)

字元串

使用以上基本類型的哪一個取決于子產品找到的可行的表示。但是請注意,盡管整型和雙精度浮點數類型的值是以一個體系結構的方式被存儲和被加載并且是以位元組序 無關的方式。如果你使用原始的字元串生成RDB的API,例如,将結構儲存在磁盤上,你必須注意這樣細節。

下面是執行生成RDB和加載RDB的函數清單:

這些函數不需要對子產品進行任何錯誤檢查,總是能夠假定成功調用。

例如,設想我有一個原生類型,它實作了一個double類型值的數組,具有以下結構:

我的<code>rdb_save</code> 方法可能如下所示:

我們要做的是存儲每個double值的元素,是以當我們稍後必須去在 <code>rdb_load</code> 方法中加載結構時,我們将做如下的事情:

加載回調函數隻是将我們存儲在RDB檔案中的資料重建回原來的資料結構。

請注意,盡管沒有從磁盤寫操作和讀操作的API沒有的錯誤處理,但是加載回調函數仍然能在出錯時傳回NULL,以防讀操作沒有正确執行。在這種情況下,Redis僅僅會這樣。

子產品資料類型應該嘗試使用 <code>RedisModule_Alloc()</code> 函數族來配置設定,重新配置設定和釋放用于實作原生資料結構的堆記憶體。(詳細細節請檢視其他Redis子產品的文檔)

這不僅是為了Redis能夠有力的說明記憶體被子產品使用,而且還有更多的優點:

Redis使用 <code>jemalloc</code> 配置設定器,通常可以防止使用libc配置設定器所造成的記憶體碎片問題。

當從RDB檔案中加載字元串時,原生類型API能夠直接傳回用 <code>RedisModule_Alloc()</code> 配置設定的字元串,以便子產品可以直接連接配接這片記憶體到資料結構的表示上,避免一個進行無用的拷貝操作。

即使你使用外部庫是實作你的資料結構,但是由子產品API提供的記憶體配置設定函數能恰好與<code>malloc()</code>, <code>realloc()</code>, <code>free()</code> 和<code>strdup()</code> 相容,是以為了使用這些函數而更換庫是不重要的。

當你有一個使用libc庫的 <code>malloc()</code> 的外部庫,并且你想去避免手動的用Redis子產品API調用代替所有調用,那麼有一種方法是使用簡單的宏定義使用Redis 的API調用替換libc的調用。方法如下所示:

但是請記住,混合使用libc調用和Redis API調用将會導緻出錯和崩潰,是以如果你使用宏定義替換,你需要確定所有的調用都被正确替換,并且有被替換調用的代碼能夠從不交叉混合,例如,嘗試調用<code>RedisModule_Free()</code> 函數确使用一個由 <code>malloc()</code> 配置設定的指針。

本文作者:陳群

本文來自雲栖社群合作夥伴rediscn,了解相關資訊可以關注redis.cn網站。

上一篇: CLUSTER SLOTS

繼續閱讀