Redis數據結構與對象(下)

  1. 對象的類型與編碼
    1. Redis使用對象來表示數據庫中的鍵和值,新創建一個鍵值對時,至少會創建兩個對象(鍵對象,值對象)
    2. Redis中每個對象都由一個RedisObject結構表示,五個屬性(type(類型)、encoding(編碼)、ptr(指向底層實現數據結構的指針)、refcount(引用計數值)、lru(記錄對象最後一次被訪問時間))
    3. 類型(type屬性):記錄了對象的類型

對象的名稱

對象type屬性的值(類型常量)

TYPE命令的輸出

字符串對象

REDIS_STRING

“string”

列表對象

REDIS_LIST

“list”

哈希對象

REDIS_HASH

“hash”

集合對象

REDIS_SET

“set”

有序集合對象

REDIS_ZSET

“zset”

 

      • 當稱呼一個數據庫鍵爲“字符串鍵”/“列表鍵”時,指的是“這個數據庫鍵所對應的值爲字符串對象/列表對象”
    • 編碼和底層實現
      • 對象的ptr指針指向對象的底層實現數據結構,而這些數據結構由對象的encoding屬性決定
      • encoding屬性記錄了對象所使用的編碼,也就是說這個對象底層實現使用了什麼數據結構;每種對象至少使用了兩種不同的編碼
      • 通過encoding屬性來設定編碼,而不是關聯一種固定的編碼,極大地提升了Redis的靈活性和效率,因爲Redis可以根據不同的使用場景來爲一個對象設置不同的編碼,從而優化對象在這一場景下的效率

 

 

  1. 字符串對象
    1. 字符串對象的編碼可以是int、raw或者embstr
    2. 字符串對象保存各類型值的編碼方式:

編碼

long類型保存的整數

Int

long類型保存的整數

embstr或者raw

字符串值,或者長度太大無法用(long/double)表示的(整數/浮點數)

embstr或者raw

 

    1. 如果字符串對象保存的是字符串值,由SDS保存字符串值
      • 長度大於32字節,字符串對象編碼設爲raw
      • 長度小於等於32字節,字符串編碼設爲embstr
    2. embstr編碼是用來保存短字符串的一種優化編碼方式
      • 與raw編碼一樣,使用redisObject結構和sdshdr結構來表示字符串對象
      • embstr編碼將創建字符串對象所需的內存分配次數從raw編碼的兩次降低爲一次
      • 釋放embstr編碼的字符串對象只需要調用一次內存釋放函數,而raw編碼需要兩次
      • 因爲embstr編碼的字符串對象的所有數據都保存在一塊連續的內存中,能夠更好的利用緩存帶來的優勢
    3. 浮點數在Redis中也是作爲字符串保存的,在有需要的時候,會轉換回浮點數,執行完成後在轉換爲字符串值
    4. int編碼和embstr編碼的字符串對象在一些情況下,會被轉換爲raw編碼的字符串對象
      • int:因某些命令,使得對象保存的不再是整數值,而是個字符串值,那麼字符串對象的編碼將從int變爲raw
      • embstr:因爲Redis沒有爲embstr編碼的字符串對象編寫相應的修改程序,所以會將embstr轉換爲raw,然後再執行修改

 

 

    1. 字符串命令:

命令

int編碼的實現方法

embstr編碼的實現方法

raw編碼的實現方法

SET

使用int編碼保存值

使用embstr編碼保存值

使用raw編碼保存值

GET

複製對象所保存的整數值,將這個複製值轉換成字符串值,然後向客戶端返回這個字符串值

直接向客戶端返回字符串值

直接向客戶端返回字符串值

APPEND

轉換成raw編碼,按raw編碼的方式執行此操作

轉換成raw編碼,按raw編碼的方式執行此操作

調用sdscatlen函數,將給定字符串追加到現有字符串的末尾

 

  1. 列表對象
    1. 列表對象的編碼可以是ziplist或者linkedlist
    2. 兩種底層實現:
      • ziplist編碼的列表對象使用壓縮列表作爲底層實現,每個壓縮列表節點(entry)都存了一個列表元素
      • linkedlist編碼的列表對象使用雙向鏈表作爲底層實現,每個鏈表節點(node)都保存了一個字符串對象,而每個字符串對象都存了一個列表元素
    3. 編碼轉換:當列表同時滿足以下兩個條件時,使用ziplist
      • 列表對象中的所有字符串長度都小於64字節
      • 列表對象中的元素數量小於512個

