圖解Redis之對象篇
通過這五種對象類型,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五種對象類型了
- 字元串對象
字元串對象的編碼可以是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 |