redis-對象

對象的類型與編碼

Redis 使用對象來表示數據庫中的鍵和值, 每次當咱們在 Redis 的數據庫中新建立一個鍵值對時, 咱們至少會建立兩個對象, 一個對象用做鍵值對的鍵(鍵對象), 另外一個對象用做鍵值對的值(值對象)。node

typedef struct redisObject {

    // 類型
    unsigned type:4;

    // 編碼
    unsigned encoding:4;

    // 指向底層實現數據結構的指針
    void *ptr;

    // 引用計數器用於垃圾回收
    int refount;
    
    //空轉時長,當前時間-上次活躍的時間
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */

} robj;
複製代碼

字符串對象

字符串對象的編碼能夠是 int 、 raw 或者 embstr 。若是一個字符串對象保存的是整數值, 而且這個整數值能夠用 long 類型來表示, 那麼字符串對象會將整數值保存在字符串對象結構的 ptr 屬性裏面(將 void* 轉換成 long ), 並將字符串對象的編碼設置爲 int 。 若是字符串對象保存的是一個字符串值, 而且這個字符串值的長度小於等於 39 字節, 那麼字符串對象將使用 embstr 編碼的方式來保存這個字符串值。redis

embstr 編碼是專門用於保存短字符串的一種優化編碼方式, 這種編碼和 raw 編碼同樣, 都使用 redisObject 結構和 sdshdr 結構來表示字符串對象, 但 raw 編碼會調用兩次內存分配函數來分別建立 redisObject 結構和 sdshdr 結構, 而 embstr 編碼則經過調用一次內存分配函數來分配一塊連續的空間, 空間中依次包含 redisObject 和 sdshdr 兩個結構, 如圖 8-3 所示。數據庫

embstr 編碼的字符串對象在執行命令時, 產生的效果和 raw 編碼的字符串對象執行命令時產生的效果是相同的, 但使用 embstr 編碼的字符串對象來保存短字符串值有如下好處:

embstr 編碼將建立字符串對象所需的內存分配次數從 raw 編碼的兩次下降爲一次。 釋放 embstr 編碼的字符串對象只須要調用一次內存釋放函數, 而釋放 raw 編碼的字符串對象須要調用兩次內存釋放函數。 由於 embstr 編碼的字符串對象的全部數據都保存在一塊連續的內存裏面, 因此這種編碼的字符串對象比起 raw 編碼的字符串對象可以更好地利用緩存帶來的優點。緩存

編碼轉換條件

int 編碼的字符串對象和 embstr 編碼的字符串對象在條件知足的狀況下, 會被轉換爲 raw 編碼的字符串對象。bash

對於 int 編碼的字符串對象來講, 若是咱們向對象執行了一些命令, 使得這個對象保存的再也不是整數值, 而是一個字符串值, 那麼字符串對象的編碼將從 int 變爲 raw 。服務器

在下面的示例中, 咱們經過 APPEND 命令, 向一個保存整數值的字符串對象追加了一個字符串值, 由於追加操做只能對字符串值執行, 因此程序會先將以前保存的整數值 10086 轉換爲字符串值 "10086" , 而後再執行追加操做, 操做的執行結果就是一個 raw 編碼的、保存了字符串值的字符串對象:數據結構

另外, 由於 Redis 沒有爲 embstr 編碼的字符串對象編寫任何相應的修改程序 (只有 int 編碼的字符串對象和 raw 編碼的字符串對象有這些程序), 因此 embstr 編碼的字符串對象其實是隻讀的: 當咱們對 embstr 編碼的字符串對象執行任何修改命令時, 程序會先將對象的編碼從 embstr 轉換成 raw , 而後再執行修改命令; 由於這個緣由, embstr 編碼的字符串對象在執行修改命令以後, 總會變成一個 raw 編碼的字符串對象。 如下代碼展現了一個 embstr 編碼的字符串對象在執行 APPEND 命令以後, 對象的編碼從 embstr 變爲 raw 的例子:

列表對象

列表對象的編碼能夠是 ziplist 或者 linkedlist 。函數

ziplist 編碼的列表對象使用壓縮列表做爲底層實現, 每一個壓縮列表節點(entry)保存了一個列表元素。 舉個例子, 若是咱們執行如下 RPUSH 命令, 那麼服務器將建立一個列表對象做爲 numbers 鍵的值:優化

