1. 程式人生 > >Redis筆記(1)數據結構與對象

Redis筆記(1)數據結構與對象

raw 引用 但是 種類 不同類 方式 系列 void 結構體

1.前言

  此系列博客記錄redis設計與實現一書的筆記,提取書本中的知識點,省略相關說明,方便查閱。

2.基本數據結構

2.1 簡單動態字符串SDS(simple dynamic string)

  結構體定義

    len: buf數組中已使用字節的數量,使用len判斷實際內容長度,而不是‘\0‘字符

    free: 未使用字節的數量,查詢該值,杜絕內存溢出

    buf[]: 實際分配空間及存儲內容(字節數組,保證二進制安全,怎麽存怎麽取)

  保留C語言的習慣,字符串以‘\0‘結束,好處在於可以兼容使用C的API。

  分配策略

    1.修改後小於1MB,需要擴展,分配與修改後len相同長度的額外空間,即buf總大小變成len+len+1(結尾字符)

    2.修改後大於等於1MB,需要擴展,分配1MB,即buf大小變成len+1MB

  釋放策略:惰性釋放,長度變短時不進釋放空間,有需要時釋放。

2.2 鏈表

  在redis中使用廣泛,列表鍵底層實現之一就是鏈表,當一個列表鍵包含了數量較多的元素,或者元素都是比較長的字符串,會使用鏈表作為列表鍵的底層實現。

  LLEN,LRANGE等命令:即list

  鏈表節點結構體定義listNode:

    listNode *prev: 前置節點

    listNode *next: 後置節點

    void *valud: 節點的值

  鏈表節點有前後節點,可以構成雙向鏈表

  鏈表的結構體list定義

如下:

    listNode *head: 表頭節點

    listNode *tail: 表尾節點

    unsigned long len: 節點數量

    void *(*dup)(void *ptr): 節點值復制函數

    void (*free) (void *ptr): 節點值釋放函數

    int (*match)(void *ptr, void *key): 節點值對比函數

  特性

    雙端:前後查詢O(1)

    無環:以NULL節點終結

    頭尾指針:獲取頭尾迅速O(1)

    長度計數:O(1)

    多態: 三個函數可以支持保存各種不通類型的值

2.3 字典map

  使用廣泛,比如數據庫就是使用字典作為底層實現的。SET msg "hello world" 或者 HLEN HGETALL等命令。就是map和普通鍵值對。

  哈希表dictht定義

    dictEntry **table: 哈希表數組

    unsigned long size: 哈希表大小

    unsigned long sizemask: 哈希表大小掩碼,用於計算索引值,等於size-1

    unsigned long used: 哈希表有節點的數量

  哈希節點dictEntry結構定義

    void *key: 鍵

    union {

      void *val;

      unit64_t u64;

      int64_t s64;

    }v:值

    dictEntry *next:下一個哈希表節點,形成鏈表,解決hash沖突

  字典dict結構定義

    dictType *type: 類型特定函數,多態,針對不同類型的鍵值對

    void *privdata: 私有數據

    dictht ht[2]: 哈希表 2個空間用於rehash操作,一般使用0,下標1的在rehash時使用

    int trehashidx: rehash索引,不進行時,為-1

  表位置計算

    1.hash值 MurmurHash2算法

    2.hash & sizemask

  rehash步驟

    0.rehash條件:

      負載因子: used / 表大小

      滿足一個即可:

          1)沒有執行BGSAVE或者BGREWRITEAOF命令時,負載因子大於等於1

          2)執行了上訴兩個命令,且負載因子大於等於5

          3)負載因子小於0.1時,自動收縮

    1.為ht[1]哈希表分配空間:

      擴展操作或收縮操作,ht[1]的大小>=ht[0].used*2^n(n取值使得右邊最小)

      比如used為7 那麽新表大小為8=2^3>7

    2.設置rehashidx,將其設置為0,表示開始rehash

    3.從hash表的rehashidx下標的鏈表開始,重新計算hash值,將其完全移動至另一個hash表,之後rehashidx增加1

    4.rehash期間,所有的增刪改查操作會在兩個hash表上進行:

      新增的只會添加在ht[1]表上,查找先在ht[0]上進行,沒找到再去ht[1]查找。修改刪除一樣。

      單線程執行保證沒有並發問題,漸進式rehash也是為了避免數據太大,造成一段時間內停止服務,所以一個下標一個下標移動。擴容機制以及hash算法保證hash碰撞不會過於集中,決定了單個下標數據不會很多。

    5.ht[0]上的所有鍵值對都放入到ht[1]後,rehash完成,rehashidx置為-1。

    6.轉移完畢後釋放ht[0]空間,將ht[1]設置成ht[0],在ht[1]新創建一個空白的hash表,為下一次rehash準備。

