1. 程式人生 > 實用技巧 >Redis記憶體模型原理

Redis記憶體模型原理

字串

Redis 沒有直接使用 C 字串(即以空字元’\0’結尾的字元陣列)作為預設的字串表示,而是使用了SDS。SDS 是簡單動態字串(Simple Dynamic String)的縮寫。

它是自己構建了一種名為 簡單動態字串(simple dynamic string,SDS)的抽象型別,並將 SDS 作為
Redis的預設字串表示。

SDS 定義:

struct sdshdr{
   
   
	//記錄buf陣列中已使用位元組的數量
	//等於 SDS 儲存字串的長度
	int len;
	//記錄 buf 陣列中未使用位元組的數量
	int free;
	//位元組陣列,用於儲存字串
	char buf[];
}
  • len 儲存了SDS儲存字串的長度
  • buf[] 陣列用來儲存字串的每個元素
  • free j記錄了 buf 陣列中未使用的位元組數量


SDS 在 C 字串的基礎上加入了 free 和 len 欄位,帶來了很多好處:

  1. 獲取字串長度:SDS 是 O(1),C 字串是 O(n)。
    緩衝區溢位:使用 C 字串的 API 時,如果字串長度增加(如 strcat 操作)而忘記重新分配記憶體,很容易造成緩衝區的溢位。
  2. 而 SDS 由於記錄了長度,相應的 API 在可能造成緩衝區溢位時會自動重新分配記憶體,杜絕了緩衝區溢位。
  3. 修改字串時記憶體的重分配:對於 C 字串,如果要修改字串,必須要重新分配記憶體(先釋放再申請),因為如果沒有重新分配,字串長度增大時會造成記憶體緩衝區溢位,字串長度減小時會造成記憶體洩露。
    而對於 SDS,由於可以記錄 len 和 free,因此解除了字串長度和空間陣列長度之間的關聯,可以在此基礎上進行優化。
  4. 空間預分配策略(即分配記憶體時比實際需要的多)使得字串長度增大時重新分配記憶體的概率大大減小;惰性空間釋放策略使得字串長度減小時重新分配記憶體的概率大大減小。
  5. 存取二進位制資料:SDS 可以,C 字串不可以。因為 C 字串以空字元作為字串結束的標識,而對於一些二進位制檔案(如圖片等)。
  6. 內容可能包括空字串,因此 C 字串無法正確存取;而 SDS 以字串長度 len 來作為字串結束標識,因此沒有這個問題。
  7. 此外,由於 SDS 中的 buf 仍然使用了 C 字串(即以’\0’結尾),因此 SDS 可以使用 C 字串庫中的部分函式。
  8. 但是需要注意的是,只有當 SDS 用來儲存文字資料時才可以這樣使用,在儲存二進位制資料時則不行(’\0’不一定是結尾)。

連結串列

連結串列在Redis中的應用非常廣泛,列表(List)的底層實現之一就是雙向連結串列。此外發布與訂閱、慢查詢、
監視器等功能也用到了連結串列。

typedef struct listNode {
   
   
	//前置節點
	struct listNode *prev;
	//後置節點
	struct listNode *next;
	//節點的值
	void *value;
} listNode

typedef struct list {
   
   
	//表頭節點
	listNode.head;
	//表尾節點
	listNode.tail;
	//連結串列所包含的節點數量
	unsigned long len;
	//節點值複製函式
	void *(*dup)(void *ptr);
	//節點值釋放函式
	void *(*free)(void *ptr);
	//節點值對比函式
	int (*match)(void *ptr,void *key);
} list;

Redis連結串列優勢:

  1. 雙向:連結串列具有前置節點和後置節點的引用,獲取這兩個節點時間複雜度都為O(1)。
    與傳統連結串列(單鏈表)相比,Redis連結串列結構的優勢有:
    普通連結串列(單鏈表):節點類保留下一節點的引用。連結串列類只保留頭節點的引用,只能從頭節點插入刪



  2. 無環:表頭節點的 prev 指標和表尾節點的 next 指標都指向 NULL,對連結串列的訪問都是以 NULL
    結束。
  3. 帶連結串列長度計數器:通過 len 屬性獲取連結串列長度的時間複雜度為 O(1)。
  4. 多型:連結串列節點使用 void* 指標來儲存節點值,可以儲存各種不同型別的值。

字典


字典又稱為符號表或者關聯陣列、或對映(map),是一種用於儲存鍵值對的抽象資料結構。
字典中的每一個鍵 key 都是唯一的,通過 key 可以對值來進行查詢或修改。
Redis 的字典使用雜湊表作為底層實現。
雜湊(作為一種資料結構),不僅是 Redis 對外提供的 5 種物件型別的一種(hash),也是 Redis 作
為 Key-Value 資料庫所使用的資料結構。




typedef struct dictht{
   
   
	//雜湊表陣列
	dictEntry **table;
	//雜湊表大小
	unsigned long size;
	//雜湊表大小掩碼,用於計算索引值
	//總是等於 size-1
	unsigned long sizemask;
	//該雜湊表已有節點的數量
	unsigned long used;
} dictht

