目錄
第2章:簡單動態字符串
2.1 SDS定義
2.2 SDS與C字符串的區別
第3章:鏈表
3.1鏈表和表節點的實現
第4章:字典
4.1 字典的實現
4.2 哈希算法
4.3 鍵沖突解決:類似於hashmap(個人理解)
4.4 rehash(重新散列)
4.5 漸進式rehash
第5章:跳躍表
5.1 跳躍表實現
6.2 升級
第6章:整數集合
6.1 整數集合的實現
第7章:壓縮列表
第8章:對象
第9章:數據庫
9.1 服務器中的數據
9.2 切換數據庫
9.3 數據庫鍵空間
9.4 設置鍵的生存和過期時間
9.5 過期鍵刪除策略
9.6 redis的過期鍵刪除策略(惰性刪除 + 定期刪除)
9.7 AOF、RDB 和復制功能對過期鍵的處理
第10章:RDB持久化
第11章:AOF持久化
第15章:復制
15.1&2 舊版復制功能的實現和缺陷
15.3&4 新版復制的實現、部分重同步的實現
15.5 PSYNC命令的實現
15.7 心跳檢測
第16章:Sentinel
第2章:簡單動態字符串
2.1 SDS定義
/* * 保存字符串對象的結構 */ struct sdshdr { int len; // buf 中已占用空間的長度 int free; // buf 中剩余可用空間的長度 char buf[]; // 數據空間 };
2.2 SDS與C字符串的區別
第3章:鏈表
3.1鏈表和表節點的實現
節點結構
/* * 雙端鏈表節點 */ 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)雙向:每個listNode節點帶有prev和next指針,可以找到前一個節點和后一個節點,具有雙向性。 2)無環:list鏈表的head節點的prev和tail節點的next指針都是指向null。 3)帶表頭指針和尾指針:即上述的head和tail,獲取頭指針和尾指針的時間復雜度O(1)。 4)帶鏈表長度計數器;即list的len屬性,記錄節點個數,因此獲取節點個數的時間復雜度O(1)。 5)多態:鏈表使用void*指針來保存節點的值,可以通過list的dup、free、match三個屬性為節點值設置類型特定函數,所以鏈表可以用於保存不同類型的值。
第4章:字典
字典在redis中的應用相當廣泛,比如redis的數據庫就是使用字典作為底層實現的,對數據庫的增刪改查操作也是構建在對字典的操作之上的。
4.1 字典的實現
4.1.1 哈希表
/* * 每個字典都使用兩個哈希表,從而實現漸進式 rehash 。 */ typedef struct dictht dictEntry **table; // 哈希表數組 unsigned long size; // 哈希表大小;也是table數組大小 unsigned long sizemask; // 哈希表大小掩碼,用於計算索引值;總是等於 size - 1 unsigned long used; // 該哈希表已有節點的數量 } dictht;
4.1.2 哈希表節點
/* * 哈希表節點 */ typedef struct dictEntry { void *key; // 鍵 union { // 值 void *val; uint64_t u64; int64_t s64; } v; struct dictEntry *next; // 指向下個哈希表節點,形成鏈表;指向另一個哈希表節點的指針,該指針將多個哈希值相同的鍵值對連接在一起,避免因為哈希值相同導致的沖突。 } dictEntry;
4.1.3 字典
/* * 字典 */ typedef struct dict { dictType *type; // 類型特定函數(不懂) void *privdata; // 私有數據(不懂) dictht ht[2]; // 哈希表,其中h{0}為平時使用的,h[1]為rehash使用的; int rehashidx; // rehash 索引;當 rehash 不在進行時,值為 -1(rehashing not in progress if rehashidx == -1) int iterators; // // 目前正在運行的安全迭代器的數量;(number of iterators currently running)(不懂) } dict;
4.2 哈希算法
要將新的鍵值對加到字典,程序要先對鍵進行哈希算法,算出哈希值和索引值,再根據索引值,把包含新鍵值對的哈希表節點放到哈希表數組指定的索引上。redis實現哈希的代碼是: hash =dict->type->hashFunction(key); index = hash& dict->ht[x].sizemask; 算出來的結果中,index的值是多少,則key會落在table里面的第index個位置(第一個位置index是0)。 其中,redis的hashFunction,采用的是murmurhash2算法:即使加入的鍵是有規律的,算法仍能給出一種很好的隨機分布性,並且算法的計算速度也非常快。
4.3 鍵沖突解決:類似於hashmap(個人理解)
1,redis的哈希表使用鏈地址法(seprate chaining)來解決鍵沖突,每個哈希表節點都有一個next指針,多個哈希表節點可以用next指針構成一個單向鏈表,被分配到同一個索引的多個節點可以用這些單向鏈表連接起來,這就解決了鍵沖突的問題。
2,為了速度上面的考慮,程序總是將新節點添加到鏈表的表頭位置,復雜度為0(1),排在其它已有節點的前面。
4.4 rehash(重新散列)
4.5 漸進式rehash
第5章:跳躍表
跳躍表支持評價O(logN)、最壞O(N)復雜度的節點查找,還可以通過順序性操作來批量處理節點。
在大部分情況下,跳躍表的效率可以和平衡樹相媲美,並且因為跳躍表的實現比平衡樹來得更簡單,所以有不少程序都是用跳躍表來代替平衡樹。
5.1 跳躍表實現
上圖最左邊的就是zskiplist結構,該結構包含以下屬性: 1 header:指向跳躍表的表頭表頭節點。 2 tail:指向跳躍表的表尾節點。 3 level:記錄目前跳躍表內,層數最大的那個節點的層數(表頭節點的層數不計算在內)。 4 length:記錄跳躍表的長度,也即是,跳躍表目前包含節點的數量(表頭節點不計算在內)。 位於zskiplist結構右方的四個zskiplistNode結構,該結構包含一下屬性: 1 層(level):節點中用L1、L2、L3等字樣標記節點的各個層,L1代表第一層,L2代表第二層,以此類推。每個層都帶有兩個屬性:前進指針和跨度。前進指針用於訪問位於表尾方向的其他節點,而跨度則記錄了前進指針所指向節點和當前節點的舉例。在上圖中,連線上帶有數字的箭頭就代表前進指針,而那個數字就是跨度。當程序從表頭向表尾進行遍歷時,訪問會沿着層的前進指針進行。 2 后退指針:節點中用BW(backward)字樣標記節點的后退指針,他指向位於當前節點的前一個節點。后退指針在程序從表尾向表頭遍歷時使用。 3 分值(score):各個節點中的1.0、2.0和3.0是節點所保存的分值。在跳躍表中,節點按各自所保存的分值從小到大排量。 4 成員對象(obj):各個節點中o1、o2和o3是節點所保存的成員對象。
注意:表頭節點和其他節點的構造是一樣的;表頭節點也有后退指針、分值和成員對象,不過表頭節點的這些屬性不會被用到,所以圖中省略了。
***跳躍表節點:
/* ZSETs use a specialized version of Skiplists */ /* * 跳躍表節點 */ typedef struct zskiplistNode { robj *obj; // 成員對象 double score; // 分值 struct zskiplistNode *backward; // 后退指針 // 層 struct zskiplistLevel { struct zskiplistNode *forward; // 前進指針 unsigned int span; // 跨度 } level[]; } zskiplistNode;
1)層
跳躍表節點的level數組可以包含多個元素,每個元素都包含一個指向其他節點的指針,程序可以通這些層來加快訪問其他節點的速度,一般來說,層數越多,訪問其他節點的速度就越快。 每次創建一個新的跳躍表節點的時候,程序都根據冪次定律(power law,越大的數出現的概率越小)隨機生成一個介於1和32之間的值作為level數組的大小,這個大小就是層的“高度”。下圖就是帶有不同層高的節點。
2)前進指針
每個層都有一個指向表尾方向的前進指針,用於從表頭向表尾方向訪問節點。下圖用虛線表示出了程序從表頭向表尾方向,遍歷跳躍表中所有節點的路徑: a 迭代程序首先訪問跳躍表的第一個節點(表頭),然后從第四層的前進指針移動到表中的第二個節點。 b 在第二個節點時,程序沿着第二層的前進指針移動到表中第三個節點。 c 在第三個節點時,程序同樣沿着第二層的前進指針移動到表中的第四個節點。 d 當程序再次沿着第四個節點的前進指針移動式,他碰到一個null,程序知道這時已經到達了跳躍表的表尾,於是結束這次遍歷。
3)跨度
層的跨度用於記錄兩個節點之間的距離: a 兩個節點之間的跨度越大,他們相距得就越遠。 b 指向null的所有前進指針的跨度都為0,因為他們沒有連向任何節點。 初看上去,很容易以為跨度和遍歷操作有關,但實際上並不是這樣,遍歷操作只使用前進指針就可以完成了,寬度實際上是用來計算排位(rank)的:在查找某個節點的過程中,將沿途訪問過的所有層的跨度累計起來,得到的結果就是目標節點在跳躍表中的排位。 舉個例子,下圖用虛線標記了在跳躍表中查找分值為3.0、成員對象為o3的節點時,沿途經歷的層:查找的過程只經過了一個層,並且層的跨度為3,所以目標節點在跳躍表中的排位為3。
再舉個例子,下圖用虛線標記了在跳躍表中查找分值為2.0、成員對象為o2的節點時,沿途經歷的層:在查找節點的過程中,程序經過了兩個跨度為1的節點,因此可以計算出,目標節點在跳躍表中的排位為2。
4)后退指針
節點的后退指針用於從表尾向表頭方向訪問節點:跟可以一次跳過多個節點的前進指針不同,因為每個節點只有一個后退指針,所以每次只能后退至前一個節點。
5)分值和成員
節點的分值(score屬性)是一個double類型的浮點數,跳躍表中的所有節點都按照分值從小到大來排序。 節點的成員對象是一個指針,他指向一個字符串對象,而字符串對象則保存着一個SDS值。 在同一個跳躍表中,各個節點保存的成員對象必須是唯一的,但是多個節點保存的分值卻可以是相同的:分值相同的節點按照成員對象在字典中的大小來進行排序,成員對象較小的節點會排在前面(靠近表頭方向),而成員對象較大的節點則會排在后面(靠近表尾的方向)。 舉個例子,在下圖所示的跳躍表中,三個跳躍表節點都保存了相同的分值10086.0,但保存成員對象o1的節點卻排在保存成員對象o2和o3的節點之前,由順序可知,三個對象在字典中的排序哦o1<=o2<=o3。
***跳躍表
/*
* 通過使用一個zskiplist結構來持有這些節點,程序可以更方便地對整個跳躍表進行處理,比如快速訪問跳躍表的表頭節點和表尾節點,或者快速地獲取跳躍表節點的數量等信息。
* header和tail指針分別指向跳躍表的表頭和表尾節點,通過這兩個指針,程序定位表頭及誒點和表尾節點的復雜度為O(1)。 * 通過使用length屬性來記錄節點的數量,程序可以在O(1)復雜度內返回跳躍表的長度。
* level屬性則用於在O(1)復雜度內獲取跳躍表中層高最大的那個節點的層數量,注意表頭節點的層高並不計算在內。 */ typedef struct zskiplist { struct zskiplistNode *header, *tail; // 表頭節點和表尾節點 unsigned long length; // 表中節點的數量 int level; // 表中層數最大的節點的層數 } zskiplist;
第6章:整數集合
6.1 整數集合的實現
typedef struct intset { uint32_t encoding; // 編碼方式 unit32_t length; // 集合包含的元素數量 int8_t contents[]; // 保存元素的數組 }inset;
6.2 升級
每當我們要將一個新元素添加到一個整數集合里面,並且新元素的類型比整數集合現有所有元素的類型都要長時,整數集合需要先進行升級,然后才能將新元素添加到整數集合里面;
因為每次向整數集合添加新元素都可能會引起升級,而每次升級都需要對底層數組中已有的元素進行類型轉換,所以向整數集合添加新元素的時間復雜度為o(N)。
升級的好處:提升靈活性;節約內存。
整數集合只支持升級操作,不支持降級操作。
第7章:壓縮列表
1)壓縮列表是一種為節約內存而開發的順序型數據結構;
2)壓縮列表被用作列表建和哈希鍵的實現之一;
3)壓縮列表是Redis為了節約內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型(sequential)數據結構一個壓縮列表可以包含任意多個節點(entry),每個節點可以保存一個字節數組或者一個整數值;
4)添加新節點到壓縮列表,或者從壓縮列表中刪除節點,可能會引發連鎖更新操作,但這種操作出現的幾率並不高;
第8章:對象
redis 並沒有使用這些數據結構來實現鍵值對數據庫,而是基於這些數據結構來創建了一個對象系統,這些系統包含字符串對象、列表對象、哈希對象、集合對象和有序集合對象這五種類型的對象,每種對象都用到了至少一種我們前面所介紹的數據結構。
typedef struct redisObject{ unsigned type:4; //類型 unsigned encoding:4; //編碼 void *ptr; //指向底層實現數據結構的指針 .... }robj;
-- redis對象數據結構
1)redis_encoding_embstr 編碼比 redis_encoding_raw 編碼更叼;
2)可以用long,double類型表示的浮點數在redis中也是作為字符串值來保存的。如果我們要保存一個浮點數到字符串對象里面,那么程序會先將這個浮點數轉換成字符串值,然后再保存轉換所得的字符串值。要用的時候,再取出這個字符串,再轉換成浮點型;
第9章:數據庫
9.1 服務器中的數據庫
redis服務器將所有數據庫都保存在服務器狀態redis.h/redisServer結構的db數組中,db數組中的每一項都是一個redis.h/redisDb結構,每個redisDb結構代表一個數據庫
struct redisServer { redisDb *db; // 一個數組,保存着服務器中的所有數據庫 int dbnum; // 服務器的數據庫數量,在初始化服務器時,程序會根據服務器狀態的dbnum屬性來決定應該創建多少個數據庫,默認是16 struct saveparam *saveparams; // 記錄了保存條件的數組 它包括time_t seconds int changes秒數和修改次數 long long dirty; // 修改計數器 time_t lastsave; // 上一次保存的時間 sds aof_buf; // AOF緩沖區 ... } typedef struct redisClient { redisDb *db; // 客戶端正在使用的數據庫 ... } typedef struct redisDb { dict *dict; // 數據庫鍵空間,保存着數據庫中的所有鍵值對 key永遠是字符串對象,value可以是五大類型 dict *expires // 過期字典,保存着鍵的過期時間;key為指向上面那個dict的key,value為過期時間的毫秒時間戳 ... }
9.2 切換數據庫
redisClient.db 指針指向 redisServer.db 數組的其中一個元素,而被指向的元素就是客戶端的目標數據庫
9.3 數據庫鍵空間
Redis是一個鍵值對數據庫服務器,服務器中的每個數據庫都由一個redis.h/redisDb結構表示,其中redisDB的dict字典保存了數據庫中的所有鍵值對,我們將這個字典稱為鍵空間。
9.4 設置鍵的生存和過期時間
9.5 過期鍵刪除策略
- 定時刪除
- 含義:在設置key的過期時間的同時,為該key創建一個定時器,讓定時器在key的過期時間來臨時,對key進行刪除
- 優點:保證內存被盡快釋放
- 缺點:
- 若過期key很多,刪除這些key會占用很多的CPU時間,在CPU時間緊張的情況下,CPU不能把所有的時間用來做要緊的事兒,還需要去花時間刪除這些key
- 定時器的創建耗時,若為每一個設置過期時間的key創建一個定時器(將會有大量的定時器產生),性能影響嚴重
- 沒人用
- 惰性刪除
- 含義:key過期的時候不刪除,每次從數據庫獲取key的時候去檢查是否過期,若過期,則刪除,返回null。
- 優點:刪除操作只發生在從數據庫取出key的時候發生,而且只刪除當前key,所以對CPU時間的占用是比較少的,而且此時的刪除是已經到了非做不可的地步(如果此時還不刪除的話,我們就會獲取到了已經過期的key了)
- 缺點:若大量的key在超出超時時間后,很久一段時間內,都沒有被獲取過,那么可能發生內存泄露(無用的垃圾占用了大量的內存)
- 定期刪除
- 含義:每隔一段時間執行一次刪除過期key操作
- 優點:
- 通過限制刪除操作的時長和頻率,來減少刪除操作對CPU時間的占用--處理"定時刪除"的缺點
- 定期刪除過期key--處理"惰性刪除"的缺點
- 缺點
- 在內存友好方面,不如"定時刪除"
- 在CPU時間友好方面,不如"惰性刪除"
- 難點
- 合理設置刪除操作的執行時長(每次刪除執行多長時間)和執行頻率(每隔多長時間做一次刪除)(這個要根據服務器運行情況來定了)
9.6 redis的過期鍵刪除策略(惰性刪除 + 定期刪除)
定期刪除:由redis.c/activeExpireCycle函數實現,每當Redis的服務器周期性操作redis.c/serverCron函數執行時,actieExpireCycle函數就會被調用,它在規定的時間內,分多次遍歷服務器中的各個數據庫,從數據庫的expires字典中隨機檢查一部分鍵的過期時間,並刪除其中的過期鍵。
1)函數每次運行時,都從一定數量的數據庫中取出一定數量的隨機鍵進行檢查,並刪除其中的過期鍵;
2)全局變量current_db會記錄當前activeExpireCycle函數檢查的進度,並在下一次activeExpireCycle函數調用時,接着上一次的進度進行處理,比如說,如果當前activeExpireCycle函數在遍歷10號數據庫返回了,那么下次activeExpireCycle函數執行時,將從11號數據庫開始查找並刪除過期鍵;
3)隨着activeExpireCycle函數的不斷執行,服務器中所有數據庫都會被檢查一遍,這時函數將current_db變量重置為0,然后再次開始新一輪的檢查工作;
9.7 AOF、RDB 和復制功能對過期鍵的處理
生成RDB文件:在執行SAVE命令或者BGSAVE命令創建一個新的RDB命令時,程序會對數據庫中的鍵進行檢查,已過期的鍵不會被保存到新創建的RDB文件中;
載入RDB文件:
以主服務器的模式運行:在載入RDB文件時,程序會對文件中的鍵進行檢查,未過期的鍵會被載入數據庫中,而過期鍵則會被忽略;
以從服務器的模式運行:在載入RDB文件時,程序會對文件中的鍵進行檢查,不論鍵是否過期,都會被載入到數據庫中。不過,因為主從服務器在進行數據同步的時候,從服務器的數據就會被清空;
AOF文件寫入:當過期鍵被惰性刪除或定期刪除后,程序會向AOF追加(append)一條DEL命令,來顯示記錄該鍵已被刪除;
AOF重寫:在執行AOF重寫的過程中,程序會對數據庫中的鍵進行檢查,已過期的鍵不會被保存到重寫后的AOF文件中;
復制:當服務器運行在復制模式下時,從服務器的過期鍵刪除動作由主服務器控制:
1)主服務器在刪除一個過期鍵之后,會顯式地向所有從服務器發送一個del命令,告知從服務器刪除這個過期鍵
2)從服務器在執行客戶端發送的讀命令時,即使碰到過期鍵也不會將過期鍵刪除,而是繼續像處理未過期鍵一樣處理過期鍵
3)從服務器只有在接到主服務器發來的del命令之后,才會刪除過期鍵。
第10章:RDB持久化
save命令:會阻塞redis服務器進程,知道RDB文件創建完畢為止,在服務器進程阻塞期間,服務器不能處理任何命令請求; bgsave命令:會派生出一個子進程,然后由子進程負責創建RDB文件,服務器不能處理任何命令請求;
注意:SAVE/BGSAVE/BGREWRITEOF 這三個命令不能同時執行,因為避免產生競爭條件,或者性能方面考慮
因為AOF文件的更新頻率通常比RDB文件的更新頻率高,所以: 1)如果服務器開啟了AOF持久化功能,那么服務器會優先使用AOF文件來還原數據庫狀態; 2)只有在AOF持久化功能處於關閉狀態時,服務器才會使用RDB文件來還原數據庫狀態;
載入RDB文件的實際工作有rdb.c/rdbLoad函數完成,這個函數和rdbSave函數之間的關系可以用圖10-5表示:
第11章:AOF持久化
與RDB持久化通過保存數據庫中的鍵值對來記錄數據庫狀態不同,AOF持久化是通過保存redis服務器所執行的寫命令來記錄數據庫狀態的
AOF重寫:不需要對現有的AOF文件進行任何讀取、分析或者寫入操作,這個功能是通過讀取服務器當前的數據庫狀態來實現的。(一條命令就搞定,替代之前的多條命令)
第15章:復制
15.1&2 舊版復制功能的實現和缺陷
Redis的復制功能分為同步(sync)和命令傳播(command propagate)兩個操作: 同步操作用於將服務器的數據庫狀態更新至主服務器當前的數據庫狀態; 命令傳播操作則用於在主服務器的數據庫狀態被修改,導致主從服務器的數據庫狀態出現不一致時,讓主從服務器的數據庫重新回到一致狀態;
15.1.1 舊版復制/同步
15.1.2 命令傳播
Redis中,從服務器對主服務器的復制可以分為以下兩種情況: 初次復制:從服務器以前沒有復制過任何主服務器,或者從服務器當前要復制的主服務器和上一次復制的主服務器不同; 斷線后復制:處於命令傳播階段的主從服務器因為網絡原因而中斷了復制,但從服務器通過自動重新連接連上了主服務器,並繼續復制主服務器;
在主從服務器斷線期間,主服務器執行的寫命令可能會有成百上千個之多,而不僅僅是兩三個寫名ing。但總的來說,主從服務器斷開的時間越短,主服務器在斷線期間執行的寫命令就越少,而執行少量寫命令所鏟山的數據量通常比整個數據庫的數據量要少得多,在這種情況下,為了讓從服務器補足一小部分缺失的數據,卻要讓主服務器重新執行一次SYNC,這種做法無疑是非常低效的。
15.3&4 新版復制的實現、部分重同步的實現
為了解決舊版復制功能在處理斷線重復機制情況時的低效問題,Redis從2.8版本開始,使用PSYNC命令代替SYNC命令來執行復制時的同步操作: 完整同步(full resynchronization):用於處理初次復制的情況,和SYNC命令的執行步驟基本一樣,他們都是通過讓主服務器創建並發送RDB文件,以及向從服務器發送保存在緩沖區里面的寫命令來進行同步; 部分同步(partial resynchronization):用於處理斷線后重復機制,當從服務器在斷線后重新連接主服務器時,如果條件允許,主服務器可以將主服務器連接斷開期間執行的寫命令發送給從服務器,從服務器只要接收並執行這些寫命令,就可以將數據更新至主服務器當前所處的狀態;
PSYNC功能由一下三個部分構成: 1)主服務器的復制偏移量(replication offset)和從服務器的復制偏移量 2)主服務器的復制積壓緩沖區(replication backlog) 3)服務器的運行ID(run ID)
15.5 PSYNC命令的實現
15.7 心跳檢測
在命令傳播階段,從服務器默認會以每秒一次的頻率,向主服務器發送命令:REPLCONF ACK <replication_off>,其中 replication_offset 是從服務器當前的復制偏移量。 發送REPLCONF ACK 命令對於主從服務器有三個作用: 1)檢測主從服務器的網絡連接狀態 2)輔助實現min-slaves選項 3)檢測命令丟失:主從服務器的復制偏移量是否一致
其中,redis的min-slaves-to-write 和 min-slaves-max-lag 兩個選項可以防止主服務器在不安全的情況下執行寫命令。例如:
min-slaves-to-write 3
min-slaves-max-lag 10
在從服務器的數量少於3個,或者三個從服務器是延遲(lag)值都大於或等於10秒時,主服務器將拒絕執行寫命令。
第16章:Sentinel
THE END!