Redis複習(一):Redis數據類型、底層數據結構、過期鍵刪除策略、內存回收策略、RDB和AOF、Redis Pipeline、事務

一、Redis數據類型

類型 特性
string(字符串) 二進制安全的,可以包含任何數據,一個鍵最大能存儲512M
list(列表) 雙向鏈表,按照插入順序排序,可以從鏈表兩端進行push和pop操作
hash(散列表) 鍵值對集合,適合存儲對象
set(集合) 元素不重複的無序集合
zset(有序集合) 將set中的元素增加一個權重參數score,元素按score有序排列,數據插入集合時,已經進行天然排序

二、Redis底層數據結構

1、SDS

Redis構建了一種名爲簡單動態字符串(SDS)的抽象類型,並將SDS用作Redis的默認字符串表示

struct sdshdr {        
		//記錄buf數組中已使用字節的數量,等於SDS所保存字符串的長度
    int len;
  
    //記錄buf數組中未使用字節的數量
    int free;
  
    //字節數組,用於保存字符串
    char buf[];
}; 

在這裏插入圖片描述

特性

1)、獲取字符串長度的複雜度爲O(1)

2)、API是安全的,不會造成緩衝區溢出

當SDS API需要對SDS進行修改時,API會先檢查SDS的空間是否滿足修改所需的要求,如果不滿足的話,API會自動將SDS的空間擴展至修改所需的大小,然後才執行實際的修改操作

3)、減少修改字符串時帶來的內存重分配次數

SDS通過未使用空間解除了字符串長度和底層數組長度之間的關聯:在SDS中,buf數組裏面可以包含未使用的字節,而這些字節的數量就由SDS的free屬性記錄

1)空間預分配

空間預分配用於優化SDS的字符串增長操作:當SDS的API對一個SDS進行修改,並且需要對SDS進行空間擴展的時候,程序不僅會爲SDS分配修改所必須的空間,還會爲SDS分配額外的未使用空間

  • 如果對SDS進行修改之後,SDS的長度(也就是len屬性的值)將小於1MB,那麼程序分配和len屬性同樣大小的未使用空間,這時SDS len屬性的值將和free屬性的值相同
  • 如果對SDS進行修改之後,SDS的長度將大於等於1MB,那麼程序會分配1MB的未使用空間
    通過空間預分配策略,Redis可以減少連續執行字符串增長操作所需的內存重分配次數

在擴展SDS空間之前,SDS API會先檢查未使用空間是否足夠,如果足夠的話,API就會直接使用未使用空間,而無須執行內存重分配

2)惰性空間釋放

惰性空間釋放用於優化SDS的字符串縮短操作:當SDS的API需要縮短SDS保存的字符串時,程序並不立即使用內存重分配來回收縮短後多出來的字節,而是使用free屬性將這些字節的數量記錄起來,並等待將來使用

4)、二進制安全

通過使用二進制安全的SDS,使得Redis不僅可以保存文本數據,還可以保存任意格式的二進制數據

2、鏈表

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

每個鏈表節點使用一個adlist.h/listNode結構來表示:

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

在這裏插入圖片描述

多個listNode可以通過prev和next指針組成雙端鏈表

使用adlist.h/list來持有鏈表

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;

在這裏插入圖片描述

特性

1)、雙端:鏈表節點帶有prev和next指針,獲取某個節點的前置節點和後置節點的複雜度都是O(1)

2)、無環:表頭節點的prev指針和表尾節點的next指針都指向NULL,對鏈表的訪問以NULL爲終點

3)、帶表頭指針和表尾指針:通過list結構的head指針和tail指針,程序獲取鏈表的表頭節點和表尾節點的複雜度爲O(1)

4)、帶鏈表長度計數器:程序使用list結構的len屬性來對list持有的鏈表節點進行計數,程序獲取鏈表中節點數量的複雜度爲O(1)

3、字典

Redis的數據庫就是使用字典來作爲底層實現的,對數據庫的CRUD操作也是構建在對字典的操作之上的

一個沒有進行rehash的字典如下:

在這裏插入圖片描述

dict結構內部包含兩個hashtable,通常情況下只有一個hashtable是有值的。但是在dict擴容縮容時,需要分配新的hashtable,然後進行漸進式搬遷,這時候兩個hashtable存儲的分別是舊的hashtable和新的hashtable。待搬遷結束後,舊的hashtable被刪除,新的hashtable取而代之

