Redis學習總結

一,底層數據結構

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

在Redis數據庫裏,包含字符串值得鍵值對在底層都是由SDS實現的。

如:
127.0.0.1:6379> set msg hello
鍵msg是一個字符串對象,其底層實現是一個值爲"msg"的SDS。
值也是一個字符串對象,其底層實現是一個值爲"hello"的SDS。

SDS結構定義:

Redis學習總結

SDS遵循C字符串以‘\0’作爲結尾,保存其的一個字節的額外空間不計入SDS的len屬性裏,且添加空字符串到末尾等操作都是由SDS自動完成的,遵循空字符串結尾的好處是可以重用C字符串函數庫裏的函數。

SDS相對於C字符串的優勢:

常數複雜度獲取字符串長度
由於SDS在len屬性中保存了字符串長度,因此不需要遍歷字符串計算長度。

杜絕緩衝區溢出
由於C字符串不記錄自身長度,當拼接一個長度大於當前字符數組剩餘空間的字符串時則會出現緩衝區溢出;

而SDS再修改前會先判斷剩餘空間是否能裝下修改後的字符串,若不能,則會先進行擴容,其擴容規則爲:
--若修改後的SDS長度,即len屬性的值小於1MB,那麼程序將分配和len屬性同樣大小的未使用空間,即屬性 free的值和len相同,此時buf數組實際長度爲:len + free + 1,多餘的1用於保存結尾的空字符。
--若修改後的SDS長度大於等於1MB,那麼程序將額外分配1MB的未使用空間。

二進制安全:

C字符以空字符作爲結尾標誌,若字符串中包含空字符,則它會被誤以爲是字符串的結尾,這限制了C字符串只能保存文本數據,不能保存如圖片、視頻等二進制數據;

SDS以處理二進制的方式處理buf數組中的數據,不會對其做任何限制,數據寫入時什麼樣子,讀取出來就是什麼樣子。

2,鏈表

鏈表在Redis中應用十分廣泛,如列表的底層實現之一就是鏈表。

鏈表和鏈表節點結構定義:

鏈表:
Redis學習總結

節點:
Redis學習總結Redis學習總結

包含三個節點的鏈表:
Redis學習總結

Redis鏈表特點:

雙端:鏈表節點對prev和next指針;
無環:表頭節點prev和表尾節點next都指向NULL;
帶表頭和表尾指針:通過list的head和tail指針獲取表頭和表尾節點時間複雜度爲O(1);
長度計數器:list屬性len記錄節點數;
多態:節點使用void*指針保存節點值,並且可通過list的dup,free,match爲節點值設置特定函數,所以鏈表可以保存不同類型的值;

3,字典

保存鍵值對,鍵不可以重複,類似Java中的HashMap

字典結構定義

字典:
Redis學習總結

其中ht[2]用戶保存哈希表,一個用於保存數據,一個用於rehash時使用,其哈希表結構爲:
Redis學習總結

這個和Java中的HashMap很像,都是一個保存Entry的數組,當hash衝突時使用鏈指法,其dictEntry結構爲:
Redis學習總結

這個和HashMap裏那個內部類Node也很像

來一個完成的字典結構圖:
Redis學習總結

rehash

隨着不斷地操作,哈希表的鍵值對會逐漸增多或減少,爲了維持哈希表的負載因子處在一個合理範圍內,當哈希表保存的鍵值對太多或者太少時,程序會對哈希表進行擴展或者收縮。進行擴展的目的是爲了減少hash衝突,防止鏈表過長導致查詢效率低,收縮的話就是爲了節約內存。

其中負載因子定義爲:
load_factor = ht[0].used / ht[0].size

對於一個初始大小爲4,包含四個鍵值對的哈希表來說:
load_factor = 4 / 4 = 1

當滿足下面兩個條件之一時,哈希表將進行擴展:

1)服務器未執行BGSAVE或BGREWRITEAOF命令,且負載因子大於等於1

2)服務器正在執行BGSAVE或BGREWRITEAOF命令,且負載因子大於等於5

當負載因子小於0.1時,程序自動開始對哈希表進行收縮操作。

漸進式rehash

rehash時需要將所有ht[0]中的鍵值對全部移到ht[1]中,如果ht[0]中數據量非常龐大,那麼一次性將這些鍵全部rehash到ht[1]中的話,龐大的計算量可能導致服務器在一段時間內停止服務。因此,會分多次,漸進式的將ht[0]中的數據慢慢的rehash到ht[1]中。