不能同時滿足時使用linkedlist編碼;即使已經使用ziplist編碼後,任意一點不被滿足仍然會轉換爲linkedlist編碼

以上兩個條件的上限值可以修改(配置文件中的list-max-ziplist-value/list-max-ziplist-entries選項說明)

 

    1. 列表命令的實現

命令

ziplist編碼的實現方法

linkedlist編碼的實現方法

LPUSH

調用ziplistpush函數,將新元素推入到壓縮列表的表頭

調用listAddNodeHead函數,將新元素推入到雙向鏈表的表頭

RPUSH

調用ziplistpush函數,將新元素推入到壓縮列表的表尾

調用listAddNodeTail函數,將新元素推入到雙向鏈表的表尾

LLEN

調用ziplistLen函數返回壓縮列表的長度

調用listLength函數返回雙向鏈表的長度

 

  1. 哈希對象
    1. 哈希對象的編碼可以是ziplist或者hashtable
    2. 兩種底層實現:
      • ziplist編碼的哈希對象由壓縮列表作爲底層實現,每當新加入鍵值對時,程序會依次將保存了鍵的壓縮列表節點和保存了值的節點先後推入到壓縮列表結尾。
        1. 保存了同一鍵值對的兩個節點總是緊挨在一起,鍵在前,值在後
        2. 先進來的鍵值對離表頭近,後進來的離表尾近
      • hashtable編碼的哈希對象使用字典作爲底層實現,哈希對象中的每個鍵值對都使用一個字典鍵值對來保存
        1. 字典的每個鍵/值都是一個字符串對象,對象中保存了鍵值對的鍵/值
    3. 編碼轉換:同時滿足以下條件時,哈希對象使用ziplist編碼:
      • 哈希對象保存的所有鍵值對的鍵和值的字符串長度都小於64字節
      • 哈希對象保存的鍵值對數量小於512個

不能滿足這兩個條件的哈希對象使用hashtable編碼;即使已經使用ziplist之後,不滿足條件也會被轉換成hashtable編碼

以上兩個條件的上限值可以修改(配置文件中的hash-max-ziplist-value/hash-max-ziplist-entries選項說明)

    1. 哈希命令的實現

命令

ziplist編碼實現方法

hashtable編碼的實現方法

HSET

先後調用ziplistPush函數,依次將鍵/值推入到壓縮列表表尾

調用dictAdd函數,將新節點添加到字典

HGET

首先調用ziplistFind函數,在壓縮列表中查找鍵所對應的節點,然後調用ziplistNext函數,將指針移動到鍵節點旁的值節點,最後返回值節點

調用dictFind函數,在字典中查找給定鍵,然後調用dictGetVal函數,返回該鍵所對應的值

 

 

 

  1. 集合對象
    1. 集合對象的編碼可以是intset/hashtable
    2. 底層實現:
      • intset編碼的集合對象使用整數集合作爲底層實現,集合對象包含的所有元素都被保存在整數集合中
      • hashtable編碼的集合對象使用字典作爲底層實現,字典的每個鍵都是一個字符串對象,每個字符串對象包含了一個集合元素,字典的值全部設置爲null
    3. 編碼的轉換:當集合對象同時滿足以下兩個條件時,使用intset編碼:
      • 集合對象保存的所有元素都是整數值;
      • 集合對象保存的元素數量不超過512個;
      • 不滿足時 使用hashtable編碼;即使集合對象使用intset編碼,當不滿足任一條件時仍然會轉換成hashtable
    4. 集合命令的實現

命令

intset編碼的實現方式

hashtable編碼的實現方式

SADD

調用intsetAdd函數,將所有新元素添加到整數集合

調用dictAdd,以新元素爲鍵,null爲值,將鍵值對添加到字典

SCARD

調用intsetLen函數,返回整數集合所包含的元素數量(集合對象的元素數量)

