redis复习(参考书籍redis设计与实现)
數據結構與對象
簡單動態字符串
Simple Dynamic String
struct sdshdr {//記錄buf數組中已使用字節的數量//等于SDS所保存字符串的長度int len;//記錄buf數組中未使用的字節的數量int free;//字節數組,用于保存字符串char[] buf; }SDS與C字符串的區別
- 如果對SDS修改之后,SDS長度將小于1MB,則程序分配和len屬性同樣大小的未使用空間。
- 如果修改之后,SDS長度大于等于1MB,則分配1MB的未使用空間
當SDS的API需要縮短SDS保存的字符串時,程序并不立即使用內存重分配來回收縮短后多出來的字節,而是使用free屬性將這些字節的數量記錄起來,并等待將來使用
SDS的API都會以二進制的方式來處理buf數組里面的數據,程序不會對其中的數據做任何限制、過濾或者假設,數據在寫入時是怎么樣的,他被讀取時就是什么樣。
鏈表
typedef struct listNode{//前置節點struct listNode* prev;//后置節點struct listNode* next;//節點的值//void的字面意思是“無類型”,void *則為“無類型指針”,void *可以指向任何類型的數據。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;
特性
雙端、無環、帶表頭指針和表尾指針、帶鏈表長度計數器、多態
字典
哈希表
typedef struct dicht {//哈希表數組dicEntry **table;//哈希表大小unsigned long size;//哈希表大小掩碼,用于計算索引值,總是等于size-1unsigned long sizemask;//該哈希表已有節點的數量unsigned long used; } dictht;哈希表節點
typedef struct dictEntry {//鍵void *key;//值union {void *val;uint64_tu64;int64_ts64;}v;//指向下個哈希表節點,形成鏈表struct dictEntry *next; } dictEntry;字典
typedef struct dict {//類型特定函數dictType *type;//私有數據void *privdata;//哈希表dictht ht[2];//rehash索引,當rehash不在進行時,值為-1int trehashidx; }dict;typedef struct dictType {//計算哈希值的函數unsigned int (*hashFunction)(const void *key);//復制鍵的函數void *(*keyDup)(void *privdata, const void *key);//復制值的函數int (*keyCompare)(void *privdata, void *key);//對比鍵的函數void (*valDestructor)(void *privdata, void *key);//銷毀鍵的函數void (*keyDestructor)(void *privdata, void *obj));//銷毀值的函數void (*valDestructor)(void *privdata, void *obj); }dictType;
解決鍵的沖突
和java中的HashMap一樣,不過redis使用的是頭插法
rehash
redis對字典的哈希表執行rehash的步驟如下
哈希表的擴展與收縮
負載因子 = 哈希表已保存節點數量 / 哈希表大小
load_factor = ht[0].used / ht[0].size
當以下條件中的任意一個被滿足時,程序會自動開始對哈希表執行擴展操作
根據BGSAVE指令或者BGREWRITEAOF命令是否正在執行,服務器執行擴展操作所需的負載因子并不相同,這是因為在執行BGSAVE指令或者BGREWRITEAOF命令的過程中,redis需要創建當前服務器進程的子進程而大多數操作系統都采用寫時復制技術來優化子進程的使用效率,所以在子進程存在期間,服務器會提高執行擴展操作所需的負載因子,從而盡可能的避免在子進程存在期間進行哈希表擴展操作,這可以避免不必要的內存寫入操作,最大限度地節約內存
另一方面,當哈希表的負載因子小于0.1時,程序自動開始對哈希表執行收縮操作
漸進式rehash
為了避免rehash對服務器性能造成影響,服務器不是一次性將ht[0]里面的所有鍵值對全部rehahs到ht[1],而是分多次、漸進式地將ht[0]里面的鍵值對慢慢地rehash到ht[1]
以下是哈希表漸進式rehash的詳細步驟
漸進式rehash的好處在于他采取分而治之的方式,將rehash鍵值對所需的計算工作均攤到字典的每個添加、刪除、查找和更新操作上,從而避免了集中式rehahs而帶來的龐大計算量
漸進式rehash執行期間的哈希表操作
因為在進行漸進式rehash的過程中,字典會同時使用ht[0]和ht[1]兩個哈希表,所以在漸進式rehash進行期間,字典的刪除、查找、更新等操作會在兩個哈希表上進行。例如,要在字典里面查找一個鍵的話,程序會現在ht[0]里面進行查找,如果沒找到的話,就會繼續到ht[1]里面進行查找
另外,在漸進式rehash執行期間,新添加到字典的鍵值對一律會被保存到ht[1]里面,而ht[0]則不再進行任何添加操作,這一措施保證了ht[0]包含的鍵值對數量會只減不增,并隨著rehash操作的執行而最終變成空表
跳躍表
skiplist是一種有序數據結構,它通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的
redis只在兩個地方用到了跳躍表,一個是實現有序集合鍵,另一個是在集群節點中用作內部數據結構,除此之外,跳躍表在redis里面沒有其他用途
跳躍表節點
typedef struct zskiplistNode{//層struct zskiplistLevel {//前進指針struct zskiplistNode *forward;//跨度unsigned int span;} level[];//后退指針struct zskiplistNode* backward;//分值double score;//成員對象robj *obj; } zskiplistNode;層
跳躍表節點的level數組可以包含多個元素,每個元素都包含一個指向其他節點的指針,程序可以通過這些層來加快訪問其他節點的速度,一般來說,層的數量越多,訪問其他接地那的速度就越快
每次創建一個新跳躍表及節點的時候,程序都根據冪次定理隨機生成一個介于1和32之間的值作為level數組的大小,這個大小就是層的“高度”
前進指針
每個層都有一個指向表尾方法的前進指針(level[i].forward屬性),用于從表頭向表尾方向訪問節點。
跨度
層的跨度(level[i].span屬性)用于記錄兩個節點之間的距離
- 兩個節點之間的跨度越大,他們相距的就越遠
- 指向null的所有區前進指針的跨度都為0,以為他們沒有連向任何節點
后退指針
節點的后退指針(bacward屬性)用于從表尾向表頭方向訪問節點;跟可以一次跳過多個節點的前進指針不同,因為每個節點只有一個后退指針,所以每次只能后退至前一個節點。
分值和成員
? 節點的分值(score屬性)是一個double類型的浮點數,跳躍表中的所有節點都按分值從小到大來排序
? 節點的成員對象(obj屬性)是一個指針,它指向一個字符串對象,而字符串對象則保存著一個SDS值
? 在同一個跳躍表中,各個節點保存的成員對象必須是唯一的,但是多個節點保存的分支卻可以是相同的。分值相同的節點將按照成員對象在字典中的大小來進行排序,成員對象較小的節點會排在前面(靠近表頭的方向),而成員對象較大的節點則會排在后面(靠近表尾的方向)
跳躍表
僅靠多個跳躍表節點就可以組成一個跳躍表
但通過使用一個zskilist結構來持有這些節點,程序可以更方便地對整個跳躍表進行處理,比如快速訪問跳躍表的表頭節點和表尾節點,或者快速的獲取跳躍表節點的數量等信息
typedef struct zskiplist {//表頭節點和表尾節點struct skiplistNode *header, *tail;//表中節點的數量unsigned long length;//表中層數最大的節點的層數int level; } zskiplist;整數集合
整數集合(intset)是集合鍵的底層實現之一,當一個集合只包含整數值元素,并且這個集合的元素數量不多時,redis就會使用整數集合作為集合鍵的底層實現
typedef struct intset { //編碼方式 uint32_t encoding; //集合包含的元素數量 uint32_t length; //保存元素的數組 int8_t contents[]; } intset; //contents數組是整數集合的底層實現;整數集合的每個元素都是contents數組的一個數組項(item),各個項在數組中按值的大小從小到大有序地排列,并且數組中不包含任何重復項 encoding的取值,決定了contents數組的真正的類型 #define INTSET_ENC_INT16 (sizeof(int16_t)) //16位,2個字節,表示范圍-32,768~32,767 #define INTSET_ENC_INT32 (sizeof(int32_t)) //32位,4個字節,表示范圍-2,147,483,648~2,147,483,647 #define INTSET_ENC_INT64 (sizeof(int64_t)) //64位,8個字節,表示范圍-9,223,372,036,854,775,808~9,223,372,036,854,775,807升級
每當我們要將一個新元素添加到整數集合里面,并且新元素的類型比整數集合現有所有元素的類型都要長時,整數集合需要先進行升級(upgrade),然后才能將新元素添加到整數集合里面
升級整數集合并添加元素共分為三步
升級的好處
整數集合的升級策略有兩個好處,一個是提升整數集合的靈活性,另一個是盡可能的節約內存
降級
整數集合不支持降級操作,一旦對數據進行了升級,編碼就會一直保持升級后的狀態
壓縮列表
壓縮列表(ziplist)是列表鍵和哈希鍵的底層實現之一。當一個列表鍵只包含少量列表項,并且每個列表項要么就是小整數值,要么就是長度比較短的字符串,那么redis就會使用壓縮列表來做列表鍵的底層實現
壓縮列表的構成
typedef struct ziplist { //記錄整個壓縮列表占用的內存的字節數,在對壓縮列表進行內存重分配或者計算zlend的位置時使用 uint32_t zlbytes; //記錄壓縮列表表尾節點距離壓縮列表的起始地址有多少字節 uint32_t zltail; //記錄了壓縮列表包含的節點數量 uint16_t zllen; //壓縮列表包含的各個節點,節點的長度由節點保存的內容決定 entry entryx[]; //特殊值0xFF(255),用于標記壓縮列表的末端 uint8_t zlend;}zllen:當這個屬性的值小于UINT16_MAX的時候,這個屬性的值就是壓縮列表包含節點的數量;當這個值等于UINT16_MAX時,節點的真實數量需要遍歷整個壓縮列表才能計算得出
壓縮列表節點的構成
previous_entry_length
節點的previous_entry_length屬性以字節為單位,記錄了壓縮列表中前一個節點的長度。previous_entry_length屬性的長度可以是1字節或者5字節
- 如果前一字節的長度小于254,那么previous_entry_length屬性的長度為1字節,前一字節的長度就保存在這一個字節里面
- 如果前一個節點的長度大于等于254字節,那么previous_entry_length屬性的長度為5字節;其中屬性的第一字節會被設置為0xFE(254),而之后的四個字節則用于保存前一節點的長度
encoding
節點的encoding屬性記錄了節點的content屬性所保存數據的類型以及長度
- 一字節長、兩字節長、五字節長,值的最高位為00、01、10的是字節數組的編碼:這種編碼表示節點的content屬性保存著字節數組,數組的長度由編碼除去最高兩位之后的其他位記錄
- 一字節長,值的最高位以11開頭的是整數編碼,這種編碼表示節點的content屬性保存著整數值,整數值的類型和長度由編碼除去最高兩位之后的其他位記錄
content
節點的content屬性負責保存節點的值,節點值可以是一個節點數組或者整數,值的類型和長度由節點的encoding屬性決定
連鎖更新
壓縮列表里面要恰好有多個連續的、長度介于250字節至253字節之間的節點,而如果要將一個長度大于等于254字節的新節點new放在這些節點之前,連鎖更新才有可能被引發
對象
對象的類型與編碼
typedef struct redisObject {//類型unsigned type:4;//編碼unsigned encoding:4;//指向底層實現數據結構的指針void *ptr;//... } robj;對象的類型
| REDIS_STRING | 字符串對象 |
| REDIS_LIST | 列表對象 |
| REDIS_HASH | 哈希對象 |
| REDIS_SET | 集合對象 |
| REDIS_ZSET | 有序集合對象 |
類型
對象的type屬性記錄了對象的類型,這個屬性的值可以是表8-1列出的常量的其中一個
對于redis數據庫保存的鍵值來說,鍵總是一個字符串對象,而值則可以是字符串對象、列表對象、哈希對象、集合對象或者有序集合對象的其中一種,因此:
-
當我們稱呼一個數據庫鍵為“字符串鍵”時,我們指的是“這個數據庫鍵所對應的值為字符串對象”
-
當我們稱呼一個鍵為“列表鍵”時,我們指的是“這個數據庫鍵所對應的值為列表對象”
TYPE命令的實現方式也與此類似,當我們對一個數據庫鍵執行TYPE命令時,命令返回的結果為數據庫鍵對應的值對象的類型,而不是鍵對象的類型
編碼和底層實現
對象的ptr指針指向對對象的底層實現數據結構,而這些數據結構由對象的encoding屬性決定
encoding屬性記錄了對象所使用的編碼,也就是說這個對象使用了什么數據結構作為對象的底層實現
對象的編碼
| REDIS_ENCODING_INT | long類型的整數 |
| REDIS_ENCODING_EMBSTR | embstr編碼的簡單動態字符串 |
| REDIS_ENCODING_RAW | 簡單動態字符串 |
| REDIS_ENCODING_HT | 字典 |
| REDIS_ENCODING_LINKEDLIST | 雙端鏈表 |
| REDIS_ENCODING_ZIPLIST | 壓縮列表 |
| REDIS_ENCODING_INTSET | 整數集合 |
| REDIS_ENCODING_SKIPLIST | 跳躍表和字典 |
每種類型的對象都使用了兩種不同的編碼
不同類型和編碼的對象
| REDIS_STRING | REDIS_ENCODING_INT | 使用整數值實現的字符串對象 |
| REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用embstr編碼的簡單動態字符串實現的字符串對象 |
| REDIS_STRING | REDIS_ENCODING_RAW | 使用簡單動態字符串實現的字符串對象 |
| REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用壓縮列表實現的列表對象 |
| REDIS_LIST | REDIS_ENCODING_LINKEDLIST | 使用雙端鏈表實現的隊列對象 |
| REDIS_HASH | REDIS_ENCODING_ZIPLIST | 使用壓縮列表實現的哈希對象 |
| REDIS_HASH | REDIS_ENCODING_HT | 使用字典實現的哈希對象 |
| REDIS_SET | REDIS_ENCODING_INTSET | 使用整數集合實現的集合對象 |
| REDIS_SET | REDIS_ENCODING_HT | 使用字典實現的集合對象 |
| REDIS_ZSET | REDIS_ENCODING_ZIPLIST | 使用壓縮列表實現的有序集合對象 |
使用OBJECT ENCODING命令可以查看一個數據庫鍵的值對象的編碼
字符串對象
字符串對象的拜納姆可以是int、raw、embstr
如果一個字符串對象保存的是整數值,并且這個整數值可以用long類型來表示,那么字符串對象會將整數值保存在字符串對象結構的ptr屬性里,并將字符串對象的編碼設置為int
如果字符串對象保存的時一個字符串值,并且這個字符串值的長度大于32字節,那么字符串對象將使用一個簡單動態字符串來保存這個字符串值,并將對象的編碼設置為raw
如果字符串對象保存的時一個字符串值,并且這個字符串值的長度小于等于32字節,那么字符串對象將使用embstr編碼的方式來保存這個字符串值
- embstr編碼將創建字符串對象所需的內存分配次數從raw編碼的兩次降低為一次
- 釋放embstr編碼的字符串對象只需要調用一次內存釋放函數,而釋放raw編碼的字符串對象需要調用兩次內存釋放函數
- 因為embstr編碼的字符串對象的所有數據都保存在一塊連續的內存里面,所以這種編碼的字符串對象比起raw編碼的字符串兌現能夠更好的利用緩存帶來的優勢
編碼的轉換
列表對象
編碼可以是ziplist或者linkedlist
編碼轉換
當列表對象可以同時滿足以下兩個條件時,列表對象使用ziplist編碼
- 列表對象保存的所有字符串元素的長度都小于64字節
- 列表保存的元素數量小于512個
不能滿足這兩個條件的列表需要使用linkedlist編碼
注意:以上兩個條件的上限值是可以修改的,具體可以看配置文件中關于list-max-ziplist-value 和 list-max-ziplist-entries選項的說明
哈希對象
哈希對象的編碼可以是ziplist或者hashtable
ziplist編碼的哈希對象使用壓縮列表作為底層實現,每當有新的鍵值對要加入到哈希對象時,程序會先將保存了鍵的壓縮列表節點推入到壓縮列表表尾,然后再將保存了值的壓縮列表節點推入到壓縮列表表尾
- 保存了同一鍵值對的兩個節點總是緊挨在一起,保存鍵的節點在前,保存值的節點在后
- 先添加到哈希對象中的鍵值對會被放在壓縮列表的表頭方向,而后來添加到哈希對象中的鍵值對會被放在壓縮列表的表尾方向
編碼轉換
當哈希對象可以同時滿足以下兩個條件時,哈希對象使用ziplist編碼
- 哈希對象保存的所有鍵值對的鍵和值的字符串長度都小于64字節
- 哈希對象保存的鍵值對數量小于512個
不能滿足這兩個條件的哈希對象需要使用hashtable編碼
注意:以上兩個條件的上限值是可以修改的,具體可以看配置文件中關于list-max-ziplist-value 和 list-max-ziplist-entries選項的說明
集合對象
集合對象的編碼可以是intset或者hashtable
intset編碼的集合對象使用證書集合作為底層實現,集合對象包含的所有元素都被保存在整數集合里面
hashtable編碼的集合對象使用字典作為字典作為底層實現,字典的每個鍵都是一個字符串對象,每個字符串對象包含了一個集合元素,而字典的值則全部被設置為NULL
編碼的轉換
當以下兩個條件都滿足時,使用intset編碼
- 集合對象保存的所有元素都是整數值
- 集合對象保存的元素數量不超過512個
不能滿足的需要使用hashtable
第二個條件上限值可以修改,set-max-intset-entries
有序集合對象
可以是ziplist或者skiplist
- ziplist編碼的壓縮列表對象使用壓縮列表作為底層實現,每個集合元素使用兩個緊挨在一起的壓縮列表節點來保存,第一個節點保存元素的成員,第二個節點保存元素的分值
-
skiplist編碼的有序集合使用zset結構作為底層實現
typedef struct zset { zskiplist *zsl; dict *dict;} zset; }zset結構中的zsl跳躍表按照從小到大的順序保存。每個跳躍表節點都保存了一個集合元素,跳躍表節點的object保存了值,score保存了分數,通過這個跳躍表,程序可以對有序集合進行范圍型操作
除此之外,zset結構中dict字典為有序集合創建了一個從成員到分值的映射,字典中的每個鍵值對都保存了一個集合元素;字典的鍵保存了元素的成員,而字典的值則保存了元素的分值。
有序集合每個元素的成員都是一個字符串對象,而每個元素的分值都是一個double類型的浮點數。值得一提的是,雖然zset結構同時使用跳躍表和字典來保存有序集合元素,但這兩種數據結構都會通過指針來共享相同元素的成員和分值,所以同時使用跳躍表和字典來保存集合元素不會產生任何重復元素或者分值,也不會因此而浪費額外的內存
為什么有序集合需要同時使用跳躍表和字典來實現?
為了兼顧范圍查找和定向查找
編碼的轉換
ziplist:
- 有序集合保存的元素數量小于128個
- 有序集合保存的所有元素成員的長度都小于64字節
不滿足就用skiplist
但是可以通過zset-max-ziplist-entries和zset-max-ziplist-value修改其上限
類型檢查與命令多態
類型檢查的實現
為了確保只有指定類型的鍵可以執行某些特定的命令,在執行一個類型特定的命令之前,redis會檢查輸入鍵的類型是否正確,然后再決定是否執行給定的命令
? 類型特定命令所進行的類型檢查是通過redisObject結構的type屬性來實現的
- 在執行一個類型特定命令之前,服務器會先檢查輸入數據庫鍵的值對象是否為執行命令所需的類型,如果是的話,服務器就對鍵執行指定的命令
- 否則,服務器將拒絕執行命令,并向客戶端返回一個類型錯誤
多態命令的實現
? redis除了會根據值對象的類型來判斷鍵是否能夠執行指定命令之外,還會根據值對象的編碼方式,選擇正確的命令實現代碼來執行命令
內存回收
redis在自己的對象系統中構建了一個引用計數器(reference counting)技術實現的內存回收機制,通過這一機制,程序可以通過追蹤對象的引用計數信息,在適當的時候自動釋放對象并進行內存回收
每個對象的引用計數信息由redisObject結構的refcount屬性記錄
typedef struct redisObject {//引用計數int refcount;//... } robj;對象共享
redis會在初始化服務器時,創建一萬個字符串對象,這些對象包含了從0到9999的所有整數值,當服務器需要用到值為0到9999的字符串對象時,服務器就會使用這些共享對象,而不是新創建對象
創建共享字符串對象的數量可以通過修改redis.h/REDIS_SHARE_INTEGERS常量來修改
為什么redis不共享包含字符串的對象
當服務器考慮將一個共享對象設置為鍵的值對象時,程序需要先檢查給定的共享對象和鍵想創建的目標對象是否完全相同,只有在共享對象和目標對象完全相同的情況下,程序才會將共享對象用作鍵的值對象,而一個共享對象保存的值越復雜,驗證共享對象和目標對象是否相同所需的復雜度就會越高,消耗CPU時間也會越多
- 如果共享對象是保存整數值的字符串對象,那么驗證操作的復雜度為O(1)
- 如果共享對象是保存字符串值的字符串對象,那么驗證操作的復雜度為O(N)
- 如果共享對象是多個值(或者對象)的對象,那么驗證操作的復雜度為O(N2)
因此,盡管共享更復雜的對象可以節約更多的內存,但受到CPU時間的限制,redis只對包含整數值的字符串對象進行共享
對象的空轉時長
typedef struct redisObject {//記錄了對象最后一次被命令程序訪問的時間unsigned lru:22;//... } robj;OBJECT IDLETIME命令可以打印出給定鍵的空轉時長,這一個空轉時長就是通過將當前時間減去鍵的值對象的lru時間計算得出的
OBJECT IDLETIME命令的實現是特殊的,這個命令在訪問鍵的值對象時,不會修改值對象的lru屬性
除了可以被OBJECT IDLETIME命令打印出來之外,鍵的空轉時長還有另外一項作用:如果服務器打開了maxmemory選項,并且服務器用于回收內存的算法為volatile-lru或者allkeys-lru,那么當服務器占用的內存數超過了maxmemory選項所設置的上限值時,空轉時間較高的那部分鍵會優先被服務器釋放,從而回收內存
單機數據庫的實現
數據庫
服務器中的數據庫
redis服務器將所欲數據庫都保存在服務器狀態redis.h/redisServer結構的db數組,db數組的每個項都是一個redis.h/redisDb結構,每個redisDb結構代表一個數據庫
struct redisServer {//一個數組,保存著服務器中的所有數據庫redisDb *db;//服務器的數據庫數量int dbnum; } redisClient;dbnum屬性的值由服務器配置的database選項決定,默認情況下,該選項的值為16,所以redis服務器默認會創建16個數據庫
切換數據庫
默認情況下,redis客服端的目標數據庫為0號數據庫,但是客戶端可以通過執行SELECT命令來切換目標數據庫
謹慎處理多數據庫程序
到目前為止,redis仍然沒有可以返回客戶端目標數據庫的命令。雖然redis-cli客戶端會在輸入符旁邊提示當前所使用的目標數據庫[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-7aGlRX61-1628578058177)(H:\Notes\redis\upload\image-20210728145411521.png)]但是如果你在使用其他語言的客戶端中執行reids命令,并且該客戶端沒有像redis-cli那樣一直顯示目標的數據庫的號碼,那么在數次切換數據庫之后,你很可能會忘記自己當前正在使用的是哪個數據庫。當出現這種情況時,為了避免對數據庫進行誤操作,在執行redis命令特別是像FLUSHDB這樣的危險命令之前,最好先執行一個SELECT命令,顯式地切換到指定的數據庫,然后才執行別的命令
數據庫的鍵空間
reids是一個鍵值對數據庫服務器,服務器中的每個數據庫都由一個redis.h/redisDb結構表示,其中,redisDb結構的dict字典保存了數據庫中的所有鍵值對,我們將這個字典稱鍵空間(key space)
typedef struct redisDb {//數據庫鍵空間,保存著數據庫中的所有鍵值對dict *dict;//... } redisDb;鍵空間和用戶所見的數據庫是直接對應的
- 鍵空間的鍵也就是數據庫的鍵,每個鍵都是一個字符串對象
- 鍵空間的值也就是數據庫的值,每個值可以是字符串對象、列表對象等
讀寫鍵空間時的維護操作
- 在讀取一個鍵之后,服務器會根據鍵是否存在來更新服務器的鍵空間命中(hit)次數或鍵空間不命中(miss)次數,這兩個值可以再INFO stats命令的keyspace_hits屬性和keyspace_misses屬性中查看
- 在讀取一個鍵之后,服務會更新鍵的LRU(最后一次使用)時間,這個值可以用于計算鍵的閑置時間
- 如果服務器在讀取一個鍵時發現該鍵已經過期,那么服務會先刪除這個過期鍵,然后才執行余下的其他操作
- 如果有客戶端使用WATCH命令監視了某個鍵,你們服務器在對被監視的鍵進行修改之后,會將這個鍵標記為臟(dirty),從而讓事務程序注意到這個鍵已經被修改
- 服務器每次修改一個鍵之后,都會對臟鍵計數器的值增1,這個計數器會出發服務的持久化以及復制操作
- 如果服務器開啟了數據庫通知功能,那么在對鍵進行修改之后,服務器將按配置發送相應的數據庫通知
設置鍵的生存時間或過期時間
通過EXPIRE命令或者PEXPIRE命令,客戶端可以以秒或者毫秒的精度為數據庫中的某個鍵設置生存時間(Time To Live, TTL),在經過指定的秒數或者毫秒數之后,服務就會自動刪除生存時間為0的鍵
SETEX命令可以在設置一個字符串鍵的同時為鍵設置過期時間,因為這個命令是一個類型限定命令(只能用于字符串鍵)
四個命令
redis有四個不同的命令可以用于設置鍵的生存時間或者過期時間
- EXPIRE命令將key的生存時間設置為ttl秒
- PEXPIRE命令將key的生存時間設為ttl毫秒
- EXPIREAT命令將key的過期時間設置為timestamp所指定的秒數時間戳
- PEXPIREAT命令將key的過期時間設置為timestamp所指定的毫秒數時間戳
雖然有多種不同單位和不同形式的設置命令,但實際上EXPIRE、PEXPIRE、EXPIREAT三個命令都是使用PEXPIREAT命令來實現的;無論客戶端執行的是以上四個命令中的哪一個,經過轉換之后,最終的執行效果都和執行PEXPIREAT命令一樣
保存過期時間
typedef struct redisDb {//過期字典,保存著鍵的過期時間dict *expires;//... } redisDb;移除過期時間
PERSIST就是PEXPOREAT命令的反操作,PERSIST命令在過期字典中查找給定的鍵,并解除鍵和值(過期時間)在過期字典中的關聯
計算并返回剩余生存時間
TTL以秒為單位返回鍵的剩余生存時間,而PTTL命令則以毫秒為單位返回鍵的剩余生存時間
他們兩都是通過計算鍵的過期時間和當前時間之間的差來實現的
過期鍵的判定
通過過期字典,程序可以用以下步驟檢查一個給定鍵是否過期
過期鍵刪除策略
- 定時刪除:在設置鍵的過期時間的同時,創建一個定時器(timer),讓定時器在鍵的過期時間來臨時,立即執行對鍵的刪除操作
- 惰性刪除:放任鍵過期不管,但是每次從鍵空間中獲取鍵時,都檢查取得的鍵是否過期,如果過期的話,就刪除該鍵
- 定期刪除:每隔一段時間,程序就對數據庫進行一次檢查,刪除里面的過期鍵。至于要刪除多少過期鍵,以及要檢查多少個數據庫,則由算法決定
在這三種策略中,第一種和第三種為主動刪除策略,而第二種則為被動刪除策略
定時刪除
對內存是最有好的,但是對CPU時間是最不友好的
創建一個定時器需要用到redis服務器中的時間事件,而當前時間事件的實現方式–無序鏈表,查找一個時間的時間復雜度為O(N)–并不能高效的處理大量時間事件
惰性刪除
對CPU時間最友好,對內存最不友好
如果數據庫中有非常多的過期鍵,而這些過期鍵又恰好沒有被訪問到的話,那么他們也許永遠也不會被刪除(除非用戶手動執行flushdb)
定期刪除
- 定期刪除策略每隔一段時間執行一次刪除過期鍵擦操作,并通過限制刪除操作執行的時長和頻率來減少刪除操作對CPU的影響
- 除此之外,定期刪除過期鍵,定期刪除策略有效的減少了因為過期鍵而帶來的內存浪費
定期刪除策略的難點是確定刪除操作執行的時長和頻率
- 如果刪除執行的太頻繁,或者執行的時間太長,就會退化成定時刪除
- 如果執行的太少,或者時間太短,就會退化成惰性刪除
redis的過期鍵刪除策略
惰性刪除
db.c/expireIfNeeded
- 如果鍵已經過期,這個函數就會將鍵從數據庫中刪除
- 如果輸入鍵未過期,這個函數不做動作
定期刪除
redis.c/activeExpireCycle
- 函數每次運行時,都從一定數量(默認16)的數據庫中取出一定數量(默認20)的隨機鍵進行檢查,并刪除其中的過期鍵
- 全局變量current_db會記錄當前activeExpireCycle函數檢查的進度,并在下一次activeExpireCycle調用時,接著上一次的進度進行處理
- 隨著activeExpireCycle不斷執行,服務器中所有數據庫都會被檢查一遍,這時將current_db=0,開始新一輪的檢查工作
AOF、RDB和復制功能對過期鍵的處理
RDB
生成RDB文件
執行save或者bgsave命令創建一個新的RDB文件時,程序會對數據庫中的鍵進行檢查,已過期的鍵不會被保存到新創建的RDB文件中
載入RDB
- 如果服務器以主服務器模式運行。那么在載入的時候,會對文件中保存的鍵進行檢查,未過期的鍵會被載入到數據庫中,而過期的鍵會被忽略
- 如果是從服務器,字載入文件的時候,文件中保存的所有的鍵,無論是否過期,都會被載入到數據庫中。但是主從同步的時候,從服務器會被清空,所以不會造成影響
AOF
AOF文件寫入
當服務以AOF持久化模式運行時,如果數據庫中的某個鍵已經過期,但它還沒有被惰性刪除或者定期刪除,那么AOF文件不會因為這個過期間而產生任何影響
當過期鍵被惰性刪除或者定期刪除之后,程序會向AOF文件追加(append)一條DEL命令,來顯式記錄該鍵已被刪除
AOF重寫
AOF重寫的過程中,程序會對數據庫中鍵進行檢查,已過期的鍵不會被保存到重寫后的AOF文件中
復制
當服務器運行在復制模式下,從服務的過期鍵刪除動作由主服務器控制
- 主服務器在刪除一個過期鍵之后,會顯式的向所有從服務器發送一個DEL命令,告知從服務器刪除這個過期鍵
- 從服務器在執行客戶端發送的讀命令時,即使碰到過期鍵也不會將過期鍵刪除,而是繼續像處理未過期的鍵一樣來處理過期鍵
- 從服務器只有在接到主服務器發來的DEL命令之后,才會刪除過期鍵
通過主服務器來控制從服務統一的刪除過期鍵,可以保證主從服務器數據的一致性
數據庫通知
數據庫通知是在redis2.8版本新增的功能,這個功能可以讓客戶端通過訂閱給定的頻道或者模式,來獲知數據庫中鍵的變化,以及數據庫中命令的執行情況
#獲取0號數據庫對message這個鍵的所有操作 subscribe _ _keyspace@0_ _:message#獲取0號數據庫所有執行了del命令的鍵 subscribe _ _keyevent@0:del鍵空間通知(key-space notification):某個鍵執行了什么命令
鍵事件通知(key-event notification):某個命令被什么鍵執行了
服務器配置notify-keyspace-events選項決定了服務器所發送通知的類型
| AKE | 發送所有類型的鍵空間通知和鍵事件通知 |
| AK | 發送所有類型的鍵空間通知 |
| AE | 發送所有類型的鍵事件通知 |
| K$ | 只發送和字符串鍵有關的鍵空間通知 |
| E1 | 只發送和列表鍵有關的鍵事件通知 |
RDB持久化
redis是一個鍵值對數據庫服務器,服務器中通常包含著任意個非空數據庫,而每個非空數據庫中又可以包含任意個鍵值對,為了方便起見,我們將服務器中的非空數據庫以及它們的鍵值對統稱為數據庫狀態
RDB文件的創建與載入
創建
有兩個redis命令可以用于生成RDB文件,一個是SAVE,另一個是BGSAVE
save會阻塞redis服務器進程,直到rdb文件創建完畢為止,在服務器進程阻塞期間,服務器不能處理任何命令請求
bgsave會派生出一個子進程,然后由子進程負責創建rdb文件,服務器進程(父進程)繼續處理命令請求
載入
RDB文件的載入工作是在服務器啟動時自動執行的,所以redis并沒有專門用于載入rdb文件的命令,只要redis服務器在啟動時檢測到rdb文件存在,他就會自動載入rdb文件
因為AOF文件的更新頻率通常比RDB文件的更新頻率高,所以:
- 如果服務器開啟了AOF持久化功能,那么服務器會優先使用aof文件來還原數據庫狀態
- 只有在aof持久化功能處于關閉狀態時,服務器才會使用rdb文件來還原數據庫狀態
SAVE
當save命令執行時,redis服務器會被阻塞,所以當save命令正在執行時,客戶端發送的所有命令請求都會被拒絕
只有在服務器執行完save命令的時候,重新開始接受命令請求之后,客服端發送的命令才會被處理
BGSAVE
因為是子進程執行的,所以在子進程創建RDB的過程中,redis服務器仍然可以繼續處理客戶端的命令請求,但是在BGSAVE命令執行期間,服務器處理SAVE、BGSAVE、BGREWRITEAOF三個命令會和平時有所不同
在BGSAVE執行期間
save命令會被拒絕,避免父進程和子進程同時執行兩個rbSave調用,防止產生競爭條件
bgsave命令也會被拒絕,兩個bgsave也會產生競爭條件
bgrewriteaof命令會被延遲到bgsave執行完畢之后執行
但是如果bgrewriteaof命令正在執行,那么客戶端會拒絕bgsave命令
服務器在載入rdb文件期間,會一直處于阻塞狀態,直到載入工作完成為止
自動間隔性保存
保存條件
用戶可以通過指定配置文件或者傳入啟動參數的方式設置save選項,如果用戶沒有主動設置save選項,那么服務器會為save選項設置默認條件:
- save 900 1
- save 300 10
- save 60 10000
接著,服務器會根據save選項所設置的保存條件,設置服務器狀態redisServer結構的saveparams屬性
struct redisServer {//記錄了保存條件的數組struct saveparam *saveparams; } struct savaparam {//秒數time_t seconds;//修改數int changes; }dirty計數器和lastsave屬性
除了saveparams數組之外,服務器狀態還維持著一個dirty計數器,以及一個lastsave屬性
- dirty計數器記錄距離上一次成功執行save/bgsave命令之后,服務器對數據庫狀態進行了多少次修改(cud)
- lastsave是一個unix時間戳,記錄了服務器上一次成功執行save/bgsave命令的時間
當服務器成功執行一個數據庫命令之后,程序就會對dirty計數器進行更新;命令修改了多少次數據庫,dirty計數器的值就增加多少
檢查保存條件是否滿足
redis每個100毫秒就會執行一次serverCron,該函數會檢查save選項所設置的保存條件是否已經滿足,滿足就執行bgsave
RDB文件結構
其中databases包含著0個或任意多個數據庫,以及各個數9據庫中的鍵值對數據
如果只有0號和3號數據庫有數據,rdb文件如下
type可以是以下常量中的一個
key總是一個字符串對象
value的編碼
字符串
編碼可以是int或者raw
如果服務器打開了rdb文件壓縮功能,如果字符串長度大于20字節,就會選擇壓縮再保存
2. 列表
3. 集合
5. 有序集合
INTSET編碼的集合
先將整數結合轉換為字符串對象,然后將這個字符串對象保存到RDB文件里面
ZIPLIST編碼的列表、哈希表、有序集合
將壓縮列表轉換成一個字符串對象,將得到的字符串對象保存到RDB文件中
分析RDB文件
AOF持久化
AOF的實現
當AOF打開時,服務器在執行完一個寫命令之后,會以協議格式將被執行的寫命令追加到服務器狀態的aof_buf緩沖區的末尾
aof文件的寫入與同步
redis服務器進程就是一個事件循環(loop),這個循環中的文件時間負責接受客戶端的命令請求,以及向客戶端發送命令回復,而時間事件則負責執行像serverCron函數這樣需要定時運行的函數
flushAppendOnlyFile函數的行為由服務器配置的appendsync選項的值來決定
| always | 將aof_buf緩沖區中的所有內容寫入并同步到AOF文件 |
| everysec | 將aof_buf緩沖區中的所有內容寫入到AOF文件,如果上次同步AOF文件的時間距離現在超過一秒鐘,那么再次對AOF文件進行同步,并且這個同步操作是由一個線程專門負責執行的 |
| no | 將aof_buf緩沖區中的所有內容寫入到AOF文件,但并不對AOF文件進行同步,何時同步由操作系統來決定 |
文件的載入與還原
AOF重寫
為了解決AOF文件體積膨脹的問題,redis提供了AOF文件重寫(rewrite)功能
因為aof_rewrite函數生成的新AOF文件只包含還原當前數據庫狀態所必須的命令,所以新AOF文件不會浪費任何硬盤空間
后臺重寫
因為aof_rewrite函數會進行大量的寫入操作,所以調用這個函數的線程將被長時間阻塞,但是redis是單線程來處理命令請求的,所以如果由服務器直接調用這個函數,在執行期間,服務器將無法處理客戶端發來的命令請求
所以redis決定將aof重寫程序放到子進程里執行,這樣做可以同時達到兩個目的:
但是,在子進程進行重寫期間,服務器進程還需要繼續處理命令請求,而新的命令可能會對現有的數據庫狀態進行修改,從而使得服務器當前的數據庫狀態和重寫后的aof文件所保存的數據庫狀態不一致
為了解決這個數據不一致的問題,redis服務器設置了一個aof重寫緩沖區,這個緩沖區在服務器創建子進程之后開始使用,當redis服務器執行完一個寫命令之后,他會同時將這個寫命令發送給aof緩沖區和aof重寫緩沖區
這樣可以保證:
當子進程完成重寫工作之后,他會向父進程發送一個信號,父進程在接收到該信號之后,會調用一個信號處理函數,并執行以下工作
事件
文件事件
redis服務器通過套接字與客戶端進行連接,而文件事件就是服務器對套接字操作的抽象。服務器與客戶端的通信會產生相應的文件事件,而服務器則通過監聽并處理這些事件來完成一系列網絡通信操作
時間事件
reids服務器中的一些操作需要在給定的時間點執行,而時間事件就是服務器對這類定時操作的抽象
客戶端
通常情況下,redis只會將那些對數據庫進行了修改的命令寫入到aof文件,并復制到各個從服務器。如果一個命令沒有對數據庫進行任何修改,那么他就會被認為是只讀命令,不會被寫到aof文件,也不會被復制到 從服務
但是pubsub和script load命令是例外,他們雖然沒有修改數據庫,但是這兩個行為帶有副作用。因此服務器需要使用REDIS_FORCE_AOF標志,強制將這個命令寫入AOF文件
服務器
命令請求的執行過程
發送命令請求
讀取命令請求
查找命令
執行預備操作
進行各種檢查
調用命令的實現函數
client->cmd->proc(client);執行后續工作
是否添加一條慢查詢日志,檢查命令執行的時長,是否寫入aof,是否傳給從服務器
將命令回復發送給客戶端
客戶端接收并打印命令回復
初始化服務器
初始化服務器狀態結構
載入配置選項
初始化服務器數據結構
還原數據庫狀態
執行事件循環
多機數據庫實現
復制
舊版復制功能
- 同步操作用于將從服務器的數據庫狀態更新至主服務器當前所處的數據庫狀態
- 命令傳播擦操作則用于在主服務器的數據庫狀態被修改,導致主從服務的數據庫狀態出現不一致時,讓主從服務器的數據庫重新回到一致狀態
同步
命令傳播
主服務器對從服務器執行命令傳播操作,主將造成主從不一致的命令,發送給從,當從執行了相同的命令之后,再次回到主從一致性狀態
舊版的缺陷
主從斷開重連之后,需要再一次同步,浪費了系統資源
新版復制功能的實現
用PSYNC命令代替了SYNC
PSYNC具有完整重同步和部分重同步
- 完整重同步用于處理初次復制情況:完整重同步的執行步驟和SYNC命令的執行步驟基本一樣,他們都是通過主服務創建并發送RDB文件,以及向從服務器發送保存在緩沖區里面的寫命令來進行同步
- 而部分重同步則用于處理斷線后重復制情況:當從服務器在斷線后重新連接主服務器時,如果條件允許,主服務器可以將主從連接斷開期間執行的寫命令發送給從,從只要接受并執行這些寫命令,就可以將數據庫更新至主當前所處的狀態
部分重同步的實現
復制偏移量
- 主每次向從傳播n個字節的數據時,就將自己的復制偏移量的值加上n
- 從每次收到主傳播來的n個字節的數據時,就將自己的復制偏移量的值加上n
復制積壓緩沖區
復制積壓緩沖區是由主維護的一個固定長度先進先出隊列,默認大小為1MB
當主進行命令傳播時,他還會將命令寫入這里面
當從重新連上主,從會通過psync命令將自己的復制偏移量offset發送給主服務器,主服務器會根據這個復制偏移量來決定對從服務器執行何種同步操作
- 如果offset偏移量之后的數據仍然存在于復制積壓緩沖區里面,那么主服務器將對從服務器器執行部分重同步操作
- 如果不存在了,就執行完整重同步操作
根據需要調整復制積壓緩沖區的大小
second * write_size_per_second
second為從服務器斷線后重新連接上主所需的平均時間(s)
write_size_per_second是主平均每秒產生的寫命令數據量(協議格式的寫命令的長度總和)
服務器運行ID
- 每個服務都有自己的運行ID
- 當從對主進行初次復制時,主會將自己運行ID傳給從,從將其保存
- 當從斷開并重新連接上這個主服務器,主服務器可以繼續嘗試執行部分重同步操作
- 如果從保存的id和主相同,那么就嘗試執行部分重同步操作
- 否則進行完整重同步操作
PSYNC命令的實現
從:
- 如果從沒有復制過任何主,或者執行過slaveof no one,那么從在開始一次新的復制時將向主發送PSYNC ? -1命令,請求完整重同步
- 如果已經復制過,則發送PSYNC <runid> <offset>
主:
- 如果主返回 +FULLRESYNC <runid> <offset>回復,那么表示主服務器將與從服務器執行完整重同步操作
- 如果主返回 +CONTINUE回復,那么表示執行部分重同步
- -ERR,表示主服務器的版本低于Redis2.8,他識別不了PSYNC命令,從將向主服務器發送SYNC命令,并與主服務器執行完整同步操作
復制的實現
心跳檢測
在命令傳播階段,從默認會以每秒一次的頻率,向主發送命令
REPLCONF ACK <replication_offset>
其中replication_offset是服務器當前的復制偏移量
主要有三個作用
- 檢測主從的網絡連接狀態
- 輔助實現min-slaves選項
- 檢測命令丟失
檢測主從的網絡連接狀態
主可以通過發送和接受REPLCONF ACK命令來檢查兩者之間的網絡連接是否正常,如果一秒沒收到從的REPLCONF ACK,那么主就知道從出問題了
通過向主發送info relication命令,在列出從服務器列表的lag一欄中,我們可以看到相應從最后一次向主發送REPLCONF ACK距離現在過了多少秒
輔助實現min-slaves配置選項
redis的min-slaves-to-write和min-slaves-max-lag兩個選項可以防止主在不安全的情況下執行寫命令
檢測命令丟失
如果因為網絡故障,主服務器傳播給從服務器的寫命令在半路丟失,那么當從服務器向主服務器發送REPLCONF ACK命令時,主將發現從當前的復制偏移量少于自己,然后會在復制積壓緩沖區里面找到從缺少的數據,并將這些數據重新發送給從
redis2.8版本以前的命令丟失
REPLCONF ACK命令和復制積壓緩沖區都是2.8新增的,2.8以前,即使命令在傳播過程中丟失,主和從都不會注意到,主更不會想從補發丟失的數據
Sentinel
Sentinel(哨崗、哨兵)是redis的高可用(high availability)解決方案:由一個或多個sentinel實例組成的sentinel系統可以監視任意多個主服務器,以這些主服務器進入下線狀態時,自動將下線主服務器屬下的某個從服務器升級為新的主服務器,然后由新的主服務器代替已下線的主服務器繼續處理命令請求
啟動并初始化 sentinel
初始化服務器
不會載入rdb或者aof
將普通redis服務器使用的代碼替換成sentinel專用代碼
普通服務器的命令
sentinel服務器的命令
初始化sentinel狀態
根據給定的配置文件,初始化sentinel的監視主服務器列表
創建連向主服務器的網絡連接
sentinel會創建兩個連向主服務器的異步網絡連接
- 一個是命令連接,這個鏈接專門用于向主服務器發送命令,并接收命令回復
- 另一個是訂閱連接,這個鏈接專門用于訂閱主服務器的_sentinel_:hello頻道
為什么要有兩個連接
被發送的信息不會保存在服務器里面,如果信息發送時,想要接收信息的客戶端不在線或者斷線,那么這個客戶端就會丟失這條信息。因此,為了不丟失_sentinel_:hello的任何信息,seninle必須專門用一個訂閱連接來接收該頻道的信息
另一方面,除了訂閱頻道之外,sentinel還必須向主服務器發送命令,以此來與主服務器進行通信,所以sentinel還必須向主服務器創建命令連接
獲取主服務器信息
sentinel默認會以每十秒一次的頻率,通過命令連接向被監視的主服務器發送info命令,并通過分析info命令的回復來獲取主服務器的當前信息
檢查客觀下線狀態
當sentinle將一個主服務器判斷為主觀下線之后,為了確認這個主是否真的下線了,它會向同樣監視這一主服務器的其他sentinel進行詢問,看他們是否也認為主已經進入下線狀態(可以是主觀下線或者客觀下線)。當sentinel從其他sentinel那里接收到足夠數量的已下線判斷之后,sentinel將會主服務器判定為客觀下線,并對主服務器執行故障轉移操作
發送SENTINEL is-master-down-by-addr命令
SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>接受SENTINEL is-master-down-by-addr命令
會根據其中的ip和端口號,檢查主是否已下線,然后向源sentinel返回一條包含三個參數的Multi Bulk回復
<down_state> <leader_runid> <leader_epoch>接受SENTINEL is-master-down-by-addr命令的回復
統計回復的已下線數量,當這一數量達到配置指定的判斷客觀下線所需的數量時(quorum參數),sentinel會將主服務器實例結構flags屬性的的SRI_O_DOWN標識打開,表示主服務器已經進入客觀下線狀態
選舉領頭sentinel
- 所有在線的sentinel都有被選為領頭的資格
- 每次選舉之后,無論是否成功,所有sentinel的配置紀元(configuration epoch)的值都會自增一次
- 在一個配置紀元里面,所有的sentinel都有一次將某個sentinel設置為領頭的機會,并且局部領頭一旦設置,在這個配置紀元里面就不能再更改
- 每個發現主服務器進入客觀下線的sentinel都會要求其他sentinel將自己設置為局部領頭
- 當一個sentinel向另一個發送SEN…并且命令中的runid不是*符號而是sentinel的運行id時,表示源sentinel都會要求sentinel將自己設置為局部領頭
- 設置局部領頭的規則是先到先得,后面接受到的所有設置要求都會被拒絕
- 收到SENTINEL is-master-down-by-addr后會進行會回復,回復中的leader_runid和leader_epoch分別記錄了目標sentinel的局部領頭sentinel的運行id和配置紀元
- 源在接收到命令回復之后,會檢查leader_epoch是否和自己相同,如果相同,取出leader_runid參數,如果一直,表示目標將源設置為了領頭
- 如果某個sentinel被半數以上的sentinel設置成了局部領頭,那么就會成為領頭
- 一個配置紀元里面只會出現一個領頭
- 如果沒有一個被選舉為領頭,各個sentinel將在一段時間之后再次進行選舉
故障轉移
選出新的主服務器
修改從服務器的復制目標
將舊的主服務變為從服務
集群
redis集群是redis提供的分布式數據庫方案,集群通過分片來進行數據共享,并提供復制和故障轉移功能
節點
一個節點就是一個運行在集群模式西的redis服務器,redis服務器會根據clusster-enabled選項是否為yes來決定是否開啟服務器的集群模式
redisClient結構和clussterLink結構的相同和不同之處
CLUSTER MEET <ip> <port>
收到命令的節點a將與節點b進行握手,以此來確定彼此的存在
之后a會將節點b的信息通過gossip協議傳播給集群中的其他節點,然其他節點也與b握手,最終b會被集群中所有節點認識
槽指派
redis集群通過分片的方式來保存數據庫中的鍵值對,集群的整個數據庫被分為16384個槽,數據庫中的每個鍵都屬于這16384個槽的其中一個,集群中的每個節點可以處理0個或者最多16384個槽
當16384個槽都有節點在處理時,集群處于上線狀態,相反的,有任何一個槽沒有得到處理,那么集群處于下線狀態
#將一個多多個槽(solt)指派(assign)給節點負責 CLUSTER ADDSLOTS <slot> [slot...]集群中每個節點都會將自己的slots數組通過消息發送給集群中的其他節點,并且每個接收到slots數組的節點都會將數組保存到相應節點的clusterNode結構里面。因此,集群中的每個節點都會知道啊數據庫中的16384個槽分別被指派給了集群中的那些節點
但是,如果只將槽指派信息保存在各個節點的clusterNode.slots數組里,會出現一些無法高效解決的問題,而clusterState.slots解決了這些問題
如果你要知道某個槽被指派給了哪個節點,程序需要遍歷clusterState.nodes,時間復雜度為O(N)。但是如果只訪問clusterState.slots[i],僅為O(1)
(用空間換時間)
在集群中執行命令
在對數據庫中的16384個槽都進行指派之后,集群就會進入上線狀態,這時客戶端就可以向集群中的節點發送數據命令了
計算鍵屬于哪個槽
def slot_number(key):
? return CRC16(key) & 16383;
計算鍵key的CRC16校驗和,&16383則計算出一個介于0-16383的整數作為key的槽號
判斷槽是否由當前節點負責處理
MOVED錯誤
MOVED <slot> <ip>:<port>集群模式的redis-cli客戶端在接收到MOVED錯誤時,并不會打印出MOVED錯誤,而是根據MOVED錯誤自動進行節點轉向,并打印出轉向信息
但是單機模式的客戶端會將MOVED打印出來
獨立功能實現
發布與訂閱
redis的分布與訂閱功能由PUBLISH、SUBSCRIBE、PSUBSCRIBE等命令組成
通過執行subscribe,客戶端可以訂閱一個或多個頻道,從而成為這些頻道的訂閱者,每當有其他客戶端向被訂閱的頻道發送消息時,頻道的所有訂閱者都會收到這條消息
事務
事務的實現
事務開始
multi可以將執行該命令的客戶端從非事務狀態切換至事務狀態,這一切換是通過客戶端狀態的flags屬性中打開REDIS_MULTI標識來完成的
命令入隊
如果是exec、discard、watch、multi其中一個,服務器會立即執行這個命令
其他命令則會放到一個事務隊列里面
事務隊列
執行事務
當一個處于事務狀態的客戶端向服務器發送exec命令時,這個exec命令將立即被服務器這行。服務器會遍歷這個客戶端的事務隊列,執行隊列中保存的所以命令,最后將執行命令所得的結果全部返回給客戶端
watch
watch命令是一個樂觀鎖,他可以在exec命令執行之前,監視任意數量的數據庫鍵,并在exec命令執行時,檢查被監視的鍵是否至少有一個已經被修改過了,如果是的話,服務器將拒絕執行事務,并向客戶端發揮代表事務執行失敗的空回復
Lua腳本
排序
二進制位數組
慢查詢日志
監視器
未完待續
總結
以上是生活随笔為你收集整理的redis复习(参考书籍redis设计与实现)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iOS统计项目的代码总行数
- 下一篇: UE4性能优化