漸進式rehash步驟:
1) 爲ht[1]分配空間。
2) 將字典中的rehashidx置爲0,表示rehash開始。
3) rehash期間,每次對字典的增刪改查操作時,還會順帶將ht[0]哈希表在rehashidx索引上的所有數據rehash到ht[1]上,當此rehashidx索引上的數據rehash完成後,程序會將rehashidx的值增加1。因爲此時字典會同時使用ht[0]和ht[1]兩個哈希表,所以刪除、查找、更新等操作會在兩個哈希表上進行,先在ht[0]裏查找,如果沒有找到則在ht[1]中查找,其中添加操作會直接保存到ht[1]中。
4) 所有數據rehash完成後,將rehashidx值設置爲-1,表示rehash操作完成。

4,跳躍表

跳躍表是一種有序數據結構,查找平均時間複雜度爲O(logN),最壞爲O(N),可以通過順序性操作來批處理節點。Redis使用跳躍表作爲有序集合的底層實現之一。

跳躍表結構定義

跳躍表:
Redis學習總結

跳躍表節點:
Redis學習總結

示例圖:
Redis學習總結

其中level表示跳躍表內層數最大的那個節點的層數,length表示跳躍表內節點數;如上3個節點的分值分別爲1.0、2.0、3.0

跳躍表節點的level數據可以包含多個元素,每個元素都包含一個指向其他節點的指針,可以通過這些層來加快訪問其他節點的速度,一般來說,層數越多,訪問速度越快。

每創建一個跳躍表節點時,程序會隨機生成一個介於1和32的值作爲level數組的大小,這個大小就是高度。

前進指針:

每個層都有一個指向表尾方向的前進指針,用於從表頭向表尾方向訪問節點。

跨度:

層的跨度,即level[i].span 用於記錄兩個節點之間的距離,跨度是用來計算排位(rank)的,在查找某個節點的過程中,將沿途訪問過的所有層的跨度累加起來,得到的結果就是目標節點在跳躍表中的排位。

後退指針:

節點的後退指針backward,用於從表尾向表頭方向訪問節點。

分值和成員:

分值是一個double類型的浮點數,跳躍表中所有節點都按分值從大到小排序。

節點的成員對象是一個指針,指向一個使用SDS保存的字符串對象,同一個跳躍表中,成員對象不能重複。分值可以重複,且分值相同時按成員對象字典順序進行排序。

整數集合

整數集合時集合的底層實現之一,當一個集合只包含整數值元素且元素個數不多時將使用其作爲底層實現。

結構定義

Redis學習總結

contents數組保存集合元素,按從小到大排序且不能重複。

雖然contents屬性聲明爲int8_t,但是它並不保存任何int8_t的值,contents數組真正類型取決於encoding的值,encoding取值可以是:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64,其中的16、32、64表示每個整數佔用的位數。

升級

當新加入的整數類型比所有現存的元素的類型都長時,整數集合將進行升級,將所有元素類型長度升級到新加入元素的長度。

整數集合不支持降級,一旦升上去了就降不下來了。

6,壓縮列表

壓縮列表是列表和哈希鍵的底層實現之一,當一個列表只包含少量元素,且每個元素是小整數或者短字符串時,則其將使用壓縮列表作爲底層實現。

壓縮列表結構定義

壓縮列表:
Redis學習總結

zlbytes:整個壓縮列表佔用內存字節數。

zltail:壓縮列表表尾節點距離起始地址字節數,通過使用壓縮列表起始地址指針p + zltail 就能計算出最後一個節點的地址。

zlen:壓縮列表包含節點數,當這個值小於UINT16_MAX(65535)時,這個值就是節點數;當這個值等於UINT16_MAX時,需要遍歷壓縮列表才能計算出來。

entry:列表節點。

zlend:標記壓縮列表末端。

壓縮列表節點:
Redis學習總結

每個壓縮列表可以保存一個字節數組或者一個整數值。

previous_entry_length:記錄了前一個節點的長度,根據所記錄長度大小,其內存佔用大小可以是1字節或5字節,單位字節。可以通過當前節點的地址值減去這個值計算出前一個節點的地址,結合上述通過zltail計算出的最後一個節點地址值就可以實現從後向前遍歷整個壓縮列表。

encoding:記錄節點content屬性所保存數據的類型及長度。