Redis的哈希表使用鏈地址法來解決鍵衝突,每個哈希表節點都有一個next指針,多個哈希表節點可以用next指針構成一個單向鏈表,被分配到同一個索引上的多個節點可以用這個單向鏈表連接起來,這就解決了鍵衝突的問題

漸進式rehash

爲了讓哈希表的負載因子維持在一個合理的範圍之內,當哈希表保存的鍵值對數量太多或者太少時,程序需要對哈希表的大小進行相應的擴展或者收縮,擴展和收縮哈希表的工作可以通過執行rehash(重新散列)操作來完成

爲了避免rehash對服務器性能造成影響,服務器不是一次性將ht[0]裏面的所有鍵值對全部rehash到ht[1],而是分多次、漸進式地將ht[0]裏面的鍵值對慢慢地rehash到ht[1]

1)、爲ht[1]分配空間,讓字典同時持有ht[0]和ht[1]兩個哈希表

2)、在字典中維持一個索引計數器變量rehashidx,並將它的值設置爲0,表示rehash工作正式開始

3)、在rehash進行期間,每次對字典執行添加、刪除、查找或者更新操作時,程序除了執行指定的操作以外,還會順帶將ht[0]哈希表在rehashidx索引上的所有鍵值對rehash至ht[1],當rehash工作完成之後,程序將rehashidx屬性的值增一

4)、隨着字典操作的不斷進行,最終在某個時間點上,ht[0]的所有鍵值對都會被rehash至ht[1],這時程序將rehashidx屬性的值設爲-1,表示rehash操作已完成

因爲在進行漸進式rehash的過程中,字典會同時使用ht[0]和ht[1]兩個哈希表,所以在漸進式rehash進行期間,字典的刪除、查找、更新等操作會在兩個哈希表上進行。而新添加到字典的鍵值對一律會被保存到ht[1]裏面,而ht[0]則不再進行任何添加操作

4、跳錶

跳躍表是一種有序數據結構,它通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的,支持平均O(logN)、最壞O(N)複雜度的節點查找

Redis使用跳躍表實現有序集合

在這裏插入圖片描述

上圖中一個跳躍表示例,位於圖片最左邊的是zskiplist結構,該結構包含以下屬性:

  • header:指向跳躍表的表頭節點
  • tail:指向跳躍表的表尾節點
  • level:記錄目前跳躍表內,層數最大的那個節點的層數
  • length:記錄跳躍表的長度,也即是,跳躍表目前包含節點的數量

位於zskiplist結構右方的是四個zskiplistNode結構,該結構包含以下屬性:

  • 層:節點中用L1、L2、L3等標記節點的各個層,L1代表第一層、L2代表第二層,每個層都帶有兩個屬性:前進指針和跨度。前進指針用於訪問表尾方向的其他節點,而跨度則記錄了前進指針所指向節點和當前節點的距離
  • 後退指針:節點中用BW字樣標記節點的後退指針,它指向位於當前節點的前一個節點。後退指針在程序從表尾向表頭遍歷時使用
  • 分值:各個節點中的1.0、2.0和3.0是節點所保存的分值。跳躍表中的所有節點都按分值從小到大來排序
  • 成員對象:各個節點的o1、o2、o3是節點所保存的成員對象

三、過期鍵刪除策略

Redis使用的是惰性刪除定期刪除兩種策略

1、惰性刪除

過期鍵的惰性刪除策略由db.c/expireIfNeeded函數實現,所有讀寫數據庫的Redis命令在執行之前都會調用expireIfNeeded函數對輸入鍵進行檢查:

  • 如果輸入鍵已經過期,那麼expireIfNeeded函數將輸入鍵從數據庫中刪除
  • 如果輸入鍵未過期,那麼expireIfNeeded函數不做動作

在這裏插入圖片描述

2、定期刪除

過期鍵的定期刪除策略由redis.c/activeExpireCycle函數實現,每當Redis的服務器週期性操作redis.c/serverCron函數執行時,activeExpireCycle函數就會被調用,它在規定的時間內,分多次遍歷服務器中的各個數據庫,從數據庫的expires字典中隨機檢查一部分鍵的過期時間,並刪除其中的過期鍵