2.4 跳躍表

  跳躍表只有在有序集合鍵中和集群節點中使用到了,其余時候沒有作用。有序集合鍵如ZRANGE ZCARD命令相關。

  跳躍表節點zskiplistNode結構

    zskiplistNode *backward: 後退指針

    double socre: 分值

    robj *obj: 成員對象

    zskiplistLevel {

      zskiplistNode *forward: 前進指針

      unsigned int span: 跨度

    } level[]; // 層

  層level數組可以包含多個元素,每個元素包含一個指向其他節點的指針,可以通過層加快訪問速度。層數越多,速度越快。創建節點時會隨機生成一個1~32的數值作為該節點的level數組大小。

  跨度指的是兩個節點之間的距離,NULL的前指針跨度為0。跨度可用來計算目標節點在跳躍表中的位置。

  後退指針只有一個,只能退到上一個節點。

  跳躍表中的成員通過分值從小到大排列,成員對象指向一個字符串對象,即SDS值。成員對象唯一,多個成員對象可以有相同的分值

  跳躍表zskiplist結構

    zskiplistNode *header, *tail:頭尾節點

    unsigned long length: 表中節點數量

    int level: 表中層數最大的節點的層數

2.5 整數集合

  整數集合用於數量不多,且都是整型的情況,比如 SADD numbers 1 3 5 7 9, OBJECT ENCODING numbers可以顯示 "intset".

  可以保存int16_t、int32_t、int64_t類型的值。

  整數集合intset結構

    uint32_t encoding: 編碼方式

    unit32_t length: 包含的元素數量

    int8_t contents[]: 保存元素的數組

  集合中每一個元素都是contents中的一個項item,從小到大排列,不能重復。contents聲明為int8_t,但是實際上不保存這個類型的值,真正類型取決於encoding,INTSET_ENC_INT(16,32,64)。contents的數組大小等於encoding*length。比如是16位的整型,5個元素,contents大小就是80。

  升級過程:比如原本encoding是16的整型,現在新增一個32的,就需要升級。

    1.根據新類型和元素數量,擴展contents的大小,分配空間。

    2.將原元素轉換成新元素類型,放入正確的位置,保證順序不變。

    3.將新元素添加到底層數組裏面。

    4.修改encoding的值,length+1

  每次添加元素都可能造成升級,每次升級要處理所有的元素,時間復雜度為o(N)。

  升級提升靈活度,隨意將16,32,64位的整型添加到集合中。節約內存,要存放16,32,64位的整型最好的方法是直接使用64位的,升級可以減少內存消耗。

  inset集合不支持降級操作