content:保存節點值,可以是字節數組或整數。

連鎖更新

前面說過previous_entry_length根據前一個節點的長度大小可以佔用1字節或者5字節,當前一個節點長度小於254字節時,它佔用1字節;而前一個節點長度大於等於254字節時它就佔用5字節。

現考慮這麼一種情況:
假設壓縮列表中保存若干節點,它們的長度都介於250到253字節之間,如圖:
Redis學習總結

現我們將一個長度大於254字節的新節點設置爲壓縮列表的頭節點:
Redis學習總結

由於e1之前的previous_entry_length是1字節,不足以保存長度大於254的new節點長度,因此它會擴容至5字節,使自己的長度也大於或等於254,這樣e2也就得跟着擴容了......如此直到最後一個節點。

二,對象

前面我們介紹了Redis用到的所有主要的數據結構,但Redis並沒有直接使用這些數據結構來實現鍵值對數據庫,而是基於這些數據結構創建了一個對象系統,這個系統包含:字符串、列表、哈希、集合、有序集合。

Redis使用對象來表示數據庫中的鍵和值,每當我們在Redis中創建一個鍵值對時,我們至少會創建兩個對象,一個鍵對象,一個值對象;鍵總是一個字符串對象,而值則可以是字符串對象,列表對象,哈希對象,集合對象或者有序集合對象。

Redis中每個對象都有redisObject結構表示:
Redis學習總結

1,類型及編碼

type:記錄對象類型,它可以是下圖中的任何一種
Redis學習總結

TYPE命令可以返回數據庫鍵對應的值對象的類型:

127.0.0.1:6379> set msg hello
OK
127.0.0.1:6379> rpush list hello world
(integer) 2
127.0.0.1:6379> type msg
string
127.0.0.1:6379> type list
list

不同類型值對應的type輸出:
Redis學習總結

encoding:記錄對象使用了什麼數據結構作爲對象底層實現,它可以是下圖中的任何一種
Redis學習總結

每種對象可以使用的編碼:
Redis學習總結

可以使用OBJECT ENCODING 命令查看一個數據庫鍵的值對象的編碼:

127.0.0.1:6379> object encoding msg
"embstr"

2,對象

1,字符串對象

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

int:保存的是整數值且該整數可以用long類型表示。

embstr:保存的是字符串值且長度小於等於32字節,使用SDS保存。

raw:保存字符串值且長度大於32字節,使用SDS保存。

embstr和raw區別:
raw會調用兩次內存分配函數分別創建redisObject結構和sdshdr結構;而embstr只調用一次內容分配函數來分配一塊連續的空間,空間依次包含redisObject結構和sdshdr結構。

Redis學習總結

編碼轉換:
int編碼的字符串被修改成不再是整數、embstr編碼的字符串執行任何修改命令都會被轉換爲raw

2,列表對象

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

ziplist:列表對象保存的所有字符串元素長度小於64字節且元素數量小於512個。

linkedlist:列表對象保存的所有字符串元素大於64字節或元素數量大於等於512個。

64和512可以通過配置文件中list-max-ziplist-value和list-max-ziplist-entries修改。

結構圖:

Redis學習總結

Redis學習總結

補充:上兩圖中保存字符串"three"的對象的完×××式爲:

Redis學習總結

3,哈希對象

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

ziplist:哈希對象保存的所有鍵值對的鍵和值的字符串長度都小於64字節且鍵值對數量小於512個。

hashtable:不滿足ziplist中的任一約束時。

64和512可以通過配置文件中list-max-ziplist-value和list-max-ziplist-entries修改。

127.0.0.1:6379> hmset profile name Tom age 25 career Programmer

.結構圖:

Redis學習總結

Redis學習總結

4,集合對象

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

intset:保存的所有元素都是整數值且元素數量不超過512個。

hashtable:不滿足intset中任一約束時。

512可以通過配置文件中set-max-intset-entries修改。

127.0.0.1:6379> sadd Dfruits apple banana cherry

結構圖:

Redis學習總結

Redis學習總結

5,有序集合對象

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

ziplist:保存的所有元素成員的長度都小於64字節且元素數量小於128個。

skiplist:不滿足ziplist中任一約束時。

128和64可以銅牌配置文件中的zset-max-ziplist-entries和zset-max-ziplist-value修改。

127.0.0.1:6379> zadd price 8.5 apple 5.0 banana 6.0 cherry