調用dictSize函數,返回字典的鍵值對個數(集合對象的元素數量)

 

 

  1. 有序集合對象
    1. 有序集合的編碼可以是ziplist/skiplist
    2. 底層實現:
      • ziplist編碼的有序集合對象使用壓縮列表作爲底層實現,每個集合元素使用兩個緊挨在一起的壓縮列表節點來保存,第一個節點保存元素的成員,而第二個節點則保存元素的分值;按分值從小到大排序,小的在前(靠近表頭),大的在後(靠近表尾)
      • skiplist編碼的有序集合對象使用zset結構作爲底層實現,一個zset結構同時包含一個字典和一個跳錶:
        1. zset結構中的zsl跳錶按分值從小到大保存了所有集合元素,每個跳錶節點都存了一個集合元素:跳錶節點的object屬性保存了元素的成員,score屬性保存了元素的分值。通過當前跳錶,程序可以對有序集合進行範圍型操作,比如ZRANK、ZRANGE命令就是基於跳錶API實現的
        2. zset結構中的dict字典爲有序集合創建了一個從成員到分值的映射,字典中的每個鍵值對都保存了一個集合元素:鍵存了元素的成員,值存了元素的分值。通過當前字典,程序可以用O(1)複雜度查找給定成員的分值
      • 有序集合每個元素的成員都是一個字符串對象,分值都是一個double浮點數。雖然zset同時使用字典和跳錶來保存元素,但是他們之間保存元素不會產生重複成員或者分值,他們會通過指針來共享相同元素的成員和分值,不會因此浪費額外內存
    3. 重點:爲什麼有序集合需要同時使用跳錶和字典來實現?
      • 理論上,可以單獨使用其中一種數據結構來實現,但性能上比同時使用會有所降低
      • 比如只用字典,但字典以無序方式保存元素。每次執行範圍性操作(ZRANK、ZRANGE),都需要先排序(需要O(nlogn)時間複雜度以及O(n)的空間複雜度(因爲要創建一個數組來保存排序後的元素))
      • 再比如只用跳錶,沒了字典,根據成員查找分值的複雜度會由O(1)上升爲O(logn)
    4. 個人理解重點:使用字典是爲了彌補跳錶查找分值的O(logN)的效率;跳錶是爲了彌補字典的無序、創建數組排序的額外O(N)內存空間;同時使用是爲了讓他們之間取長補短,形成更健壯的結構
    5. 編碼的轉換:同時滿足兩種條件時,使用ziplist編碼
      • 有序集合的所有元素成員的長度都小於64字節
      • 有序集合的元素數量小於128個

不能同時滿足時使用skiplist;即使已經使用ziplist,只要任一不滿足都會轉換爲skiplist

    1. 有序集合命令的實現:因爲有序集合對象的值爲哈希對象,所以命令都是由哈希對象來構建的

命令

ziplist編碼的實現方式

zset編碼的實現方式

ZADD

調用ziplistInsert函數,將成員和分值作爲兩個節點分別插入到壓縮列表

先調用zslInsert函數,將新元素添加到跳錶,然後調用dictAdd函數,將新元素關聯到字典

ZCARD

調用ziplistLen函數,獲得壓縮列表包含的節點的數量,將這個數量除以2得出集合元素的數量

訪問跳錶的length屬性,直接返回集合元素的數量

ZCOUNT

遍歷壓縮列表,統計分值在給定範圍內的節點的數量

遍歷跳錶,統計分值在給定範圍內的節點數量

ZRANGE

從表頭向表尾遍歷壓縮列表,返回給定索引範圍內的所有元素

從表頭向表尾遍歷跳錶,返回給定索引範圍內的所有元素

ZREVRANGE

從表尾向表頭遍歷壓縮列表,返回給定索引範圍內的所有元素

從表尾向表頭遍歷跳錶,返回給定索引範圍內的所有元素

 

  1. 類型檢查與命令多態
    1. 類型檢查的實現:在執行一個類型特定的命令前,Redis會先檢查輸入鍵的類型是否正確,然後再決定是否執行給定的命令(SET、GET只能給字符串對象使用)
    2. 多態命令的實現:
      • 比如DEL、RENAME等命令是基於類型的多態:一個命令可以同時處理多種不同類型的鍵
      • 比如LLEN等命令是基於編碼的多態:一個命令可以同時用於處理多種不同編碼

 

  1. 內存回收
    1. 概述:採用引用計數(reference counting)法實現內存回收機制,通過這一機制,程序可以通過跟蹤對象的引用計數信息,在適當的時候自動釋放對象並內存回收
    2. 對象的引用計數會隨着使用狀態而不斷變化:
      • 對象被創建時,引用計數值初始化爲1
      • 當對象被一個新程序使用時,引用計數值+1
      • 當對象不在被一個程序使用時,引用計數值-1
      • 引用計數爲0時,所佔用的內存會被釋放
    3. 對象的整個生命週期:創建對象、操作對象、釋放對象三階段