activeExpireCycle函數的工作模式:

  • 函數每次運行時,都會從一定數量的數據庫中取出一定數量的隨機鍵進行檢查,並刪除其中的過期鍵
  • 有一個全局變量current_db會記錄當前activeExpireCycle函數檢查的進度,並在下一次activeExpireCycle函數調用時,接着上一次的進度進行處理
  • 隨着activeExpireCycle函數的不斷執行,服務器中的所有數據庫都會被檢查一遍,這時函數將current_db變量重置爲0,然後再次開始新一輪的檢察工作

四、Redis內存回收策略

當Redis內存使用達到maxmemory上限時觸發內存溢出控制策略,具體策略受maxmemory-policy參數控制,Redis支持6種策略

  • noeviction:默認策略,當內存不足以容納新寫入數據時,新寫入操作會報錯
  • allkeys-lru:當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的key。推薦使用
  • allkeys-random:當內存不足以容納新寫入數據時,在鍵空間中,隨機移除某個key
  • volatile-lru:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,移除最近最少使用的key
  • volatile-random:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,隨機移除某個key
  • volatile-ttl:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,刪除存活時間最短的key。如果沒有對應的鍵,則回退到noeviction策略

五、RDB和AOF

1、RDB

RDB是Redis默認的持久化方案。在指定的時間間隔內,執行指定次數的寫操作,則會將內存中的數據寫入到磁盤中,即在指定目錄下生成一個dump.rdb文件,Redis重啓會通過加載dump.rdb文件恢復數據

1)、RDB文件的創建與載入

有兩個Redis命令可以用於生成RDB文件,一個是SAVE,另一個是BGSAVE

SAVE命令會阻塞Redis服務器進程,直到RDB文件創建完畢爲止,在服務器進程阻塞期間,服務器不能處理任何命令請求

BGSAVE命令會派生出一個子進程,然後由子進程負責創建RDB文件,服務器進程繼續處理命令請求

RDB文件的載入工作是在服務器啓動時自動執行的,所以Redis並沒有專門用於載入RDB文件的命令,只要Redis服務器在啓動時檢測到RDB文件存在,它就會自動載入RDB文件

因爲AOF文件的更新頻率通常比RDB文件的更新頻率高,所以:

  • 如果服務器開啓了AOF持久化功能,那麼服務器會優先使用AOF文件來還原數據庫狀態
  • 只有在AOF持久化功能處於關閉狀態時,服務器纔會使用RDB文件來還原數據庫狀態

2)、RDB文件載入時的服務器狀態

服務器在載入RDB文件期間,會一直處於阻塞狀態,直到載入工作完成爲止

2、AOF

AOF持久化是通過保存Redis服務器所執行的寫命令來記錄數據庫狀態的,默認不開啓

1)、AOF持久化的實現

1)命令追加

當AOF持久化功能處於打開狀態時,服務器在執行完一個寫命令之後,會以協議格式將被執行的寫命令追加到服務器狀態的aof_buf緩衝區的末尾

2)AOF文件的寫入與同步

Redis的服務器進程就是一個事件循環,這個循環中的文件事件負責接收客戶端的命令請求,以及向客戶端發送命令回覆,而時間事件則負責執行serverCron函數這樣需要定時運行的函數

因爲服務器在處理文件事件時可能會執行寫命令,使得一些內容被追加到aof_buf緩衝區裏面,所以在服務器每次結束一個事件循環之前,它都會調用flushAppendOnlyFile函數,考慮是否需要將aof_buf緩衝區中的內容寫入和保存到AOF文件裏面

2)、AOF文件的載入與數據還原

Redis讀取AOF文件並還原數據庫狀態的詳細步驟如下:

1)創建一個不帶網絡連接的僞客戶端:因爲Redis的命令只能在客戶端上下文中執行,而載入AOF文件時所使用的命令直接來源於AOF文件而不是網絡連接,所以服務器使用了一個沒有網絡連接的僞客戶端來執行AOF文件保存的寫命令,僞客戶端執行命令的效果和帶網絡連接的客戶端執行命令的效果完全一樣

2)從AOF文件中分析並讀取出一條寫命令

3)使用僞客戶端執行被讀出的寫命令

4)一直執行步驟2和步驟3,直到AOF文件中的所有寫命令都被處理完畢爲止

在這裏插入圖片描述

3)、AOF重寫