若是 numbers 鍵的值對象使用的是 ziplist 編碼, 這個這個值對象將會是圖 8-5 所展現的樣子。

另外一方面, linkedlist 編碼的列表對象使用雙端鏈表做爲底層實現, 每一個雙端鏈表節點(node)都保存了一個字符串對象, 而每一個字符串對象都保存了一個列表元素。

舉個例子, 若是前面所說的 numbers 鍵建立的列表對象使用的不是 ziplist 編碼, 而是 linkedlist 編碼, 那麼 numbers 鍵的值對象將是圖 8-6 所示的樣子。ui

注意, linkedlist 編碼的列表對象在底層的雙端鏈表結構中包含了多個字符串對象, 這種嵌套字符串對象的行爲在稍後介紹的哈希對象、集合對象和有序集合對象中都會出現, 字符串對象是 Redis 五種類型的對象中惟一一種會被其餘四種類型對象嵌套的對象。

編碼轉換條件

當列表對象能夠同時知足如下兩個條件時, 列表對象使用 ziplist 編碼:

列表對象保存的全部字符串元素的長度都小於 64 字節; 列表對象保存的元素數量小於 512 個; 不能知足這兩個條件的列表對象須要使用 linkedlist 編碼。

以上兩個條件的上限值是能夠修改的, 具體請看配置文件中關於 list-max-ziplist-value 選項和 list-max-ziplist-entries 選項的說明。

哈希對象

哈希對象的編碼能夠是 ziplist 或者 hashtable 。

ziplist 編碼的哈希對象使用壓縮列表做爲底層實現, 每當有新的鍵值對要加入到哈希對象時, 程序會先將保存了鍵的壓縮列表節點推入到壓縮列表表尾, 而後再將保存了值的壓縮列表節點推入到壓縮列表表尾, 所以:

保存了同一鍵值對的兩個節點老是緊挨在一塊兒, 保存鍵的節點在前, 保存值的節點在後; 先添加到哈希對象中的鍵值對會被放在壓縮列表的表頭方向, 然後來添加到哈希對象中的鍵值對會被放在壓縮列表的表尾方向。 舉個例子, 若是咱們執行如下 HSET 命令, 那麼服務器將建立一個列表對象做爲 profile 鍵的值:

若是 profile 鍵的值對象使用的是 ziplist 編碼, 那麼這個值對象將會是圖 8-9 所示的樣子, 其中對象所使用的壓縮列表如圖 8-10 所示。

另外一方面, hashtable 編碼的哈希對象使用字典做爲底層實現, 哈希對象中的每一個鍵值對都使用一個字典鍵值對來保存:

字典的每一個鍵都是一個字符串對象, 對象中保存了鍵值對的鍵; 字典的每一個值都是一個字符串對象, 對象中保存了鍵值對的值。 舉個例子, 若是前面 profile 鍵建立的不是 ziplist 編碼的哈希對象, 而是 hashtable 編碼的哈希對象, 那麼這個哈希對象應該會是圖 8-11 所示的樣子。

編碼轉換條件

當哈希對象能夠同時知足如下兩個條件時, 哈希對象使用 ziplist 編碼:

哈希對象保存的全部鍵值對的鍵和值的字符串長度都小於 64 字節; 哈希對象保存的鍵值對數量小於 512 個; 不能知足這兩個條件的哈希對象須要使用 hashtable 編碼。

這兩個條件的上限值是能夠修改的, 具體請看配置文件中關於 hash-max-ziplist-value 選項和 hash-max-ziplist-entries 選項的說明。

集合對象

集合對象的編碼能夠是 intset 或者 hashtable 。

intset 編碼的集合對象使用整數集合做爲底層實現, 集合對象包含的全部元素都被保存在整數集合裏面。

舉個例子, 如下代碼將建立一個如圖 8-12 所示的 intset 編碼集合對象:

另外一方面, hashtable 編碼的集合對象使用字典做爲底層實現, 字典的每一個鍵都是一個字符串對象, 每一個字符串對象包含了一個集合元素, 而字典的值則所有被設置爲 NULL 。

舉個例子, 如下代碼將建立一個如圖 8-13 所示的 hashtable 編碼集合對象:

