Redis數據結構(1)——redisObject對象和string

前言

該系列重點講解Redis在內存中的數據結構實現(暫不涉及基礎api)。Redis本質上是一個數據結構服務器(data structures server),以高效的方式實現了多種現成的數據結構,研究它的數據結構和基於其上的算法,對於我們自己提升局部算法的編程水平有很重要的參考意義。

當我們在本文中提到Redis的“數據結構”,可能是在兩個不同的層面來討論它。
第一個層面,是從使用者的角度。比如:
string
list
hash
set
sorted set
這一層面也是Redis暴露給外部的調用接口。
第二個層面,是從內部實現的角度,屬於更底層的實現。比如:
dict
sds
ziplist
quicklist
skiplist

 

目錄:
1  redisObject對象
2  string
    2.1 int編碼 
    2.2 簡單動態字符串(sds)
        2.2.1  SDS 結構
        2.2.2  raw編碼(長度<=39)
        2.2.3  embstr編碼(長度>39)
        2.2.4  embstr 和 raw 編碼區別
        2.2.5  預分配機制
        
注1:整數對象共享池

redisObject對象:

本篇主要講string 和 sds,開始之前有必要講下redis  redisObject對象。Redis存儲的數據都使用redisObject來封裝(如圖),包括string、hash、list、set、zset在內的所有數據類型。下面針對每個字段做詳細說明:

  • type字段:表示當前對象使用的數據類型,Redis主要支持5種數據類型:string、hash、list、set、zset。可以使用type{key}命令查看對象所屬類型,type命令返回的是值對象類型,鍵都是string類型。
  • encoding字段:表示Redis內部編碼類型,encoding在Redis內部使用,代表當前對象內部採用哪種數據結構實現。理解Redis內部編碼方式對於優化內存非常重要,同一個對象採用不同的編碼實現內存佔用存在明顯差異。
  • lru字段:記錄對象最後一次被訪問的時間,當配置了maxmemory和maxmemory-policy=volatile-lru或者allkeys-lru時,用於輔助LRU算法刪除鍵數據。可以使用object idletime{key}命令在不更新lru字段情況下查看當前鍵的空閒時間(object idletime{key}=當前時間-lru記錄時間 =空閒時間)。   注:可以使用scan+object idletime命令批量查詢哪些鍵長時間未被訪問,找出長時間不訪問的鍵進行清理,可降低內存佔用
  • refcount字段:記錄當前對象被引用的次數,用於通過引用次數回收內存,當refcount=0時,可以安全回收當前對象空間。使用object refcount{key}獲取當前對象引用。當對象爲整數且範圍在[0-9999]時,Redis可以使用共享對象的方式來節省內存(見  1
  • *ptr字段:與對象的數據內容相關,如果是整數,直接存儲數據;否則表示指向數據的指針。Redis在3.0之後對值對象是字符串且長度<=39字節的數據,內部編碼爲embstr類型,字符串sds和redisObject一起分配,從而只要一次內存操作即可。

 

 string

      字符串類型是Redis最基礎的數據結構。鍵都是字符串類型,而且其他幾種數據結構都是在字符串類型基礎上構建的,值最大不能超過512MB。

 內部編碼:字符串對象的編碼可以是 int(數字時) 、 raw(字符長度>39) 或者 embstr (字符長度<=39)  。

 

1.int編碼

如果一個字符串對象保存的是整數值, 並且這個整數值可以用 long 類型來表示, 那麼字符串對象會將整數值保存在字符串對象結構的 ptr 屬性裏面(將 void* 轉換成 long ), 並將字符串對象的編碼設置爲 int 。(值得注意的是,如果數值在[0,9999] redis使用的是整數緩存池的(見  1

舉個例子, 如果我們執行以下 SET 命令, 那麼服務器將創建一個 int 編碼的字符串對象作爲 number 鍵的值:

redis> SET number 10086
OK
redis> OBJECT ENCODING number  (返回具體編碼類型)
"int"

如圖:

 

2.簡單動態字符串(simple dynamic string)SDS (raw 和 embstr內部實現,內存分配方式有區別)

2.1 SDS 結構

/*  
 * 保存字符串對象的結構  (sds)
 */  
struct sdshdr {  
      
    // buf 中已佔用空間的長度  
    int len;  
  
    // buf 中剩餘可用空間的長度  (初次申請內存空間爲0)
    int free;  
  
    // 數據空間  
    char buf[];  
};  

2.2 raw編碼

redis> SET story "Long, long, long ago there lived a king ..."
OK
redis> STRLEN story
(integer) 43 
redis> OBJECT ENCODING story
"raw"  //長度大於39編碼方式爲  raw

如圖:

2.3 embstr編碼

redis> SET msg "hello"
OK
redis> OBJECT ENCODING msg
"embstr"

如圖:

2.4 embstr 和 raw 編碼區別(最主要的就是embstr創建字符串redisObject對象的時候直接分配字符串內存空間了)

  • embstr 編碼將創建字符串對象所需的內存分配次數從 raw 編碼的兩次降低爲一次。
  • 釋放 embstr 編碼的字符串對象只需要調用一次內存釋放函數, 而釋放 raw 編碼的字符串對象需要調用兩次內存釋放函數。
  • 因爲 embstr 編碼的字符串對象的所有數據都保存在一塊連續的內存裏面, 所以這種編碼的字符串對象比起 raw 編碼的字符串對象能夠更好地利用緩存帶來的優勢。

2.5  預分配機制

在字符串拼接的時候如append、setrange操作會引起 SDS 擴容進行內存空間預分配,這樣帶來的一個好處就是  減少修改字符串時帶來的內存重分配次數

如:操作一set 一個60字節長度字符串

階段1插入新的字符串後,free字段保留空間爲0,總佔用空間=實際佔用空間+1字節,最後1字節保存‘\0’標示結尾

操作二  append 60 字節

追加操作後字符串對象預分配了一倍容量作爲預留空間(並不是所有情況都擴容一倍,見下文),而且大量追加操作需要內存重新分配,造成內存碎片率上升。

操作三  直接插入與階段2相同數據的空間佔用

相比階段二  節省了內存預分配的空間。

字符串之所以採用預分配的方式是防止修改操作需要不斷重分配內存和字節數據拷貝(頻繁的內存重分配是個耗時的操作,這裏算是redis在空間和時間上的一個權衡)。

SDS帶來的另一個好處就是降低strlen複雜度(O(n) -> 0(1)),直接獲取len的值。

空間預分配規則

1、第一次創建len屬性等於數據實際大小,free等於0,不做預分配

2、修改後如果已有free空間不夠且數據小於1M,每次預分配一倍容量。如原有len=60byte,free=0,在追擊60byte,預分配120byte,總佔用空間:60byte+60byte+120byte+1byte

3、修改後如果已有free空間不夠且數據大於1M,每次預分配1M數據。如原有len=30M,free=0,當在追擊100byte,預分配1M,總佔用空間:1M+100byte+1M+1byte

注1

整數對象共享池

Redis爲了節省內存開銷,內部維護[0-9999]的整數對象池。創建大量的整數類型redisObject存在內存開銷,每個redisObject內部結構至少佔16字節,甚至超過了整數自身空間消耗。所以Redis內存維護一個[0-9999]的整數對象池,用於節約內存。除了整數值對象,其他類型如list、hash、set、zset內部元素也可以使用整數對象池。因此開發中在滿足需求的前提下,儘量使用整數對象以節省內存。

可以通過object refcount命令查看對象引用數驗證是否啓用整數對象池技術(上文中介紹的redisObject refcount 字段),如下:

redis> set foo 100
OK
redis> object refcount foo(integer) 
2
redis> set bar 100
OK
redis> object refcount bar(integer) 
3

設置鍵foo等於100時,直接使用共享池內整數對象,因此引用數是2,再設置鍵bar等於100時,引用數又變爲3。

值得注意的是:使用整數對象共享池會節約大量內存,但是對象池並不是只要存儲[0-9999]的整數就可以工作。當設置maxmemory並啓用LRU相關淘汰策略(redis淘汰策略另一篇寫~)如:volatile-lru,allkeys-lru時,Redis禁止使用共享對象池,測試命令如下:

redis> set key:1 99
OK // 設置key:1=99

redis> object refcount key:1(integer) 
2 // 使用了對象共享,引用數爲2

redis> config set maxmemory-policy volatile-lru
OK // 開啓LRU淘汰策略

redis> set key:2 99
OK // 設置key:2=99

redis> object refcount key:2(integer) 
3 // 使用了對象共享,引用數變爲3

redis> config set maxmemory 1GB
OK // 設置最大可用內存

redis> set key:3 99
OK // 設置key:3=99

redis> object refcount key:3(integer) 
1 // 未使用對象共享,引用數爲1

redis> config set maxmemory-policy volatile-tt
OK // 設置非LRU淘汰策略

redis> set key:4 99
OK // 設置key:4=99

redis> object refcount key:4(integer) 
4 // 又可以使用對象共享引用數變爲4

 

爲什麼開啓maxmemoryLRU淘汰策略後對象池無效

LRU算法需要獲取對象最後被訪問時間,以便淘汰最長未訪問數據,每個對象最後訪問時間存儲在redisObject對象的lru字段。對象共享意味着多個引用共享同一個redisObject,這時lru字段也會被共享,導致無法獲取每個對象的最後訪問時間。如果沒有設置maxmemory,直到內存被用盡Redis也不會觸發內存回收,所以共享對象池可以正常工作。

對於ziplist編碼的值對象,即使內部數據爲整數也無法使用共享對象池,因爲ziplist使用壓縮且內存連續的結構,對象共享判斷成本過高。

爲什麼redis不共享包含字符串的對象?

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

  • 如果共享對象時保存整數值的字符串對象,那麼驗證操作的複雜度爲o(1)
  • 如果共享對象時保存字符串值的字符串對象,那麼驗證操作的複雜度爲o(n)
  • 如果共享對象時包含了多個值(或者對象的)對象,比如列表對象或者哈希對像,那麼那麼驗證操作的複雜度爲o(n^{2})

因此,儘管共享對象更復雜的對象可以節約更多的內存,但受到CPU時間的限制,redis只建立了一個小整數共享池

文章部分內容來自@張鐵磊http://zhangtielei.com/posts/blog-redis-dict.html,《redis設計與實現》,《redis開發與運維》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章