函數

作用

incrRefCount

將對象的引用計數值+1

decrRefCount

將對象的引用計數值-1,當引用計數值爲0時,釋放對象

resetRefCount

將對象的引用計數值設爲0,但不釋放對象,用於重置引用計數值

 

  1. 對象共享
    1. 對象的引用計數屬性還帶有對象共享的作用
    2. Redis中,讓多個對象共享同一個值對象需要執行兩步驟:
      • 將數據庫對象的值指針指向一個現有的值對象
      • 將被共享的值對象的引用計數+1
    3. 優勢:數據庫中保存的相同值對象越多,對象共享機制就能節約越多的內存
    4. Redis會在初始化服務器時,創建一萬個(0-9999)整數值作爲共享對象使用
    5. 爲什麼Redis不共享包含字符串的對象:
      • 因爲一個共享對象保存的值越複雜,驗證共享對象和目標對象是否相同所需的複雜度就會更高,消耗的CPU時間也會越多
      • 共享對象爲整數值/字符串時,複雜度爲O(1)/O(n)
      • 共享對象爲複雜對象(哈希/列表等對象),複雜度爲O(n2)

 

  1. 對象的空轉時長
    1. 用途:
      • RedisObject結構包含的最後一個屬性lru,該屬性記錄了對象最後一次被訪問的時間
        1. OBJECT IDLETIME命令可以打印出給定對象的空轉時長,這一空轉時長就是通過將當前時間減去鍵的值對象的lru時間得出
      • 如果服務器打開了maxmemory選項,並且服務器用於回收內存的算法爲volatile-lru/allkeys-lru,那麼當服務器佔用的內存數超過了maxmemory選項所設置的上限值時,空轉時長較高的那部分對象會優先被服務器釋放,從而回收內存
  2. Maxmemory***
    1. volatile-lru(least recently used):最近最少使用算法,從設置了過期時間的鍵中選擇空轉時間最長的鍵值對清除掉;
    2. volatile-lfu(least frequently used):最近最不經常使用算法,從設置了過期時間的鍵中選擇某段時間之內使用頻次最小的鍵值對清除掉;
    3. volatile-ttl:從設置了過期時間的鍵中選擇過期時間最早的鍵值對清除;
    4. volatile-random:從設置了過期時間的鍵中,隨機選擇鍵進行清除;
    5. allkeys-lru:最近最少使用算法,從所有的鍵中選擇空轉時間最長的鍵值對清除;
    6. allkeys-lfu:最近最不經常使用算法,從所有的鍵中選擇某段時間之內使用頻次最少的鍵值對清除;
    7. allkeys-random:所有的鍵中,隨機選擇鍵進行刪除;
    8. noeviction:不做任何的清理工作,在redis的內存超過限制之後,所有的寫入操作都會返回錯誤;但是讀操作都能正常的進行;

前綴爲volatile-和allkeys-的區別在於二者選擇要清除的鍵時的字典不同,volatile-前綴的策略代表從redisDb中的expire字典中選擇鍵進行清除;allkeys-開頭的策略代表從dict字典中選擇鍵進行清除。

 

 

Redis在你們工作中設置的maxmemorey是多少

前提:redis作爲緩存使用。
如果機器不是主要作爲緩存用,只是想內存的部分作爲緩存(例如服務器託管的網站也在這臺機器上),那麼需要設置maxmemory,不過設置maxmemory後可能會發生寫入失敗的情況,這就要選擇一個好的淘汰策略,例如LRU。下面是一個例子。
`maxmemory 2mb
 maxmemory-policy allkeys-lru `
設置一方面多大內存看需求了。也要考慮是否達到了緩存的效果,例如設置後只能放入兩條記錄,還不如不做緩存。

 

往 redis 寫入的數據怎麼沒了

依據Redis的內存淘汰機制策略,可能內存已達到峯值,被Redis給清除了。也許是maxmemory選項設置爲allkeys-random/volatile-random算法,隨機刪除了這個數據

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