2.6 壓縮列表

  壓縮列表是列表鍵和哈希鍵的底層實現之一。列表鍵中包含少量列表項,並且列表項是小整數值或長度較短的字符串,就會使用壓縮列表。

  比如RPUSH lst 1 3 5 10086 "hello" "world"     OBJECT ENCODING lst 輸出”ziplist"

    HMSET profile “name" "jack" ...

  壓縮列表用於節約內存,特殊編碼的連續內存塊組成的順序型數據結構。一個壓縮列表可以保存一個字節數組或者整數值。

  具體結構按照下列順序排列

    zlbytes unit32_t 4字節 記錄整個壓縮列表占用內存字節數,在進行內存重分配或計算zlend位置時使用。

    zltail unit32_t 4字節 記錄壓縮列表尾節點距起始地址有多少字節,通過這個程序無需遍歷整個壓縮列表可以確定尾節點位置。

    zllen  unit16_t 2字節 記錄壓縮列表包含的節點數量,小於65535時為真,等於需要遍歷才能計算出來。

    entryX 列表節點 不定 各個節點,長度由節點保存內容決定

    zlend unit8_t 1字節 特殊值0xFF 用於標記壓縮列表的末端

  壓縮節點的構成entryX:

    1.字節數組長度為下面3種之一:

      長度小於等於63 2^6-1字節的字節數組

      長度小於等於16383 2^14-1字節的字節數組

      長度小於等於2^32-1字節的字節數組

    2.整型可以是下面6種之一:

      4位長度,0~12之間的無符號整型

      1字節長的有符合整數

      3字節長的有符號整數

      int16_t類型整數

      int32_t類型整數

      int64_t類型整數

    3.由下面三個內容構成一個節點:

      previous_entry_length: 記錄了壓縮列表中前一個節點的長度,該字段長度可以是1字節(前節點長度小於254)或5字節(大於等於254,第一個節點會設置成254後面4個節點保存前一個節點長度)。通過這個屬性可以遍歷到頭節點。

      encoding:保存數據的類型以及長度。由開頭前2位判斷類型及該字段的長度,後面的判斷長度

        00:該字段占用一個字節,後面6個比特位是數組長度,即長度小於等於63的字節數組

        01:該字段占用2個字節,後面6+8個比特位是數組長度,即長度小於等於2^14-1個字節數組

        10:該字段占用5個字節,後面4個字節記錄長度,即長度小於等於2^32-1個字節數組

        11000000: int16_t類型整數

        11010000: int32_t類型整數

        11100000: int64_t類型整數

        11110000: 24位有符號整數

        11111110: 8位有符號整數

        1111xxxx: 該值的時候就沒有content屬性了,因為本身xxxx就足夠保存0~12之間的值了。

      content:具體的內容。由encoding決定

  連鎖更新:

    由於previous_entry_length記錄了之前的節點長度,但是其有兩種形態1字節和5字節。這裏就會產生一個麻煩,原本前一個節點是小於254個字節的,本節點使用的1字節形態的previous_entry_length記錄了這個情況,現在在該節點前插入了一個大於254個字節長度的節點,要將其改成5字節形態。但是這又產生了一個麻煩,比如當前節點是253個字節,由於previous_entry_length由1變成了5,增加了4個字節長度,導致該節點超過了254個字節,進而後置節點的previous_entry_length也要改變形態,這樣可能會發生連鎖反應。

    刪除節點一樣會導致連鎖反應,都稱之為連鎖更新。最壞的情況下需要N次空間重新分配,每次最壞O(N),所以最壞為O(N^2)。但是由於可能性太小了,而且只要不是大面積的連鎖更新都是可以接受的。所以這個不需要過度擔心。

3.對象

  redis中沒有直接使用上述數據結構來實現鍵值對數據庫,而是基於此實現了一個對象系統,包含:字符串對象,列表對象,哈希對象,集合對象和有序集合對象。redis使用了引用計數技術來控制內存回收機制,不再使用的對象會被釋放。

  對象基本結構redisObject:

    unsigned type:4 類型

    unsigned encoding:4 編碼

    void *ptr 指向底層的數據結構的指針

    ...

  類型有5種:REDIS_STRING 字符串對象、REDIS_LIST 列表對象、REDIS_HASH 哈希對象、REDIS_SET 集合對象、REDIS_ZSET 有序集合對象。使用type命令可以查看對象類型。

  encoding決定ptr指向的數據結構,一共有以下幾種數據結構:

    REDIS_ENCODING_INT    long類型的整數

    REDIS_ENCODING_EMBSTR  embstr編碼的簡單動態字符串

    REDIS_ENCODING_RAW    簡單動態字符串

    REDIS_ENCODING_HT    字典

    REDIS_ENCODING_LINKEDLIST    雙端鏈表

    REDIS_ENCODING_ZIPLIST    壓縮列表

    REDIS_ENCODING_INSET    整數集合

    REDIS_ENCODING_SKIPLIST  跳躍表和字典

  不同的對象類型有相關的encoding,下面是一個對應關系:

    String: int、embstr、raw

    list: ziplist、linkedlist

    hash: ziplist、ht

    set: intset、ht

    zset: ziplist、 skiplist

  使用OBJECT ENCODING可以看見對象當前編碼。

3.1 字符串對象

  字符串對象的編碼可以是int、raw、embstr。

  如果一個字符串對象保存的是整型,並且可以用long類型表示,就會設置成int編碼。

  如果一個字符串對象保存的是字符串值,並且長度大於39字節,那麽使用SDS來保存這個字符串值,設置編碼為raw

  如果一個字符串對象保存的是字符串值,並且長度小於等於39字節,那麽使用embstr來保存這個字符串值。

  raw和embstr的區別在於,raw會開辟兩次空間,創建redisObject和sdshdr結構,但是embstr只分配一塊連續的區間,依次包含redisObject和sdshdr。對應的釋放空間也只需要一次。

  編碼的轉換:

    int和embstr在條件滿足的情況下會被轉換成raw編碼。

    假如一個字符串對象中保存的是整數值,但是使用了append命令追加了字符串,就會變成raw:set number 123, append number " xxx"

    embstr編碼的字符串沒有編寫任何的修改程序,所以該類型實際上是只讀的。修改的時候都會變成raw,再執行修改命令。