結構圖:

使用ziplist:

Redis學習總結

Redis學習總結

使用skiplist:

Redis學習總結

其中,zset結構中的zsl爲指向跳躍表的指針,dict爲指向字典的指針。

Redis學習總結

zsl跳躍表按分值從小到大保存了所有集合元素,每個跳躍表節點都是一個集合元素,節點的object屬性保存了元素成員,score屬性保存分值。通過跳躍表,程序可以快速的對集合進行範圍操作,如zrank、zrange命令就是基於跳躍表實現的。

dict字典爲有序集合創建了一個從成員到分值的映射,字典中每個鍵值對保存一個元素,鍵保存元素成員,值保存分值。通過這個字典,程序可以一O(1)的時間複雜度查到給定成員的分值,如zscore命令就是基於此特性。

zsl和dict通過指針共享相同元素的成員和分值。

三,持久化

Redis提供了兩種不同的持久化方法。一個叫做快照,它可以將存在於某一時刻的所有數據都寫入硬盤裏。另一種叫只追加文件,它會在執行寫命令時,將被執行的命令複製到硬盤裏。

1,快照持久化

Redis可以通過創建快照來獲得存儲在內存裏面的數據在某個時間點上的副本。創建快照後,用戶可以對快照進行備份,可以將快照複製到其他服務器從而創建具有相同數據的服務器副本,也可留在本地以便重啓服務器時使用。

根據配置,快照將被寫入dbfilename指定的文件裏,並存儲在dir指定的路徑上。

如果在新的快照創建完畢前,Redis、系統或硬件三者之中任一一個崩潰,Redis都將丟失上一次創建完快照之後的數據。

創建快照的方式:

客戶端發送BGSAVE命令來創建快照。對於支持BGSAVE命令的平臺來說,Redis會調用fork來創建一個子進程,然後由子進程負責將快照寫入硬盤,父進程則繼續處理命令請求。

客戶端發送SAVE命令來創建快照。接到SAVE命令的Redis服務器在快照創建完畢之前不會響應任何其他命令。SAVE命令並不常用,通常只在沒有足夠內存執行BGSAVE時纔會使用。

配置文件中進行了save配置,如 save 60 10000,那麼從Redis上一次創建快照之後開始算起,當60秒內有10000次寫入時,Redis就會自動觸發BGSAVE命令。如果設置多個save選項,當任一一個滿足時,Redis都會觸發BGSAVE。

當Redis通過SHUTDOWN命令接收到關閉服務器的請求時,或者接收到標準TERM信號時,會執行SAVE命令,並在SAVE命令執行完畢後關閉服務器。

當一個Redis服務器接收到另一個Redis服務器發來的SYNC命令時,如果當前Redis服務器還沒有執行BGSAVE命令或並非剛剛執行完BGSAVE命令,那麼當前服務器會執行BGSAVE命令。

2,AOF持久化

AOF持久化會將被執行的寫命令寫到AOF文件的末尾,因此,Redis只要從頭到尾執行一次AOF文件包含的所有寫命令,就可以恢復數據集。

AOF可以通過在配置文件中設置 appendonly yes 選項來打開。

配置 同步頻率:

appendfsync always : 每個寫命令都要同步寫入硬盤,這樣做會嚴重降低Redis的速度。

appendfsync everysec :每秒執行一次同步,顯示地將多個命令寫入硬盤。

appendfsync no :讓操作系統來決定何時進行同步。

AOF存在的問題:

隨着Redis的不斷運行,AOF文件體積也會不斷增長,甚至可能會用完硬盤所有空間。另一個問題,如果AOF文件過大,當Redis重啓後執行所有寫命令將會非常耗時。

AOF重寫:

用戶可以發送BGREWRITEAOF命令來重寫AOF文件,Redis接收到此命令後會fork一個子進程進行AOF文件重寫以使其體積更小。

配置文件中配置auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size 選項來自動執行BGREWRITEAOF。如auto-aof-rewrite-percentage = 100 和 auto-aof-rewrite-min-size = 64mb,並啓用AOF持久化,那麼當AOF文件的體積大於64mb並且AOF文件的體積比上一次重寫之後的體積大了一倍,即100%時,Redis會執行BGREWRITEAOF。

四,多機數據庫

1,主從模式

在關係數據庫中,通常使用一個主服務器向多個從服務器發送更新,並使用從服務器來處理所有度請求。Redis也採用了同樣的方式來實現自己的複製特性。