/*雜湊表是由陣列 table 組成,table 中每個元素都是指向 dict.h/dictEntry 結構,
dictEntry 結構定義如下:
*/
typedef struct dictEntry {
   
   
	//鍵
	void *key;
	//值
	union{
   
   
	void *val;
	uint64_tu64;
	int64_ts64;
} v;

//指向下一個雜湊表節點,形成連結串列
struct dictEntry *next;
} dictEntry

跳錶(zset)

普通單鏈表查詢一個元素的時間複雜度為O(n),即使該單鏈表是有序的。


查詢46 : 55—21—55–37–55–46

typedef struct zskiplistNode {
   
   
	//層
	struct zskiplistLevel{
   
   
	//前進指標
	struct zskiplistNode *forward;
	//跨度
	unsigned int span;
	}level[];
	//後退指標
	struct zskiplistNode *backward;
	//分值
	double score;
	//成員物件
	robj *obj;
} zskiplistNode

//連結串列
typedef struct zskiplist{
   
   
	//表頭節點和表尾節點
	structz skiplistNode *header, *tail;
	//表中節點的數量
	unsigned long length;
	//表中層數最大的節點的層數
	int level;
} zskiplist;

  1. 搜尋:從最高層的連結串列節點開始,如果比當前節點要大和比當前層的下一個節點要小,那麼則往下
    找,也就是和當前層的下一層的節點的下一個節點進行比較,以此類推,一直找到最底層的最後一個節
    點,如果找到則返回,反之則返回空。

  2. 插入:首先確定插入的層數,有一種方法是假設拋一枚硬幣,如果是正面就累加,直到遇見反
    面為止,最後記錄正面的次數作為插入的層數。當確定插入的層數k後,則需要將新元素插入到從底層
    到k層。

  3. 刪除:在各個層中找到包含指定值的節點,然後將節點從連結串列中刪除即可,如果刪除以後只剩
    下頭尾兩個節點,則刪除這一層。

快取淘汰策略

最大快取

  • 在 redis 中,允許使用者設定最大使用記憶體大小maxmemory,預設為0,沒有指定最大快取,如果有新的資料新增,超過最大記憶體,則會使redis崩潰,所以一定要設定。
  • redis 記憶體資料集大小上升到一定大小的時候,就會實行資料淘汰策略。

淘汰策略

  • redis淘汰策略配置:maxmemory-policy voltile-lru,支援熱配置

redis 提供 6種資料淘汰策略:

  1. volatile-lru:從已設定過期時間的資料集(server.db[i].expires)中挑選最近最少使用的資料淘汰
  2. volatile-ttl:從已設定過期時間的資料集(server.db[i].expires)中挑選將要過期的資料淘汰
  3. volatile-random:從已設定過期時間的資料集(server.db[i].expires)中任意選擇資料淘汰
  4. allkeys-lru:從資料集(server.db[i].dict)中挑選最近最少使用的資料淘汰
  5. allkeys-random:從資料集(server.db[i].dict)中任意選擇資料淘汰
  6. no-enviction(驅逐):禁止驅逐資料

LRU原理

LRU(Least recently used,最近最少使用)演算法根據資料的歷史訪問記錄來進行淘汰資料,其核心
思想是“如果資料最近被訪問過,那麼將來被訪問的機率也更高”。


  1. 新資料插入到連結串列頭部;
  2. 每當快取命中(即快取資料被訪問),則將資料移到連結串列頭部;
  3. 當連結串列滿的時候,將連結串列尾部的資料丟棄。
    在Java中可以使用LinkHashMap去實現LRU

事務

事務中命令按順序執行,中途有命令出錯後續命令仍執行,如果是語法錯誤則事務無法提交。

  • Redis事務沒有隔離級別:
    Redis事務執行命令會放入佇列中,事務未提交時不會被執行,也就不存在事務內的查詢要看到事務裡的更新,事務外查詢不能看到。

  • Redis不保證原子性:
    Redis中單條命令是原子性執行的,但事務不保證原子性,且沒有回滾。事務中任意命令執行失敗,其餘的命令仍會被執行。

redis 127.0.0.1:6379> MULTI
OK

redis 127.0.0.1:6379> SET book-name "Hello World"
QUEUED

redis 127.0.0.1:6379> GET book-name
QUEUED

redis 127.0.0.1:6379> SADD tag "java" "go" "c"
QUEUED

redis 127.0.0.1:6379> SMEMBERS tag
QUEUED

redis 127.0.0.1:6379> EXEC
1) OK
2) "Hello World"
3) (integer) 3
4) 1) "c"
   2) "go"
   3) "java"

WATCH機制(樂觀鎖)

watch變數,並開啟事務,如果該變數被修改那麼事務無法執行,否則成功執行。

    • 初始化信用卡可用餘額和欠額

    • 用watch監控,進行資料監控,事務成功執行

    • 監控過程中,他人纂改,事務無法執行