爲了解決AOF文件體積膨脹的問題,Redis提供了AOF文件重寫功能。通過該功能,Redis服務器可以創建一個新的AOF文件來替代現有的AOF文件,新舊兩個AOF文件所保存的數據庫狀態相同,但新AOF文件不會包含任何浪費空間的冗餘命令,所以新AOF文件的體積通常會比舊AOF文件的體積要小很多

1)AOF文件重寫的實現

AOF重寫功能的實現原理:首先從數據庫中讀取鍵現在的值,然後用一條命令去記錄鍵值對,代替之前記錄這個鍵值對的多條命令

aof_rewrite函數生成的新AOF文件只包含還原當前數據庫所必須的命令,所以新AOF文件不會浪費任何硬盤空間

2)AOF後臺重寫

aof_rewrite函數可以很好地完成創建一個新AOF文件的任務,但是因爲這個函數會進行大量的寫入操作,所以調用這個函數的線程將被長時間阻塞,因爲Redis服務器使用單個線程來處理命令請求,所以如果由服務器直接調用aof_rewrite函數的話,那麼在重寫AOF文件期間,服務器將無法處理客戶端發來的命令請求

Redis將AOF重寫程序放到子進程裏執行,這樣做可以同時達到兩個目的:

  • 子進程進行AOF重寫期間,服務器進程可以繼續處理命令請求
  • 子進程帶有服務器進程的數據副本,使用子進程而不是線程,可以在避免使用鎖的情況下,保證數據的安全性

Redis服務器設置了一個AOF重寫緩衝區,這個緩衝區在服務器創建子進程之後開始使用,當Redis服務器執行完一個寫命令之後,它會同時將這個寫命令發送給AOF緩衝區和AOF重寫緩衝區

在子進程執行AOF重寫期間,服務器進程需要執行以下三個工作:

  • 執行客戶端發來的命令

  • 將執行後的寫命令追加到AOF緩衝區

  • 將執行後的寫命令追加到AOF重寫緩衝區

在這裏插入圖片描述

3、混合持久化

Redis4.0版本添加了新的混合持久化方式,混合持久化就是同時結合RDB持久化以及AOF持久化混合寫入AOF文件。這樣做的好處是可以結合RDB和AOF的優點,快速加載同時避免丟失過多的數據,缺點是AOF裏面的RDB部分就是壓縮格式不再是AOF格式,可讀性差

1)、開啓混合持久化

4.0版本的混合持久化默認關閉的,通過aof-use-rdb-preamble配置參數控制,yes則表示開啓,no表示禁用,默認是禁用的

2)、混合持久化過程

混合持久化同樣也是通過bgrewriteaof完成的,不同的是當開啓混合持久化時,fork出的子進程先將共享的內存副本全量的以RDB方式寫入aof文件,然後在將重寫緩衝區的增量命令以AOF方式寫入到文件,寫入完成後通知主進程更新統計信息,並將新的含有RDB格式和AOF格式的AOF文件替換舊的的AOF文件。新的AOF文件前半段是RDB格式的全量數據後半段是AOF格式的增量數據

3)、數據恢復

當開啓了混合持久化時,啓動Redis依然優先加載AOF文件,AOF文件加載可能有兩種情況如下:

  • AOF文件開頭是RDB的格式, 先加載RDB內容再加載剩餘的AOF
  • AOF文件開頭不是RDB的格式,直接以AOF格式加載整個文件

4、RDB和AOF優缺點對比

1)、RDB的優點

  • RDB是一個快照文件,數據很緊湊,它保存了Redis在某個時間點上的數據集,體積比較小
  • RDB適合用於災難恢復,因爲它只有一個文件,而且體積小,方便拷貝
  • RDB可以最大化Redis的性能:父進程在保存RDB文件時唯一要做的就是fork出一個子進程,然後這個子進程就會處理接下來的所有保存工作,父進程無須執行任何磁盤I/O操作
  • RDB在恢復大數據集時的速度比AOF的恢復速度要快

2)、RDB的缺點

  • 服務器故障時候會丟失數據。雖然可以調整RDB文件的保存頻率,但是要保存整個數據集的快照,也不可能太頻繁。所以使用RDB如果服務器出現故障可能出現丟失幾分鐘的數據
  • 每次保存RDB的時候,Redis都要fork()出一個子進程,並由子進程來進行實際的持久化工作。在數據集比較龐大時,fork()可能會非常耗時,造成服務器在某某毫秒內停止處理客戶端;如果數據集非常巨大,並且CPU時間非常緊張的話,那麼這種停止時間甚至可能會長達整整一秒