3.2 列表對象

  列表對象的編碼可以是ziplist或者是linkedlist。

  編碼的轉換:

  滿足以下2個條件的時候會使用ziplist:

    列表對象保存的所有字符串元素長度都小於64個字節。

    列表對象保存的元素數量小於512個。

  不滿足上述條件的會轉成linkedlist。

  這兩個條件可以進行修改,配置list-max-ziplist-value和list-max-ziplist-entries。

3.3 哈希對象

  哈希對象的編碼可以是ziplist或者是hashtable。

  ziplist的時候,鍵值是緊挨在一起的,先鍵放入尾端,再把值放入尾端。

  編碼的轉換:

  滿足以下2個條件的時候會使用ziplist:

    列表對象保存的所有字符串元素長度都小於64個字節。

    列表對象保存的元素數量小於512個。

  不滿足上述條件會轉成hashtable。

  這兩個條件可以進行修改,配置hash-max-ziplist-value和hash-max-ziplist-entries。

3.4 集合對象

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

  編碼的轉換:

  滿足以下兩個條件時,對象使用intset編碼:

    集合對象保存的所有元素都是整數值

    集合對象保存的元素數量不超過512個

  不滿足上述條件的會使用hashtable編碼

  第二個上限值是可以修改的,配置set-max-intset-entries選項。

3.5 有序集合對象

  有序集合的編碼可以是ziplist或者skiplist。

  ziplist作為實現的時候,第一個節點保存元素的成員,第二個元素則保存元素的分值。

  zset結構包含一個zsl跳躍表和一個dict字典表。跳躍表使得有序集合可以進行範圍型操作,如ZRANK和ZRANGE命令。字典表可以O(1)復雜度查找給定成員的分值,ZSCORE命令。

  編碼的轉化:

  滿足以下兩個條件時,對象使用ziplist編碼:

    有序集合保存的元素數量小於128個

    有序集合保存的所有元素成員的長度都小於64字節。

  不能滿足以上兩個條件的有序集合對象使用skiplist編碼。

  以上兩個條件的值可以修改,配置zset-max-ziplist-entries和zset-max-ziplist-value。

4.其它概念

4.1 類型檢查和命令多態

  redis的命令基本上分兩種類型:

    所有鍵都能執行,比如delete、expire、rename、type、object。

    特定類型鍵執行,比如:

      SET、GET、APPEND、STRLEN只能針對字符串鍵執行

      HDEL、HSET、HGET、HLEN只能針對哈希鍵執行

      RPUSH、LPOP、LINSERT、LLEN只能針對列表鍵執行

      SADD、SPOP、SINTER、SCARD只能針對集合鍵執行

      ZADD、ZCARD、ZRANK、ZSCORE只能針對有序集合鍵執行

  為了不執行錯誤的命令,都會先進行類型檢查,通過redisObject type來實現。

  相同的類型但是編碼是不同的,意味著命令要適應不同編碼結構,這就是命令的多態。

4.2 內存回收

  redis構建了一個引用計數技術來實現內存回收機制,在適當的時候自動釋放對象,進行內存回收。

  redisObject中有一個refcount字段用於引用計數。

    創建對象時,引用計數的值會被初始化為1.

    被使用時,+1

    不被使用時,-1

    為0時,釋放內存。

4.3 對象共享

  鍵A創建了一個整型100,鍵B也要創建一個整型100,做法有兩種:新建一個或者使用A的。當然後者更節省內存。

    1.B指向A的值

    2.值的引用計數+1

  redis在初始化服務器的時候,會創建一萬個字符串對象,包含從0~9999所有整數值。

  redis只共享整型值的字符串對象,因為字符串類型的驗證相同操作復雜,多個對象時更復雜。

4.4 對象的空閑時長

  redisObject還有一個屬性是lru屬性,記錄最後一次被程序訪問的時間。OBJECT IDLETIME 可以打印出對象從當前時間到最後一次訪問時間的空閑時長。這個命令不會修改lru的值。

  空閑時長的一個作用在於,如果服務器打開了maxmemory選項,並且服務器用於回收內存的算法是volatile-lru或者是allkeys-lru,當占用內存超過了maxmemory設置的上限,空轉時長較高的鍵會被優先釋放,從而回收內存。

  配置文件中maxmemory和maxmeory-policy選項介紹了相關信息。

Redis筆記(1)數據結構與對象