用戶可以通過執行SLAVEOF命令或者設置slaveof選項,讓一個服務器去複製另一個服務器,我們稱被複制的服務器爲主服務器,而對主服務器進行復制的被稱爲從服務器。

如使用:

127.0.0.1:6379> slaveof 127.0.0.1 6380

那麼服務器 127.0.0.1:6379 將成爲服務器127.0.0.1:6380 的從服務器, 127.0.0.1:6380將成爲主服務器。

1.1 複製功能的實現

Redis的複製功能分爲同步(sync/psync)和命令傳播兩個操作:

同步:同步操作用於將從服務器的數據庫庫狀態更新至主服務器當前所處的狀態。其中sync爲老版本中的,psync從2.8版本開始,用於代替sync命令。

命令傳播:同步完成後,主服務器將自己執行的寫命令發送給從服務器執行。

老闆本複製過程:
Redis學習總結

新老版本主要差別是在斷線重連時的處理方式不同。老版本在斷線重連後需要重新發送sync命令,主服務器在收到命令後將重新執行BGSAVE創建一個完整的RDB文件,如上圖中所示,斷線過程中,主服務器僅多增加了三條數據卻需要主服務器重新執行BGSAVE,這是很不划算的。

新版本複製過程:
Redis學習總結

psync命令具有完整同步和部分重同步兩種模式:

完整同步:用於初次複製情況,其步驟同sync基本一樣,都是通過讓主服務器創建併發送RDB文件,以及向從服務器發送保存在緩衝區裏面的寫命令來進行同步。

部分重同步:用於斷線後重複製情況,當斷線重連後,如條件允許,主服務器僅需將斷線後的寫命令發送給從服務器,而無需主服務器創建完成RDB文件。

1.2 部分重同步的實現

部分重同步以一下三個部分構成:

主服務器的複製偏移量和從服務器的複製偏移量。

主服務器的複製積壓緩衝區。

服務器的運行ID。

1.2.1 複製偏移量

執行復制的雙方都維護了一個複製偏移量。

主服務器每次向從服務器傳播N個字節的數據時,就將自己的複製偏移量加N。

從服務器每次收到主服務器傳播來的N個字節的數據時,就將自己的複製偏移量加N。

通過對比主從服務器的複製偏移量,就可以知道主從服務器是否處於一致狀態。

1.2.2 複製積壓緩衝區

複製積壓緩衝區是由主服務器維護的一個固定長度的先進先出隊列,默認爲1MB。當主服務器進行命令傳播時,它不僅會將命令發送給所有從服務器,還會將命令入隊到複製積壓緩衝區裏,並且複製積壓緩衝區還會爲隊列中的每個字節記錄相應的複製偏移量。

當從服務器重新連上主服務器時,會將自己的複製偏移量offset發送給主服務器,主服務器會根據這個偏移量來決定對從服務器執行何種同步操作:

如果offset偏移量之後的數據還存在於複製積壓緩衝區裏,那麼執行部分重同步操作。

如果offset偏移量之後的數據已經不存在了,那麼久執行完整的重同步操作。

1.2.3 服務器運行ID

每個Redis服務器都有自己的運行ID,在服務器啓動時自動生成,由40個隨機的十六進制字符組成。

當從服務器對主服務器進行初次複製時,主服務器會將自己的ID發送給從服務器,從服務器會將其保存起來。當從服務器斷線重連時,會將這個保存的主服務器ID發送給主服務器。

如果從服務器發送的ID和主服務器自己的ID相同,那麼說明從服務器斷線前複製的主服務器就是當前主服務器,主服務器可以繼續嘗試部分重同步操作。

如果從服務器發送的ID和主服務器自己的ID不同,主服務器將對從服務器執行完整的重同步操作。

1.3 主從鏈

當複製需要通過互聯網進行或者需要在不同的數據中心之間進行的時候,過多的從服務器可能會導致網絡不可用。而Redis的主從服務器並沒有什麼特別的不同,所以從服務器也可以擁有自己的從服務器,像這樣:

Redis學習總結

2,哨兵模式

Sentinel是Redis的高可用解決方案,由一個或多個Sentinel實例組成的Sentinel系統可以監視任意多個主服務器,以及這些主服務器的所有從服務器,當主服務器下線時,自動將下線主服務器下的某個從服務器升級爲新的主服務器。