編碼轉換

當集合對象能夠同時知足如下兩個條件時, 對象使用 intset 編碼:

集合對象保存的全部元素都是整數值; 集合對象保存的元素數量不超過 512 個; 不能知足這兩個條件的集合對象須要使用 hashtable 編碼。

第二個條件的上限值是能夠修改的, 具體請看配置文件中關於 set-max-intset-entries 選項的說明。

有序集合對象

有序集合的編碼能夠是 ziplist 或者 skiplist 。

ziplist 編碼的有序集合對象使用壓縮列表做爲底層實現, 每一個集合元素使用兩個緊挨在一塊兒的壓縮列表節點來保存, 第一個節點保存元素的成員(member), 而第二個元素則保存元素的分值(score)。

壓縮列表內的集合元素按分值從小到大進行排序, 分值較小的元素被放置在靠近表頭的方向, 而分值較大的元素則被放置在靠近表尾的方向。

舉個例子, 若是咱們執行如下 ZADD 命令, 那麼服務器將建立一個有序集合對象做爲 price 鍵的值:

若是 price 鍵的值對象使用的是 ziplist 編碼, 那麼這個值對象將會是圖 8-14 所示的樣子, 而對象所使用的壓縮列表則會是 8-15 所示的樣子。

skiplist 編碼的有序集合對象使用 zset 結構做爲底層實現, 一個 zset 結構同時包含一個字典和一個跳躍表:

typedef struct zset {

    zskiplist *zsl;

    dict *dict;

} zset;
複製代碼

編碼轉換條件

當有序集合對象能夠同時知足如下兩個條件時, 對象使用 ziplist 編碼:

有序集合保存的元素數量小於 128 個; 有序集合保存的全部元素成員的長度都小於 64 字節; 不能知足以上兩個條件的有序集合對象將使用 skiplist 編碼。

以上兩個條件的上限值是能夠修改的, 具體請看配置文件中關於 zset-max-ziplist-entries 選項和 zset-max-ziplist-value 選項的說明。

類型檢查

從上面發生類型錯誤的代碼示例能夠看出, 爲了確保只有指定類型的鍵能夠執行某些特定的命令, 在執行一個類型特定的命令以前, Redis 會先檢查輸入鍵的類型是否正確, 而後再決定是否執行給定的命令。

類型特定命令所進行的類型檢查是經過 redisObject 結構的 type 屬性來實現的:

在執行一個類型特定命令以前, 服務器會先檢查輸入數據庫鍵的值對象是否爲執行命令所需的類型, 若是是的話, 服務器就對鍵執行指定的命令; 不然, 服務器將拒絕執行命令, 並向客戶端返回一個類型錯誤。 舉個例子, 對於 LLEN 命令來講:

在執行 LLEN 命令以前, 服務器會先檢查輸入數據庫鍵的值對象是否爲列表類型, 也便是, 檢查值對象 redisObject 結構 type 屬性的值是否爲 REDIS_LIST , 若是是的話, 服務器就對鍵執行 LLEN 命令; 不然的話, 服務器就拒絕執行命令並向客戶端返回一個類型錯誤; 圖 8-18 展現了這一類型檢查過程。

其餘類型特定命令的類型檢查過程也和這裏展現的 LLEN 命令的類型檢查過程相似。

多態命令的實現

Redis 除了會根據值對象的類型來判斷鍵是否可以執行指定命令以外, 還會根據值對象的編碼方式, 選擇正確的命令實現代碼來執行命令。

舉個例子, 在前面介紹列表對象的編碼時咱們說過, 列表對象有 ziplist 和 linkedlist 兩種編碼可用, 其中前者使用壓縮列表 API 來實現列表命令, 然後者則使用雙端鏈表 API 來實現列表命令。

如今, 考慮這樣一個狀況, 若是咱們對一個鍵執行 LLEN 命令, 那麼服務器除了要確保執行命令的是列表鍵以外, 還須要根據鍵的值對象所使用的編碼來選擇正確的 LLEN 命令實現:

