前面兩篇博客,第一篇介紹了五大數據類型的基本用法,第二篇介紹了Redis底層的六種數據結構。在Redis中,並無直接使用這些數據結構來實現鍵值對數據庫,而是基於這些數據結構建立了一個對象系統,這些對象系統也就是前面說的五大數據類型,每一種數據類型都至少用到了一種數據結構。經過這五種不一樣類型的對象,Redis能夠在執行命令以前,根據對象的類型判斷一個對象是否能夠執行給定的命令,並且能夠針對不一樣的場景,爲對象設置多種不一樣的數據結構,從而優化對象在不一樣場景下的使用效率。html
Redis使用前面說的五大數據類型來表示鍵和值,每次在Redis數據庫中建立一個鍵值對時,至少會建立兩個對象,一個是鍵對象,一個是值對象,而Redis中的每一個對象都是由 redisObject 結構來表示:redis
typedef struct redisObject{ //類型 unsigned type:4; //編碼 unsigned encoding:4; //指向底層數據結構的指針 void *ptr; //引用計數 int refcount; //記錄最後一次被程序訪問的時間 unsigned lru:22; }robj
對象的type屬性記錄了對象的類型,這個類型就是前面講的五大數據類型:算法
能夠經過以下命令來判斷對象類型:數據庫
type key
注意:在Redis中,鍵老是一個字符串對象,而值能夠是字符串、列表、集合等對象,因此咱們一般說的鍵爲字符串鍵,表示的是這個鍵對應的值爲字符串對象,咱們說一個鍵爲集合鍵時,表示的是這個鍵對應的值爲集合對象。安全
對象的 prt 指針指向對象底層的數據結構,而數據結構由 encoding 屬性來決定。數據結構
而每種類型的對象都至少使用了兩種不一樣的編碼:app
能夠經過以下命令查看值對象的編碼:dom
OBJECT ENCODING key
好比 string 類型:(能夠是 embstr編碼的簡單字符串或者是 int 整數值實現)分佈式
字符串是Redis最基本的數據類型,不只全部key都是字符串類型,其它幾種數據類型構成的元素也是字符串。注意字符串的長度不能超過512M。性能
①、編碼
字符串對象的編碼能夠是int,raw或者embstr。
一、int 編碼:保存的是能夠用 long 類型表示的整數值。
二、raw 編碼:保存長度大於44字節的字符串(redis3.2版本以前是39字節,以後是44字節)。
三、embstr 編碼:保存長度小於44字節的字符串(redis3.2版本以前是39字節,以後是44字節)。
由上能夠看出,int 編碼是用來保存整數值,raw編碼是用來保存長字符串,而embstr是用來保存短字符串。其實 embstr 編碼是專門用來保存短字符串的一種優化編碼,raw 和 embstr 的區別:
embstr與raw都使用redisObject和sds保存數據,區別在於,embstr的使用只分配一次內存空間(所以redisObject和sds是連續的),而raw須要分配兩次內存空間(分別爲redisObject和sds分配空間)。所以與raw相比,embstr的好處在於建立時少分配一次空間,刪除時少釋放一次空間,以及對象的全部數據連在一塊兒,尋找方便。而embstr的壞處也很明顯,若是字符串的長度增長鬚要從新分配內存時,整個redisObject和sds都須要從新分配空間,所以redis中的embstr實現爲只讀。
ps:Redis中對於浮點數類型也是做爲字符串保存的,在須要的時候再將其轉換成浮點數類型。
②、編碼的轉換
當 int 編碼保存的值再也不是整數,或大小超過了long的範圍時,自動轉化爲raw。
對於 embstr 編碼,因爲 Redis 沒有對其編寫任何的修改程序(embstr 是隻讀的),在對embstr對象進行修改時,都會先轉化爲raw再進行修改,所以,只要是修改embstr對象,修改後的對象必定是raw的,不管是否達到了44個字節。
list 列表,它是簡單的字符串列表,按照插入順序排序,你能夠添加一個元素到列表的頭部(左邊)或者尾部(右邊),它的底層其實是個鏈表結構。
①、編碼
列表對象的編碼能夠是 ziplist(壓縮列表) 和 linkedlist(雙端鏈表)。 關於鏈表和壓縮列表的特性能夠看我前面的這篇博客。
好比咱們執行如下命令,建立一個 key = ‘numbers’,value = ‘1 three 5’ 的三個值的列表。
rpush numbers 1 "three" 5
ziplist 編碼表示以下:
linkedlist表示以下:
②、編碼轉換
當同時知足下面兩個條件時,使用ziplist(壓縮列表)編碼:
一、列表保存元素個數小於512個
二、每一個元素長度小於64字節
不能知足這兩個條件的時候使用 linkedlist 編碼。
上面兩個條件能夠在redis.conf 配置文件中的 list-max-ziplist-value選項和 list-max-ziplist-entries 選項進行配置。
哈希對象的鍵是一個字符串類型,值是一個鍵值對集合。
①、編碼
哈希對象的編碼能夠是 ziplist 或者 hashtable。
當使用ziplist,也就是壓縮列表做爲底層實現時,新增的鍵值對是保存到壓縮列表的表尾。好比執行如下命令:
hset profile name "Tom" hset profile age 25 hset profile career "Programmer"
若是使用ziplist,profile 存儲以下:
當使用 hashtable 編碼時,上面命令存儲以下:
hashtable 編碼的哈希表對象底層使用字典數據結構,哈希對象中的每一個鍵值對都使用一個字典鍵值對。
在前面介紹壓縮列表時,咱們介紹過壓縮列表是Redis爲了節省內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型數據結構,相對於字典數據結構,壓縮列表用於元素個數少、元素長度小的場景。其優點在於集中存儲,節省空間。
②、編碼轉換
和上面列表對象使用 ziplist 編碼同樣,當同時知足下面兩個條件時,使用ziplist(壓縮列表)編碼:
一、列表保存元素個數小於512個
二、每一個元素長度小於64字節
不能知足這兩個條件的時候使用 hashtable 編碼。第一個條件能夠經過配置文件中的 set-max-intset-entries 進行修改。
集合對象 set 是 string 類型(整數也會轉換成string類型進行存儲)的無序集合。注意集合和列表的區別:集合中的元素是無序的,所以不能經過索引來操做元素;集合中的元素不能有重複。
①、編碼
集合對象的編碼能夠是 intset 或者 hashtable。
intset 編碼的集合對象使用整數集合做爲底層實現,集合對象包含的全部元素都被保存在整數集合中。
hashtable 編碼的集合對象使用 字典做爲底層實現,字典的每一個鍵都是一個字符串對象,這裏的每一個字符串對象就是一個集合中的元素,而字典的值則所有設置爲 null。這裏能夠類比Java集合中HashSet 集合的實現,HashSet 集合是由 HashMap 來實現的,集合中的元素就是 HashMap 的key,而 HashMap 的值都設爲 null。
SADD numbers 1 3 5
SADD Dfruits "apple" "banana" "cherry"
②、編碼轉換
當集合同時知足如下兩個條件時,使用 intset 編碼:
一、集合對象中全部元素都是整數
二、集合對象全部元素數量不超過512
不能知足這兩個條件的就使用 hashtable 編碼。第二個條件能夠經過配置文件的 set-max-intset-entries 進行配置。
和上面的集合對象相比,有序集合對象是有序的。與列表使用索引下標做爲排序依據不一樣,有序集合爲每一個元素設置一個分數(score)做爲排序依據。
①、編碼
有序集合的編碼能夠是 ziplist 或者 skiplist。
ziplist 編碼的有序集合對象使用壓縮列表做爲底層實現,每一個集合元素使用兩個緊挨在一塊兒的壓縮列表節點來保存,第一個節點保存元素的成員,第二個節點保存元素的分值。而且壓縮列表內的集合元素按分值從小到大的順序進行排列,小的放置在靠近表頭的位置,大的放置在靠近表尾的位置。
ZADD price 8.5 apple 5.0 banana 6.0 cherry
skiplist 編碼的有序集合對象使用 zet 結構做爲底層實現,一個 zset 結構同時包含一個字典和一個跳躍表:
typedef struct zset{ //跳躍表 zskiplist *zsl; //字典 dict *dice; } zset;
字典的鍵保存元素的值,字典的值則保存元素的分值;跳躍表節點的 object 屬性保存元素的成員,跳躍表節點的 score 屬性保存元素的分值。
這兩種數據結構會經過指針來共享相同元素的成員和分值,因此不會產生重複成員和分值,形成內存的浪費。
說明:其實有序集合單獨使用字典或跳躍表其中一種數據結構均可以實現,可是這裏使用兩種數據結構組合起來,緣由是假如咱們單獨使用 字典,雖然能以 O(1) 的時間複雜度查找成員的分值,可是由於字典是以無序的方式來保存集合元素,因此每次進行範圍操做的時候都要進行排序;假如咱們單獨使用跳躍表來實現,雖然能執行範圍操做,可是查找操做有 O(1)的複雜度變爲了O(logN)。所以Redis使用了兩種數據結構來共同實現有序集合。
②、編碼轉換
當有序集合對象同時知足如下兩個條件時,對象使用 ziplist 編碼:
一、保存的元素數量小於128;
二、保存的全部元素長度都小於64字節。
不能知足上面兩個條件的使用 skiplist 編碼。以上兩個條件也能夠經過Redis配置文件zset-max-ziplist-entries 選項和 zset-max-ziplist-value 進行修改。
對於string 數據類型,由於string 類型是二進制安全的,能夠用來存放圖片,視頻等內容,另外因爲Redis的高性能讀寫功能,而string類型的value也能夠是數字,能夠用做計數器(INCR,DECR),好比分佈式環境中統計系統的在線人數,秒殺等。
對於 hash 數據類型,value 存放的是鍵值對,好比能夠作單點登陸存放用戶信息。
對於 list 數據類型,能夠實現簡單的消息隊列,另外能夠利用lrange命令,作基於redis的分頁功能
對於 set 數據類型,因爲底層是字典實現的,查找元素特別快,另外set 數據類型不容許重複,利用這兩個特性咱們能夠進行全局去重,好比在用戶註冊模塊,判斷用戶名是否註冊;另外就是利用交集、並集、差集等操做,能夠計算共同喜愛,所有的喜愛,本身獨有的喜愛等功能。
對於 zset 數據類型,有序的集合,能夠作範圍查找,排行榜應用,取 TOP N 操做等。
前面講 Redis 的每一個對象都是由 redisObject 結構表示:
typedef struct redisObject{ //類型 unsigned type:4; //編碼 unsigned encoding:4; //指向底層數據結構的指針 void *ptr; //引用計數 int refcount; //記錄最後一次被程序訪問的時間 unsigned lru:22; }robj
其中關鍵的 type屬性,encoding 屬性和 ptr 指針都介紹過了,那麼 refcount 屬性是幹什麼的呢?
由於 C 語言不具有自動回收內存功能,那麼該如何回收內存呢?因而 Redis本身構建了一個內存回收機制,經過在 redisObject 結構中的 refcount 屬性實現。這個屬性會隨着對象的使用狀態而不斷變化:
一、建立一個新對象,屬性 refcount 初始化爲1
二、對象被一個新程序使用,屬性 refcount 加 1
三、對象再也不被一個程序使用,屬性 refcount 減 1
四、當對象的引用計數值變爲 0 時,對象所佔用的內存就會被釋放。
在 Redis 中經過以下 API 來實現:
學過Java的應該知道,引用計數的內存回收機制實際上是不被Java採用的,由於不能克服循環引用的例子(好比 A 具備 B 的引用,B 具備 C 的引用,C 具備 A 的引用,除此以外,這三個對象沒有任何用處了),這時候 A B C 三個對象會一直駐留在內存中,形成內存泄露。那麼 Redis 既然採用引用計數的垃圾回收機制,如何解決這個問題呢?
在前面介紹 redis.conf 配置文件時,在 MEMORY MANAGEMENT 下有個 maxmemory-policy 配置:
maxmemory-policy :當內存使用達到最大值時,redis使用的清楚策略。有如下幾種能夠選擇:
1)volatile-lru 利用LRU算法移除設置過過時時間的key (LRU:最近使用 Least Recently Used )
2)allkeys-lru 利用LRU算法移除任何key
3)volatile-random 移除設置過過時時間的隨機key
4)allkeys-random 移除隨機key
5)volatile-ttl 移除即將過時的key(minor TTL)
6)noeviction noeviction 不移除任何key,只是返回一個寫錯誤 ,默認選項
經過這種配置,也能夠對內存進行回收。
refcount 屬性除了能實現內存回收之外,還能用於內存共享。
好比經過以下命令 set k1 100,建立一個鍵爲 k1,值爲100的字符串對象,接着經過以下命令 set k2 100 ,建立一個鍵爲 k2,值爲100 的字符串對象,那麼 Redis 是如何作的呢?
一、將數據庫鍵的值指針指向一個現有值的對象
二、將被共享的值對象引用refcount 加 1
注意:Redis的共享對象目前只支持整數值的字符串對象。之因此如此,其實是對內存和CPU(時間)的平衡:共享對象雖然會下降內存消耗,可是判斷兩個對象是否相等卻須要消耗額外的時間。對於整數值,判斷操做複雜度爲O(1);對於普通字符串,判斷複雜度爲O(n);而對於哈希、列表、集合和有序集合,判斷的複雜度爲O(n^2)。
雖然共享對象只能是整數值的字符串對象,可是5種類型均可能使用共享對象(如哈希、列表等的元素可使用)。
在 redisObject 結構中,前面介紹了 type、encoding、ptr 和 refcount 屬性,最後一個 lru 屬性,該屬性記錄了對象最後一次被命令程序訪問的時間。
使用 OBJECT IDLETIME 命令能夠打印給定鍵的空轉時長,經過將當前時間減去值對象的 lru 時間計算獲得。
lru 屬性除了計算空轉時長之外,還能夠配合前面內存回收配置使用。若是Redis打開了maxmemory選項,且內存回收算法選擇的是volatile-lru或allkeys—lru,那麼當Redis內存佔用超過maxmemory指定的值時,Redis會優先選擇空轉時間最長的對象進行釋放。
參考文章:《Redis設計與實現》