3)、AOF的優點

  • AOF的默認策略爲每秒鐘fsync一次,在這種配置下,Redis仍然可以保持良好的性能,並且就算髮生故障停機,也最多隻會丟失一秒鐘的數據(fsync會在後臺線程執行,所以主線程可以繼續努力地處理命令請求),也可以根據實際情況設置fsync的策略
  • AOF文件是一個只進行追加操作的日誌文件,因此對AOF文件的寫入不需要進行seek,即使日誌因爲某些原因而包含了未寫入完整的命令(比如寫入時磁盤已滿,寫入中途停機等等),redis-check-aof工具也可以輕易地修復這種問題
  • Redis可以在AOF文件體積變得過大時,自動地在後臺對AOF進行重寫:重寫後的新AOF文件包含了恢復當前數據集所需的最小命令集合。整個重寫操作是絕對安全的,因爲Redis在創建新AOF文件的過程中,會繼續將命令追加到現有的AOF文件裏面,即使重寫過程中發生停機,現有的AOF文件也不會丟失。而一旦新AOF文件創建完畢,Redis就會從舊AOF文件切換到新AOF文件,並開始對新AOF文件進行追加操作
  • AOF文件有序地保存了對數據庫執行的所有寫入操作,這些寫入操作以Redis協議的格式保存,因此AOF文件的內容非常容易被人讀懂,對文件進行分析也很輕鬆。導出AOF 文件也非常簡單:舉個例子,如果不小心執行了 FLUSHALL命令,但只要AOF文件未被重寫,那麼只要停止服務器,移除AOF文件末尾的FLUSHALL命令,並重啓Redis,就可以將數據集恢復到FLUSHALL執行之前的狀態

4)、AOF的缺點

  • 對於相同的數據集來說,AOF文件的體積通常要大於RDB文件的體積
  • 根據所使用的fsync策略,AOF的速度可能會慢於RDB。在一般情況下,每秒fsync的性能依然非常高,而關閉fsync可以讓AOF的速度和RDB一樣快,即使在高負荷之下也是如此。不過在處理巨大的寫入載入時,RDB可以提供更有保證的最大延遲時間

六、Redis Pipeline

Redis客戶端執行一條命令分4個過程:

發送命令-〉命令排隊-〉命令執行-〉返回結果

這個過程稱爲Round trip time(簡稱RTT, 往返時間),mget mset有效節約了RTT,但大部分命令(如hgetall,並沒有mhgetall)不支持批量操作,需要消耗N次RTT ,這個時候需要Pipeline來解決這個問題

未使用Pipeline執行N條命令:

在這裏插入圖片描述

使用了Pipeline執行N條命令:

在這裏插入圖片描述

Redis Pipeline指在服務端未響應時,客戶端可以繼續向服務端發送請求,並最終一次性讀取所有服務端的響應。Pipeline能減少客戶端和服務端交互的次數,將客戶端的請求批量發送給服務器,服務器針對批量數據分別查詢並統一回復

原生批命令(mset、mget)與Pipeline對比

  • 原生批量命令是原子性,Pipeline是非原子性的
  • 原生批量命令是一個命令對應多個key,Pipeline支持多個命令
  • 原生批量命令是Redis服務端支持實現的,而Pipeline需要服務端與客戶端的共同實現

七、Redis事務

Redis事務可以一次執行多個命令, 並且帶有以下三個重要的保證:

  • 批量操作在發送EXEC命令前被放入隊列緩存
  • 收到EXEC命令後進入事務執行,事務中任意命令執行失敗,其餘的命令依然被執行
  • 在事務執行過程,其他客戶端提交的命令請求不會插入到事務執行命令序列中

一個事務從開始到執行會經歷以下三個階段:

  • 開始事務
  • 命令入隊
  • 執行事務

Redis事務相關命令:

  • watch key1 key2 ...:監視一或多個key,如果在事務執行之前,被監視的key被其他命令改動,則事務被打斷(類似樂觀鎖)
  • multi:標記一個事務塊的開始
  • exec:執行所有事務塊的命令(一旦執行exec後,之前加的監控鎖都會被取消掉)
  • discard:取消事務,放棄事務塊中的所有命令
  • unwatch:取消watch對所有key的監控
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章