Sentinel啓動運行主要流程:

1)根據載入的Sentinel配置文件獲取被監視的主服務器信息;

2)創建連向主服務器的網絡連接:對於每個被監視的主服務器,Sentinel會創建兩個連向主服務器的一部網絡連接
--- 一個命令連接,專門用於向主服務器發送命令,並接收回復。
--- 一個是訂閱連接,專門用於訂閱主服務器的sentinel:hello頻道。

Sentinel默認會以每十秒一次的頻率,通過命令連接向被監視的主服務器發送INFO命令,並通過命令回覆獲取主服務器的當前信息,包括主服務器本身的ID及服務器角色和其下的從服務器信息。

3)根據獲得的從服務器信息創建到從服務器的命令連接和訂閱連接。

Sentinel以每十秒一次的頻率向從服務器發送INFO命令,並從回覆中獲得以下信息:包括該從服務器的主服務器的IP、端口,主從服務器的連接狀態,以及從服務器的ID、角色、優先級、複製偏移量並根據這些信息對保存在Sentinel中的從服務器信息進行更新。

4)以每兩秒一次的頻率,通過命令連接向所有被監視的主服務器和從服務器的sentinel:hello頻道發送以下格式的命令:
Redis學習總結

其中以s_開頭的是Sentinel自己的信息;m_開頭的是主服務器的信息,如果發送的目的是主服務器,那麼就是該主服務器的信息,如果目的是從服務器,那麼就是該從服務器所在的主服務器的信息。

Redis學習總結

Redis學習總結

5)接收來自主服務器和從服務器的頻道信息,上面的2,3步分別訂閱了主從服務器的sentinel:hello頻道,並在第4步對該頻道發送了信息,也就是或每個Sentinel既可以向該頻道發送信息也可以接收該頻道的信息,並能從接收到的信息中獲取到其他監視了相同主服務器的Sentinel的信息。

6)根據上部獲得的其他Sentinel的信息與其他Sentinel建立命令連接,最終監視相同主服務器的Sentinel將形成相互連接的網絡。

7)檢測主觀下線狀態,Sentinel默認以每秒一次的頻率向所有與它創建命令連接的實例(包括主從服務器及其他Sentinel)發送PING命令,並通過實例的回覆來判斷實例是否在線。

Sentinel配置文件中的down-after-milliseconds指定了Sentinel判斷實例進入主觀下線的時間長度,如果一個實例在down-after-milliseconds毫秒內連續向Sentinel返回無效回覆,那麼Sentinel判定此實例下線。

8)當Sentinel將一個主服務器主觀下線後,它會想其他同樣監視此主服務器的Sentinel進行詢問。當從其他Sentinel那裏接收到足夠數量的已下線判斷後,Sentinel就會將主服務器判定爲客觀下線並對其進行故障轉移。

9)當一個主服務器被判定爲客觀下線時,監視這個主服務器的Sentinel會進行協商選出一個領頭Sentinel,並由領頭Sentinel執行故障轉移。

10)故障轉移,從已下線主服務器的所有從服務器中選出一個將其轉換爲主服務器;讓其他所有從服務器改爲複製新的主服務器;將下線的主服務器設置爲新主服務器的從服務器,當它重新上線時就會成爲新主服務器的從服務器。

新主服務器選擇依據:
Redis學習總結

3,集羣

Redis集羣是Redis提供的分佈式數據庫方案,集羣通過分片來進行數據共享,並提供複製和故障轉移功能。

1,節點

每個Redis服務器稱之爲一個節點,一個Redis集羣通常由多個節點組成,通過命令:
CLUSTER MEET ip port
將指定節點加入到當前節點所在的集羣中。

2,槽指派

Redis集羣通過分片的方式來保存數據庫中的鍵值對,集羣的整個數據庫被分爲16384個槽,數據庫中的每個鍵都屬於這16394個槽的其中一個,集羣中的每個節點可以處理0個或最多16384個槽。當所有的16394個槽都有節點處理時,集羣處於上線狀態,否則處於下線狀態。

通過向節點發送命令:
CLUSTER ADDSLOTS slot ...
可以將一個或多個槽指派給節點負責。

一個節點除了會記錄自己處理的槽外還會向其他節點發送自己處理的槽並且也會記錄集羣所有槽的指派信息。

3,執行命令

