標籤:

圖解Redis之對象篇

在圖解Redis之數據結構篇中,我們簡單介紹了Redis用到的基本數據結構,然而Redis並沒直接使用這些數據結構去實現key-value pair資料庫,而是基於這些數據結構實現了一系列對象.這些對象有:字元串對象,列表對象,集合對象,有序集合對象,哈希對象 五種基本對象.

通過這五種對象類型,Redis可以在處理用戶命令前,根據對象類型檢查輸入是否合法,若不符,則拒絕執行;此外,可以針對不同的場景,為對象設置不同的數據結構,優化了對象在不同場景的效率.而由於redis的對象系統還實現了基於引用計數的內存回收機制,Redis可以很輕鬆的解決對象回收和共享問題;最後Redis對象還帶有訪問時間信息,可以用來判斷資料庫鍵的空轉時間(拒上一次訪問),在伺服器開啟maxmemory後,空轉時間較長的那些鍵將優先被回收,下面讓我們看看redis是如何表示對象.

typedef struct redisObject { unsigned type:4; //// 類型 unsigned encoding:4;// 編碼方式 void *ptr; // 指向對象的值 int refcount;// 引用計數,可用來對象回收與共享 unsigned lru:22;//對象最後一次被訪問的時間

  • 類型type

    類型常量對象的名稱REDIS_STRING字元串對象REDIS_LIST列表對象REDIS_HASH哈希對象REDIS_SET集合對象REDIS_ZSET有序集合對象對於redis鍵值對來說,鍵總是保持一個字元換值

  • 編碼方式

不同的編碼方式對命令的處理方式是不同的,對象使用編碼方式也體現了它的多態性.通過命令object encoding key 可以查看值的編碼類型

了解到這,我們可以真正開始學習Redis五種對象類型了

  1. 字元串對象

字元串對象的編碼可以是int、raw或者embstr。

如果一個字元串的內容可以轉換為long,那麼該字元串就會被轉換成為long類型,對象的ptr就會指向該long,並且對象類型也用int類型表示。如果內容不是整數類型,則是普通字元串,讓我們看看下面代碼

#define REDIS_ENCODING_EMBSTR_SIZE_LIMIT 39 robj *createStringObject(char *ptr, size_t len) { if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT) return createEmbeddedStringObject(ptr,len); else return createRawStringObject(ptr,len); }

當字元串在EMBSTR_SIZE_LIMIT範圍(32位元組)內,編碼類型為embstr,否則為raw.

embstr編碼是專門用於保存短字元串的一種優化編碼.這種編碼和raw一樣,都使用redisobject結構和sdshdr結構來表示字元串對象,但raw編碼執行兩次內存分配函數來分別創建redis0bject結構和sdshdr結構來表示字元串對象.而embstr編調用一次內存分配函數來分配一塊連續的空間,空間中依次包含redisobject和sdshdr兩個結構.可借用<<Redis設計與實現>>中圖來表示

2. 列表對象

列表對象的編碼方式又可分為ziplist或者linkedlist.

ziplist是一種壓縮鏈表,它的好處是更能節省內存空間,因為它所存儲的內容都是在連續的內存區域當中的。當列表對象元素不多,每個元素也不大的時候,就採用ziplist存儲。但當數據量過大時就ziplist就不是那麼好用了。因為為了保證他存儲內容在內存中的連續性,插入的複雜度是O(N),即每次插入都會重新進行realloc。如下圖所示,對象結構中ptr所指向的就是一個ziplist。整個ziplist只需要malloc一次,它們在內存中是一塊連續的區域

編碼轉化:當同時滿足以下條件時使用ziplist

  • 列表對象保存的所有元素長度小於64位元組

  • 列表對象保存的元素數量不超過512個

3. 哈希對象

哈希對象的底層實現可以是ziplist或者hashtable。

若是通過zipList實現,則是按照插入順序以key1 value1,key1 value1,key1 value1,key1 value1一次插入,他們緊密相連,而如果是hashtable編碼方式,因為hashtable是通過字典實現,所以哈希對象中的鍵值對都使用一個字典鍵值對來保存.再次借用redis設計與實現中的插圖

當滿足以下所有條件時,採用ziplist編碼

  • 哈希對象中所有鍵和值長度都小於64位元組

  • 哈希對象中鍵值對數量不超過512個

4. 集合對象

集合對象的編碼可以是intset或者hashtable。

intset是一個整數集合,裡面存的為某種同一類型的整數,支持如下三種長度的整數:

#define INTSET_ENC_INT16 (sizeof(int16_t)) #define INTSET_ENC_INT32 (sizeof(int32_t)) #define INTSET_ENC_INT64 (sizeof(int64_t))

intset是一個有序集合,查找元素的複雜度為O(logN),但插入時不一定為O(logN),因為有可能涉及到升級操作。比如當集合里全是int16_t型的整數,這時要插入一個int32_t,那麼為了維持集合中數據類型的一致,那麼所有的數據都會被轉換成int32_t類型,涉及到內存的重新分配,這時插入的複雜度就為O(N)了

當滿足以下所有條件時,採用intset編碼

  • 集合元素全為int

  • 集合元素數量不超過512個

5. 有序集合

有序集合的編碼可能兩種,一種是ziplist,另一種是skiplist.

ziplist作為有序集合和作為哈希對象類似,member和score順序存放。按照score從小到大順序排列。

當有序集合對象使用skiplist編碼時,對象底層用zset作為實現

typedef struct zset { dict *dict; // 字典 zskiplist *zsl; // 跳躍表 } zset;

那麼zset為什麼要同時使用字典與跳躍表呢?

  • 只使用字典,可O(1)效率查找元素,但集合是無序的,所以每次進行範圍操作如zrange時,需要先排序

  • 只使用跳躍表,範圍操作方便,因為它根據score排序,但是查找起來性能又退化為O(logN)了

因為redis使用字典和跳躍表的結合體來處理

編碼轉換:當滿足以下所有條件時,採用ziplist編碼:

  • 有序集合元素數量小於128個
  • 所有元素長度小於64位元組

到這裡,Redis的五種對象類型已經介紹完畢,接下來學習對象的共享,回收,空轉時長和命令多態

  • 類型檢查與命令多態

Redis命令可分為兩種:

  • 通用命令,對各種類型的鍵都可執行,如del,type,object,RENAME等
  • 特定類型命令,比如
    • SET,GET,APPEND,STRLEN 等僅限用於字元串的鍵類型;
    • HDEL,HSET,HGET,HLEN 等僅限用於哈希鍵類型;
    • RPUSH,LPOP,LINSERT,LLEN 等僅限用於列表鍵類型;
    • SADD,SPOP,SINTER,SCARD 等僅限用於集合鍵類型;
    • ZADD,ZCARD,ZRANK,ZSCORE 等僅限用於有序集合鍵類型;

  • 內存回收

Redis在自己的對象系統中構建了一個引用計數技術實現的內存回收機制,通過這一機制,程序可以通過跟蹤對象的引用計數信息,在適當的時候自動釋放對象並進行內存回收。每個對象的引用計數信息由redis對象結構的refcount屬性記錄,創建一個新對象時,引用計數值會初始化為1;對象被一個新程序使用時,它的引用計數值會被增1;不再被一個程序使用時,減1;引用計數值變為0,對象所佔用的內存會被釋放

  • 對象共享

對象的引用計數屬性還帶有對象共享的作用,如果鍵A創建了一個包含整數值100的字元串對象作為值對象,那麼鍵B只要指向A的redisObject,然後將B的refcount+1,就可以和A共享內存.共享對象機制對於節約內存非常有幫助,資料庫中保存的相同值對象越多,對象共享機制就能節約越多的內存。

redis默認對整數型字元串對象進行共享.那麼為什麼只對整數型進行共享呢?

  • 如果共享對象是保存整數值的字元串對象,驗證操作的複雜度O(1)

  • 如果共享對象是保存字元串值的字元串對象,驗證操作的複雜度O(N)

  • 如果共享對象是包含多個值(比如列表對象或者哈希對象)對象,驗證操作的複雜度O(N^2)

故儘管共享更複雜的對象可以節約更多的內存,但受到CPU時間的限制,Redis只對包含整數值得字元串對象進行共享。

  • 空轉時長

當前時間減去lru即為空轉時長,可用OBJECT IDLETIME列印出.

在伺服器開啟maxmemory後,當伺服器內存佔用超過maxmemory設置的上線,並且伺服器使用volatile-lru或者allkey-lru演算法時,空轉時長較高的那部分鍵將優先被釋放.

今天專題到這就結束了,可繼續閱讀我的 圖解Redis之持久化篇

如果喜歡我的文章,歡迎點擊關注.

參考資料:<<Redis設計與實現>>


推薦閱讀:

Redis 哈希槽的概念,到底是什麼?是一個表的表名就會佔用一個哈希槽?
使用telnet連接redis
redis的setbit這個bit怎麼理解,配合bitcount使用?
Redis源碼剖析--源碼結構解析

TAG:Redis |