若是列表對象的編碼爲 ziplist , 那麼說明列表對象的實現爲壓縮列表, 程序將使用 ziplistLen 函數來返回列表的長度; 若是列表對象的編碼爲 linkedlist , 那麼說明列表對象的實現爲雙端鏈表, 程序將使用 listLength 函數來返回雙端鏈表的長度; 借用面向對象方面的術語來講, 咱們能夠認爲 LLEN 命令是多態(polymorphism)的: 只要執行 LLEN 命令的是列表鍵, 那麼不管值對象使用的是 ziplist 編碼仍是 linkedlist 編碼, 命令均可以正常執行。

圖 8-19 展現了 LLEN 命令從類型檢查到根據編碼選擇實現函數的整個執行過程, 其餘類型特定命令的執行過程也是相似的。

內存回收

由於 C 語言並不具有自動的內存回收功能, 因此 Redis 在本身的對象系統中構建了一個引用計數(reference counting)技術實現的內存回收機制, 經過這一機制, 程序能夠經過跟蹤對象的引用計數信息, 在適當的時候自動釋放對象並進行內存回收。

每一個對象的引用計數信息由 redisObject 結構的 refcount 屬性記錄:

對象的引用計數信息會隨着對象的使用狀態而不斷變化:

在建立一個新對象時, 引用計數的值會被初始化爲 1 ; 當對象被一個新程序使用時, 它的引用計數值會被增一; 當對象再也不被一個程序使用時, 它的引用計數值會被減一; 當對象的引用計數值變爲 0 時, 對象所佔用的內存會被釋放。

對象共享

目前來講, Redis 會在初始化服務器時, 建立一萬個字符串對象, 這些對象包含了從 0 到 9999 的全部整數值, 當服務器須要用到值爲 0 到 9999 的字符串對象時, 服務器就會使用這些共享對象, 而不是新建立對象。

建立共享字符串對象的數量能夠經過修改 redis.h/REDIS_SHARED_INTEGERS 常量來修改。

爲何 Redis 不共享包含字符串的對象?

當服務器考慮將一個共享對象設置爲鍵的值對象時, 程序須要先檢查給定的共享對象和鍵想建立的目標對象是否徹底相同, 只有在共享對象和目標對象徹底相同的狀況下, 程序纔會將共享對象用做鍵的值對象, 而一個共享對象保存的值越複雜, 驗證共享對象和目標對象是否相同所需的複雜度就會越高, 消耗的 CPU 時間也會越多:

若是共享對象是保存整數值的字符串對象, 那麼驗證操做的複雜度爲 O(1) ;
若是共享對象是保存字符串值的字符串對象, 那麼驗證操做的複雜度爲 O(N) ;
若是共享對象是包含了多個值(或者對象的)對象, 好比列表對象或者哈希對象, 那麼驗證操做的複雜度將會是 O(N^2) 。
所以, 儘管共享更復雜的對象能夠節約更多的內存, 但受到 CPU 時間的限制, Redis 只對包含整數值的字符串對象進行共享。
複製代碼

對象的空轉時長

除了前面介紹過的 type 、 encoding 、 ptr 和 refcount 四個屬性以外, redisObject 結構包含的最後一個屬性爲 lru 屬性, 該屬性記錄了對象最後一次被命令程序訪問的時間:

OBJECT IDLETIME 命令能夠打印出給定鍵的空轉時長, 這一空轉時長就是經過將當前時間減去鍵的值對象的 lru 時間計算得出的: OBJECT IDLETIME 命令的實現是特殊的, 這個命令在訪問鍵的值對象時, 不會修改值對象的 lru 屬性。

重點回顧

  • Redis 數據庫中的每一個鍵值對的鍵和值都是一個對象。
  • Redis 共有字符串、列表、哈希、集合、有序集合五種類型的對象, 每種類型的對象至少都有兩種或以上的編碼方式, 不一樣的編碼能夠在不一樣的使用場景上優化對象的使用效率。
  • 服務器在執行某些命令以前, 會先檢查給定鍵的類型可否執行指定的命令, 而檢查一個鍵的類型就是檢查鍵的值對象的類型。
  • Redis 的對象系統帶有引用計數實現的內存回收機制, 當一個對象再也不被使用時, 該對象所佔用的內存就會被自動釋放。
  • Redis 會共享值爲 0 到 9999 的字符串對象。
  • 對象會記錄本身的最後一次被訪問的時間, 這個時間能夠用於計算對象的空轉時間。
相關文章
相關標籤/搜索