當客戶端向節點發送與數據庫相關的命令時,接收命令的節點會計算出命令要處理的數據庫鍵屬於哪個槽,如果鍵所在的槽正好就指派給了當前節點,那麼節點就直接處理這個命令;如果不是,那麼節點就給客戶端返回一個MOVED錯誤,並將命令轉發給正確的節點。

可以通過命令:
CLUSTER KEYSLOT key
獲取指定鍵屬於哪個槽。

4,重新分片

Redis集羣的重新分片操作可以將任意數量已經指派給某個節點的槽改爲指派給另一個節點,並且相關槽所屬的鍵值對也會被移動到目標節點。

5,複製與故障轉移

Redis中的節點分爲主節點和從節點,主節點用於處理槽,從節點用於複製主節點,並在主節點下線時,代替主節點處理請求成爲新的主節點,其具體步驟爲:

1)從所有從節點中選擇一個成爲新的主節點。

2)新的主節點會撤銷所有對已下線主節點的槽指派,並將這些槽都指派給自己。

3)新的主節點向集羣廣播一條PONG消息,讓其他主節點知道自己接管了下線的主節點並負責處理下線主節點原來的槽。

選舉新的主節點:

1)集羣的配置紀元是一個自增計數器,初始爲0,集羣裏的某個節點開始一次故障轉移操作時,紀元增加1

2)對於每個配置紀元,集羣裏每個負責處理槽的主節點都有一次投票機會,而第一個向主節點要求投票的從節點將會獲得主節點的投票。

3)當從節點發現複製的主節點下線時,它會廣播一條消息,要求所有接收到消息且有投票權(正在負責處理槽)的主節點投票給自己。

4)如果一個從節點收到的投票數大於具有投票權節點總數的一半時,這個從節點就當選爲新的主節點。

5)如果沒有從節點收到足夠多的投票,那麼集羣進入新的紀元,並在此選舉,直到新的主節點被選出。

可以通過命令:
CLUSTER REPLICATE node_id
讓接收命令的節點成爲node_id所指定的節點的從節點。

五,事務

Redis通過multi、exec、watch、discard等命令來實現事務功能。事務提供了一種將多個命令請求打包,然後一次性、按順序地執行多個命令的機制,並且在事務執行期間,服務器不會中斷事務而改去執行其他客戶端的命令請求,它會將事務中的所有命令都執行完畢,然後纔去處理其他客戶端的命令請求。

1,事務的實現

一個事務由multi命令開始,由exec命令將這個事務提交給服務器執行。

1)multi命令可以將執行該命令的客戶端從非事務狀態切換至事務狀態。

2)當一個客戶端處於非事務狀態時,這個客戶端發送的命令會立即被服務器執行;當處於事務狀態時,服務器會根據這個客戶端發來的不同命令而執行不同的操作:

如果客戶端發送的命令爲exec、discard、watch、multi四個命令的其中一個,那麼服務器立即執行這個命令。

如果發送的是其他命令,那麼服務器將這個命令放入事務隊列裏,然後向客戶端返回queued回覆。

3)當一個處於事務狀態的客戶端向服務器發送exec命令時,這個exec命令會被立即執行,服務器會遍歷這個客戶端的事務隊列,執行隊列中保存的所有命令,並將執行結果返回給客戶端。

2,watch命令的實現

watch命令是一個樂觀鎖,它可以在exec命令執行之前,監視任意數量的數據庫鍵,並在exec命令執行時,檢查被監視的鍵是否至少有一個已經被修改過了,若果是的話,服務器將拒絕執行事務,並向客戶端返回返回執行失敗的空回覆。

每個Redis數據庫都保存着一個watched_keys字典,這個字典的鍵是某個被watch命令監視的數據庫鍵,而字典值則是一個鏈表,鏈表中記錄了所有所有監視該鍵的客戶端。通過watched_keys字典,服務器可以清楚的知道哪些數據庫鍵正在被監視,以及哪些客戶端正在監視這些數據庫鍵。

所有對數據庫的修改命令,在執行之後都會對watched_keys進行檢查,查看是否有被監視的鍵被修改,如果有,則會將客戶端的REDIS_DIRTY_CAS標識打開,標識客戶端的事務安全以及被破壞。

當服務器接收到exec命令時,服務器會根據這個客戶端是否打開了REDIS_DIRTY_CAS標識來決定是否執行事務。

參考:《Redis設計與實現》《Redis實戰》

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