人工勢場路徑規劃
title: Redis設計與實現(note)
date: 2020-09-30 14:27:00
categories:
- Redis
tags: - Redis
- 資料庫
chapter2 簡單動態字串
2.1 SDS的定義
struct sdshdr{
// 記錄buf陣列中已使用位元組的數量
// 等於SDS所儲存字串的長度(不含'\0')
int len;
// 記錄buf陣列中未使用位元組的數量
int free;
// 位元組陣列,用於儲存字串
char buf[];
}
2.2 SDS 與 C 字串的區別
常數複雜度獲取字串長度
與C字串不一樣,SDS在被連結時可以通過結構中記錄的len欄位直接獲取長度資訊,防止連線字串溢位時,也可以通過這一欄位直接調整緩衝區長度
減少修改字串時帶來的記憶體重分配次數
to do 國慶期間帶書去旅遊沒有電腦做筆記
chapter8 物件
8.1 物件的型別與編碼
Redis中的每個物件都由一個redisObject結構表示,該結構中和儲存資料有關的三個屬性分別是type屬性、encoding屬性和ptr屬性
typedef struct redisObject{
// 型別
unsigned type:4;
// 編碼
unsigned encoding:4;
// 指向底層實現資料結構指標
void *ptr;
// ...
}robj;
型別
編碼和底層實現
通過encoding屬性來設定物件所使用的編碼,而不是為特定型別的物件關聯一定的編碼,極大地提升了Redis的靈活性和效率,因為Redis可以根據不用的使用場景來為一個物件設定不同的編碼,從而優化物件在某一場景下的效率
8.2 字串物件
如果字串物件儲存的是一個字串值,並且字串值的長度大於39位元組,那麼字串物件將使用簡單動態字串來儲存這個字串值
編碼的轉換
因為Redis沒有為embstr編碼的字串編寫任何的修改程式,所以對embstr編碼的字串物件實際上是隻讀的,當我們對embstr編碼的字串物件執行任何修改命令時,程式會先將物件的編碼從embstr轉換成raw,然後執行修改命令
字串命令的實現
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-7r8xIy69-1609686516233)(https://gitee.com/hsby/img/raw/master/20201008145101.png)]
8.3 列表物件
編碼轉換
當列表物件可以同時滿足以下兩個條件時,列表物件使用ziplist編碼
-
列表物件的所有字串元素的長度都效於64位元組
-
列表物件儲存的元素數量小於512個;
不能滿足這兩個條件的列表物件需要使用linkedlist編碼
8.4 雜湊物件
雜湊物件的編碼可以是ziplist 或者 hashtable
ziplist編碼的雜湊物件
hashtable編碼的雜湊物件
編碼轉換
當雜湊物件可以同時滿足一下兩個條件時,雜湊物件使用ziplist編碼
- 雜湊物件儲存的所有鍵值對的鍵和值的字串長度都小於64位元組
- 雜湊物件儲存的鍵值對數量小於512個
不能滿足這兩個條件的雜湊物件需要使用hashtable編碼
雜湊命令的實現
8.5 集合物件
集合物件的編碼可以是intset 或者 hashtable
intset編碼集合物件
hashtable編碼集合物件
編碼的轉換
當集合物件可以同時滿足一下兩個條件時,物件使用intset編碼:
-
集合物件儲存的所有都是整數值
-
集合物件儲存的元素數量不超過512個
當集合物件可以同時滿足以下兩個條件時,物件使用inteset編碼
8.4 有序集合物件
有序集合的編碼可以時ziplist 或者 skiplist
ziplist編碼的有序集合物件使用壓縮列表作為底層實現,每個集合元素使用兩個緊挨在一起的壓縮列表節點來儲存
壓縮列表內的集合元數按分支從小到大進行排序
編碼的轉換
當有序集合物件可以同時滿足以下兩個條件時,物件使用ziplist編碼
- 有序集合儲存的元素數量小於128個
- 有序集合儲存的所有元素成員的長度都小於64位元組
不能滿足以上兩個條件的有序集合集合將使用skiplist編碼
有序集合命令的實現
8.7 型別檢查與命令多型
多型命令的實現
我們可以將DEL、EXPIRE、TYPE等命令也稱為多型命令,因為無論輸入的鍵是什麼型別,這些命令都可以正確地執行
8.8 記憶體回收
通過引用計數機制實現記憶體回收
typedef struct redisObject{
//...
// 引用計數
int refcount;
//...
}
8.9 物件共享
不共享包含字串的物件,效能消耗高
8.10 物件的空轉時長
chapter9 資料庫
9.1 伺服器中的資料庫
Redis伺服器將所有都儲存在伺服器狀態redis.h/redisServer
結構中
struct redisServer{
//...
// 一個數組,儲存著伺服器中所有資料庫
redisDb *db;
//...
}
struct redisServer{
//...
// 伺服器的資料庫數量
int dbnum;
//...
}
9.2 切換資料庫
客戶端狀態redisClient結構的db屬性記錄了客戶端當前的目標資料庫
typedef struct redisClient{
//...
// 記錄客戶端當前正在使用的資料庫
redisDB *db;
//...
}
9.3 資料庫鍵空間
typedef struct redisDb{
//...
// 資料庫鍵空間,儲存著資料庫中的所有鍵值對
dict *dict;
//...
}
9.4 設定鍵的生存時間
通過EXPIRE命令或者PEXPIRE命令設定一個秒或毫秒的過期時間
通過EXPIREAT或者PEXPIREAT命令設定一個秒或者毫秒的過期時間戳
儲存過期時間
redisDb結構的expires字典儲存了資料庫中所有鍵的過期時間,我們稱這個字典為過期字典
- 過期字典是一個指標,這個指標指向空間中某個鍵物件
- 過期字典的值是一個long long型別的整數,這個整數儲存了鍵所指向的資料庫鍵的過期時間——一個毫秒精度的UNIX時間戳
typedef struct redisDb{
//...
// 過期字典,儲存著鍵的過期時間
dict *expires;
//...
}
移除過期時間
PERSIST命令可以移除一個鍵的過期時間
計算並返回剩餘生存時間
9.7 AOF、FDB 和複製功能對過期鍵的處理
9.8 資料庫通知
SUBSCRIBE __keyspace@0__:message
// 使客戶端進入接收鍵空間通知模式
SUBSCRIBE __keyevent@0__:del
// 使客戶端進入接收鍵事件通知模式
傳送通知
9.9 重點回顧
chapter10 RDB持久化
10.1 RDB檔案的建立和載入
有兩個Redis命令可以用於生成RDB檔案,SAVE和BGSAVE
SAVE阻塞伺服器程序進行RDB檔案的建立,BGSAVE則建立伺服器子程序進行RDB檔案的建立
因為AOF檔案的耿信頻率通常比RDB檔案的更新頻率高,所以
- 如果伺服器開啟了AOF持久化功能,那麼伺服器會優先使用AOF檔案來還原資料庫狀態
- 只有在AOF持久化功能處於關閉狀態時,伺服器才會使用RDB檔案來還原資料庫狀態
10.2 自動間隔性儲存
設定儲存條件
伺服器會為save選項設定預設條件:
save 900 1
save 300 10
save 60 10000
接著伺服器根據save選下昂設定的條件設定伺服器狀態redisServer結構的saveparams屬性:
struct redisServer{
//...
// 記錄了儲存條件的陣列
struct saveparam *saveparams;
//...
};
struct saveparam{
// 秒數
time_t seconds;
// 修改數
int changes;
};
dirty計數器和lastsave屬性
struct redisServer{
//...
// 修改計數器
long long dirty;
// 上一次執行儲存的時間
time_t lastsave;
//...
};
檢查儲存條件是否滿足
Redis的伺服器週期性操作函式serverCron預設每隔100毫秒就會執行一次,該函式用於對正在執行的伺服器進行維護,它的其中一項工作就是檢查save選項所設定的儲存條件是否已經滿足,如果滿足的話就執行BGSAVE命令
10.3 RDB檔案結構
datebases 部分
TYPE取值
value 的編碼
1. 字串物件
2. 列表物件
3. 集合物件
儲存結構和列表物件相似
4. 雜湊表物件
5. 有序集合物件
10.4 分析RDB檔案
包含帶有過期時間的字串鍵的RDB檔案
包含一個集合鍵的RDB檔案
10.5 重點回顧
chapter11 AOF持久化
11.1 AOF持久化的實現
命令追加
當AOF持久化處於開啟狀態時,伺服器執行完一個寫命令之後,會以協議格式將被執行的寫明瞭追加到伺服器狀態的aof_buf緩衝區
struct redisServers{
//...
// AOF緩衝區
sds aof_buf;
//...
}
AOF檔案的寫入與同步
11.2 AOF檔案的載入
11.3 AOF重寫
AOF檔案重寫的實現
AOF後臺重寫
當需要進行AOF重寫時,伺服器程序建立子程序和一個AOF重寫緩衝區,避免與父程序資料庫狀態混淆,在此之後伺服器會將接收到的寫命令同時寫入AOF緩衝區和AOF重寫緩衝區,當子程序將此前狀態重寫到AOF檔案完成後,傳送訊號給伺服器程序,伺服器程序的訊號處理函式將AOF重寫緩衝區的狀態覆蓋到AOF檔案中
chapter12 事件
12.1 檔案事件
Redis基於Reactor模式開發了自己的網路事件處理器:這個處理器被稱為檔案時間處理器:
- 檔案時間處理器使用IO多路複用程式來同時監聽多個套接字,並根據套接字目前執行的任務來為套接字關聯不用的事件處理器
- 當被監聽的套接字準備好執行連線應答(accept)、讀取(read)、寫入(write)、關閉(close)等操作時,與操作相對應的檔案時間就會產生,這是檔案事件處理器就會呼叫套接字之前關聯好的事件處理器來處理這些事件
檔案事件處理器的構成
IO多路複用程式將同時響應的套接字放在一個佇列中,每次向檔案事件分派器分派一個套接字
IO多路複用程式的實現
事件的型別
- 當套接字可讀時,產生AE_READABLE事件
- 當套接字可寫時,產生AE_WRITEABLE事件
- 當同一個套接字,可讀可寫時,先處理AE_READABLE事件,再處理AE_WRITEABLE事件
API
檔案事件的處理器
12.2 時間事件
Redis 的事件事件分為以下兩類:
- 定時事件:讓一段程式在指定的時間之後執行一次。
- 週期性事件:讓一段程式每隔指定時間就執行一次。
一個時間事件主要由以下三個屬性組成:
- id:伺服器為時間事件建立的全域性唯一ID
- when:毫秒精度的UNIX時間戳
- timeProc:時間事件處理器,一個函式
實現
伺服器將所有時間事件都放在一個無序連結串列中,每當時間事件執行器執行時,它就遍歷整個連結串列,查詢所有已到達的時間事件
時間事件應用例項:serverCron函式
它的主要工作包括:
- 更新伺服器的各類統計資訊
- 清理資料庫中的過期鍵值對
- 關閉和清理連線失效的客戶端
- 嘗試進行AOF 或 RDB持久化操作
- 如果伺服器是主伺服器
- 如果處於叢集模式,對叢集進行定期同步和連線測試
12.3 事件的排程與執行
- aeApiPoll函式的最大阻塞時間由到達時間最接近當前時間的時間事件決定,比秒伺服器對時間事件進行頻繁的輪詢,也避免阻塞時間過長
- 因為檔案時間是隨機出現的,如果等待並處理完一次檔案事件之後,仍未有任何時間事件到達,那麼伺服器繼續等待,逐漸逼近時間事件
- 對檔案事件和時間事件的處理都是同步、有序、原子地執行的
12.4 重點回顧
chapter13 客戶端
伺服器為每個客戶端建立相應的redis.h/redisClient結構,這個結構儲存了客戶端當前的狀態資訊
- 客戶端的套接字描述符
- 客戶端的名字
- 客戶端的標誌值
- 只想客戶端正在使用的資料庫的指標,以及該資料庫的號碼
- 客戶端當前要執行的命令、命令的引數、命令引數的個數,以及指向命令實現函式的指標
- 客戶端的輸入緩衝區和輸出緩衝區
- 客戶端的複製狀態資訊,以及進行符直所需的資料結構
- 客戶端執行BRPOP、BLPOP等列表阻塞命令時使用的資料結構
- 客戶端的事務狀態,以及執行WATCH命令時用到的資料結構
- 客戶端執行釋出與訂閱功能時用到的資料結構
- 客戶端的身份驗證標誌
- 客戶端的建立時間,客戶端和伺服器最後一次通訊的時間,以及客戶端的輸出緩衝區大小超出軟性限制的時間
Redis伺服器狀態結構的clients屬性是一個連結串列,這個連結串列儲存了所有與伺服器連線的客戶端的狀態結構
13.1 客戶端屬性
客戶端狀態包含的屬性可以分為兩類:
- 一類是比較通用的屬性,這些屬性很少與特定功能相關,無論客戶端執行的是什麼工作,它們都要用到這些屬性。
- 另外一類是和特定功能相關的屬性,比如操作資料結構是需要用到的db屬性和dictid屬性,執行事務時需要需要用到的mstate屬性,以及執行WATCH命令時需要用到的watched_keys屬性等等
套接字描述符
如果套接字描述符值為-1,說明這是一個偽客戶端,主要用於載入AOF檔案並還原資料庫狀態,或者用於執行Lua指令碼中包含的Redis命令
一般的客戶端描述符值都是大於-1的整數
名字
預設情況下客戶端沒有名字,可以通過CLIENT setname
命令設定,通過CLIENT list
查詢
標誌
客戶端的標誌屬性flags記錄了客戶端的角色(role),以及客戶端目前所處的狀態:
- 在主從伺服器進行復制操作時,主伺服器會成為從伺服器的客戶端,而從伺服器也成為主伺服器的客戶端。REDI_MASTER標誌代表客戶端是一個主伺服器,REDIS_SLAVE標誌表示客戶端代表的是一個從伺服器。
- REDIS_PRE_PSLAVE標誌表示客戶端代表的是一個版本低於Redis2.8 的從伺服器,主伺服器不能使用PSYNC命令與這個從伺服器進行同步。這個標誌只能在REDIS_SLAVE標誌處於開啟狀態時使用。
- REDIS_LUA_CLIENT標識代表客戶端時專門用於處理Lua腳本里麵包含的Redis命令的偽客戶端
- REDIS_MONITOR標誌表示客戶端正在執行MONITOR命令
- REDIS_UNIX_SOCKET標誌表示伺服器使用UNIX套接字連線客戶端
- REDIS_BLOCKED標誌表示客戶端正在被BRPOP、BLPOP等命令阻塞
- REDIS_UNBLOCKED標誌表示客戶端已經從REDIS_BLOCKET標誌所表示的阻塞狀態脫離出來,只能在REDIS_BLOCKED標誌已經開啟時使用
- REDIS_MULTI標誌表示客戶端正在執行事務
- REDIS_DIRTY_CAS標誌表示事務使用WATCH命令監視的資料庫鍵已經被修改,REDIS_DIRTY_EXEC標誌表示事務在命令入隊時出錯,以上兩個標誌都表示事務的安全性已經被破壞
- REDIS_CLOSE_ASAP標誌表示客戶端的輸出緩衝區大小超出了伺服器允許的範圍,伺服器會在下一次執行serverCron函式時關閉這個客戶端,以免伺服器的穩定性收到這個客戶端影響
- REDIS_CLOSE_AFTER_REPLY標誌表示有使用者對這個客戶端執行了CLENT KILL命令,或者客戶端傳送了錯誤的協議內容,伺服器將把客戶端輸出緩衝區的內容傳送,之後關閉客戶端
- REDIS_ASKING 標誌表示客戶端向叢集節點發送了ASKING命令
- REDIS_FORCE_AOF標誌強制伺服器將當前執行的命令寫入到AOF檔案裡面,REDIS_FORCE_PEPL標誌強制主伺服器將當前執行的命令複製給所有從伺服器,執行PUBUB命令會使客戶端開啟REDIS_FORCE_AOF標誌,執行SCRIPT LOAD命令回事客戶端開啟REDIS_FORCE_AOF標誌和REDIS_FORCE_REPL標誌
- 在主從伺服器進行命令傳播期間,從伺服器需要向主伺服器傳送PEPLICATION ACK命令,在傳送之前從伺服器需要開啟主伺服器對應客戶端的REDIS_MASTER_FORCE_REPLY標誌,否則傳送將被拒絕
輸入緩衝區
客戶端狀態的輸入緩衝區用於儲存客戶端傳送的命令請求:
它的大小最大大小不超過1GB,否則伺服器將關閉這個客戶端
命令和命令引數
chapter14 伺服器
14.1 命令請求的執行過程
讀取命令請求
命令執行器(1):查詢命令實現
在命令表(command table)中查詢引數所指定的命令,並將找到的命令儲存到客戶端狀態的cmd屬性裡面
命令執行器(2):執行預備操作
14.2 serverCron函式
更新伺服器時間快取
struct redisServer{
//...
// 儲存了秒級精度的系統當前UNIX時間戳
time_t unixtime;
// 儲存了毫秒級精度的系統當前UNIX時間戳
long long mstime;
// 預設10秒更新一次的時間快取, 用於計算鍵的空轉時間
unsigned lruclock:22;
};
struct redisObject{
//...
// 物件最後一次被命令訪問的時間
unsigned lru:22;
//...
};
更新伺服器每秒執行命令次數
serverCron函式中的trackOperationsPerSecond函式會以每100毫秒一次的頻率執行,這個函式的功能是以抽樣計算的方式,估算並記錄伺服器在最近一秒處理的命令請求數量
struct redisServer{
//...
// 上一次進行抽樣的時間
long long ops_sec_last_sample_time;
// 上一次抽樣時,伺服器已執行命令的數量
long long ops_sec_last_sample_ops;
// REDIS_OPS_SEC_SAMPLES大小(預設值為16)的環形陣列
// 陣列中的每個項都記錄了一次抽樣結果
long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES];
// ops_sec_samples陣列的索引值
// 每次抽樣後將值自增1
// 在值等於16時重置為0
int ops_sec_idx;
//...
};
更新伺服器記憶體峰值記錄
struct redisServer{
//...
// 已使用記憶體峰值
size_t stat_peaak_memory;
//...
};
處理SIGTERM訊號
管理客戶端資源
serverCron函式每次執行都會呼叫clientsCron函式,clientsCron函式會對一定數量的客戶端進行以下兩個檢查:
- 如果客戶端與伺服器之間的連線已經超時,那麼程式釋放這個客戶端
- 如果客戶端在上一次執行命令請求之後,輸入緩衝區的大小超過了一定的長度,那麼程式會釋放客戶端當前的輸入緩衝區,並重新建立一個預設大小的輸入緩衝區
管理資料庫資源
serverCron函式每次執行都會呼叫databasesCron函式,這個函式會對伺服器中的一部分資料庫進行檢查,刪除其中的過期鍵,並在有需要時,對字典進行收縮操作
執行被延遲的BGREWRITEAOF
伺服器的aof_rewrite_scheduled標識記錄了伺服器是否延遲了BGREWRITEAOF命令:
struct redisServer{
//...
//如果值為1, 那麼標識有 BGREWRITEAOF 命令被延遲了
int aof_rewrite_scheduled;
//...
};
檢查持久化操作的執行狀態
伺服器狀態使用rdb_child_pid 屬性和 aof_child_pid 屬性記錄執行BGSAVE命令和 BGREWRITEAOF 命令的子程序的ID
struct redisServer{
//...
// 記錄執行BGSAVE命令的子程序的ID
// 如果伺服器沒有在執行BGSAVE
// 那麼這個屬性的值為-1
pid_t rdb_child_pid;
// 記錄執行BGREWRITEAOF 命令的子程序的ID
// 如果伺服器沒有在執行BGREWRITEAOF
// 那麼這個屬性的值為-1
pid_t aof_child_pid;
//...
};
將AOF緩衝區中的內容寫入AOF檔案
關閉非同步客戶端
增加cronloops計數器的值
struct redisServer{
//...
// serverCron函式的執行次數計數器
int cronloops;
//...
};
14.3 初始化伺服器
void initServerConfig(void) {
int j;
updateCachedTime(1);
// 設定伺服器執行id
getRandomHexChars(server.runid,CONFIG_RUN_ID_SIZE);
// 設定預設伺服器頻率
server.hz = CONFIG_DEFAULT_HZ; /* Initialize it ASAP, even if it may get
updated later after loading the config.
This value may be used before the server
is initialized. */
server.timezone = getTimeZone(); /* Initialized by tzset(). */
// 設定預設配置檔案路徑
server.configfile = NULL;
// 設定伺服器的執行架構
server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
//...
}
載入配置選項
初始化伺服器資料結構
initServerConfig函式主要負責初始化一般屬性
initServer函式主要負責初始化資料結構
除了初始化資料結構之外,initServer還進行了一些非常重要的設定操作,其中包括:
- 為伺服器設定程序訊號處理器
- 建立共享物件:這些物件包含Redis伺服器經常用到的一些值,比如包含“OK”回覆的自負床物件,包含整數1 到 10000 的字串物件等等,伺服器通過重用這些共享物件來避免反覆建立相同的物件
- 開啟伺服器的監聽埠,併為監聽套接字關聯連線應答事件處理器,等待伺服器正式執行時接收客戶端的連線
- 為serverCron函式建立時間事件,等待伺服器正式執行時執行serverCron函式
- 如果AOF持久化功能已經開啟,那麼開啟現有的AOF檔案,如果AOF檔案不存在,那麼建立並開啟一個新的AOF檔案,為AAOF寫入做好準備
- 初始化伺服器的後臺IO模組,為將來的IO操作做好準備
還原資料庫狀態
根據伺服器是否啟用了AOF持久化功能,伺服器載入資料時所使用的目標檔案會有所不同:
- 如果伺服器啟用了AOF持久化功能,那麼伺服器使用AOF檔案來還原資料庫狀態
- 否則使用RDB檔案來還原資料庫狀態
執行事件迴圈
chapter15 複製
15.1 舊版複製功能的實現
同步
命令傳播
舊版複製功能的缺陷
15.3 新版複製功能的實現
Redis2.8開始,使用PSYNC命令替代SYNC命令來執行復制時的同步操作
PSYNC命令具有完整重同步和部分重同步兩種模式:
- 完整重同步與SYNC執行步驟基本相同
- 部分重同步用於處理斷線後重複製情況,當從伺服器在斷線後重新連線伺服器時,如果條件允許,主伺服器可以將主從伺服器連線斷開期間執行的寫命令傳送給從伺服器,從伺服器只要接收執行這些命令,實現同步
15.4 部分重同步的實現
- 主伺服器的複製偏移量和從伺服器的複製偏移量
- 主伺服器的複製積壓緩衝區
- 伺服器的執行ID
複製偏移量
執行復制的雙方——主伺服器和從伺服器會分別維護一直複製偏移量:
- 主伺服器每次向從伺服器傳播N個位元組的資料時,就將自己的複製偏移量的值上+N
- 從伺服器每次收到主伺服器傳播來的N個位元組的資料時,就將自己的複製偏移量的值+N
複製積壓緩衝區
一個向從伺服器傳送資料的佇列,如果傳送過程中連線中斷,重新連線就可以將佇列的內容傳送出去,實現重同步,它的大小可以參考每次斷開時間*伺服器每秒處理的寫入請求數
伺服器執行ID
從伺服器可以通過對主伺服器的執行ID進行驗證,確認連線的伺服器是否為原來的伺服器
15.5 PSYNC命令的實現
15.6 複製的實現
步驟1:設定主伺服器的地址和埠
從伺服器的伺服器狀態中設定:
struct redisServer{
//...
// 主伺服器的地址
char *masterhost;
// 主伺服器的埠
int masterport;
//...
};
步驟2:建立套接字連線
步驟3:傳送PING命令
- 檢查套接字的讀寫狀態
- 通過傳送PING命令可以檢查主伺服器能否正常處理命令請求
- 如果主伺服器返回一個錯誤,那麼表示主伺服器暫時沒辦法處理從伺服器的命令請求,不能繼續執行復制工作的後續步驟
- 如果從伺服器讀取到“PONG”回覆,標識主伺服器可以正常處理從伺服器傳送的命令請求
步驟4:身份驗證
如果從伺服器設定了masterauth選項,則進行身份驗證
步驟5:傳送埠資訊
從伺服器向執行命令REPLCONF listening-port <port-number>
主伺服器傳送埠號
主伺服器在客戶端狀態
struct redisClient{
//...
// 從伺服器的監聽埠號
int slave_listening_port;
//...
};
步驟6:同步
在同步操作執行之後,主從伺服器雙方都是對方的客戶端,它們可以互相向對方傳送命令請求,或者返回回覆
正因為主伺服器成為了從伺服器的客戶端,所以主伺服器才可以通過傳送寫命令來改變從伺服器的資料庫狀態,不僅同步操作需要用到這一點,這也是主伺服器對從伺服器執行命令傳播操作的基礎
步驟7:命令傳播
15.7 心跳檢測
在命令傳播階段,從伺服器預設會以每秒一次的頻率,向主伺服器傳送命令:
REPLCONF ACK<replication_offset>
對於主伺服器有三個作用:
- 檢測主從伺服器的網路連線狀態
- 付諸實現min-slaves選項
- 檢測命令失效
檢測主從伺服器的網路連線狀態
INFO replication
lag值顯示了從伺服器響應的時間,一般這個值在0-1之間
輔助實現min-slaves配置選項
Redis的min-slaces-to-write 和 min-slaces-max-lag 兩個選項可以繁殖主伺服器在不安全的情況下執行寫命令
檢測命令丟失
chapter16 Sentinel
16.1 啟動並初始化Sentinel
初始化伺服器
Sentinel本質上只是執行在特殊模式下的Redis伺服器,啟動第一步就是初始化一個普通的Redis伺服器
使用Sentinel專用程式碼
使用redis.h/REDIS_SERVERPORT常量值作為伺服器埠
使用redis.h/redisCommandTable作為伺服器的命令表
// 伺服器在 sentinel 模式下可執行的命令
struct redisCommand sentinelcmds[] = {
{"ping",pingCommand,1,"",0,NULL,0,0,0,0,0},
{"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0},
{"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0},
{"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
{"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0},
{"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
{"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0},
{"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0},
{"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0}
};
struct redisCommand {
// 命令名字
char *name;
// 實現函式
redisCommandProc *proc;
// 引數個數
int arity;
// 字串表示的 FLAG
char *sflags; /* Flags as string representation, one char per flag. */
// 實際 FLAG
int flags; /* The actual flags, obtained from the 'sflags' field. */
/* Use a function to determine keys arguments in a command line.
* Used for Redis Cluster redirect. */
// 從命令中判斷命令的鍵引數。在 Redis 叢集轉向時使用。
redisGetKeysProc *getkeys_proc;
/* What keys should be loaded in background when calling this command? */
// 指定哪些引數是 key
int firstkey; /* The first argument that's a key (0 = no keys) */
int lastkey; /* The last argument that's a key */
int keystep; /* The step between first and last key */
// 統計資訊
// microseconds 記錄了命令執行耗費的總毫微秒數
// calls 是命令被執行的總次數
long long microseconds, calls;
};
初始化Sentinel狀態
/* Sentinel 的狀態結構 */
struct sentinelState {
// 當前紀元
uint64_t current_epoch; /* Current epoch. */
// 儲存了所有被這個 sentinel 監視的主伺服器
// 字典的鍵是主伺服器的名字
// 字典的值則是一個指向 sentinelRedisInstance 結構的指標
dict *masters; /* Dictionary of master sentinelRedisInstances.
Key is the instance name, value is the
sentinelRedisInstance structure pointer. */
// 是否進入了 TILT 模式?
int tilt; /* Are we in TILT mode? */
// 目前正在執行的指令碼的數量
int running_scripts; /* Number of scripts in execution right now. */
// 進入 TILT 模式的時間
mstime_t tilt_start_time; /* When TITL started. */
// 最後一次執行時間處理器的時間
mstime_t previous_time; /* Last time we ran the time handler. */
// 一個 FIFO 佇列,包含了所有需要執行的使用者指令碼
list *scripts_queue; /* Queue of user scripts to execute. */
} sentinel;
初始化Sentinel狀態的masters屬性
Sentinel狀態中的master字典記錄了所有被Sentinel監視的主伺服器的相關資訊其中:
- 字典的鍵是被監視主伺服器的名字
- 而字典的值是被監視主伺服器對應的sentinel.h/sentinelRedisInstance結構
/* A Sentinel Redis Instance object is monitoring. */
/* 每個被監視的 Redis 例項都會建立一個 sentinelRedisInstance 結構
* 而每個結構的 flags 值會是以下常量的一個或多個的並 */
// 例項是一個主伺服器
#define SRI_MASTER (1<<0)
// 例項是一個從伺服器
#define SRI_SLAVE (1<<1)
// 例項是一個 Sentinel
#define SRI_SENTINEL (1<<2)
// 例項已斷線
#define SRI_DISCONNECTED (1<<3)
// 例項已處於 SDOWN 狀態
#define SRI_S_DOWN (1<<4) /* Subjectively down (no quorum). */
// 例項已處於 ODOWN 狀態
#define SRI_O_DOWN (1<<5) /* Objectively down (confirmed by others). */
// Sentinel 認為主伺服器已下線
#define SRI_MASTER_DOWN (1<<6) /* A Sentinel with this flag set thinks that
its master is down. */
// 正在對主伺服器進行故障遷移
#define SRI_FAILOVER_IN_PROGRESS (1<<7) /* Failover is in progress for
this master. */
// 例項是被選中的新主伺服器(目前仍是從伺服器)
#define SRI_PROMOTED (1<<8) /* Slave selected for promotion. */
// 向從伺服器傳送 SLAVEOF 命令,讓它們轉向複製新主伺服器
#define SRI_RECONF_SENT (1<<9) /* SLAVEOF <newmaster> sent. */
// 從伺服器正在與新主伺服器進行同步
#define SRI_RECONF_INPROG (1<<10) /* Slave synchronization in progress. */
// 從伺服器與新主伺服器同步完畢,開始複製新主伺服器
#define SRI_RECONF_DONE (1<<11) /* Slave synchronized with new master. */
// 對主伺服器強制執行故障遷移操作
#define SRI_FORCE_FAILOVER (1<<12) /* Force failover with master up. */
// 已經對返回 -BUSY 的伺服器傳送 SCRIPT KILL 命令
#define SRI_SCRIPT_KILL_SENT (1<<13) /* SCRIPT KILL already sent on -BUSY */
// Sentinel 會為每個被監視的 Redis 例項建立相應的 sentinelRedisInstance 例項
// (被監視的例項可以是主伺服器、從伺服器、或者其他 Sentinel )
typedef struct sentinelRedisInstance {
// 點陣圖,標識值,記錄了例項的型別,以及該例項的當前狀態
int flags; /* See SRI_... defines */
// 例項的名字
// 主伺服器的名字由使用者在配置檔案中設定
// 從伺服器以及 Sentinel 的名字由 Sentinel 自動設定
// 格式為 ip:port ,例如 "127.0.0.1:26379"
char *name; /* Master name from the point of view of this sentinel. */
// 例項的執行 ID
char *runid; /* run ID of this instance. */
// 配置紀元,用於實現故障轉移
uint64_t config_epoch; /* Configuration epoch. */
// 例項的地址
sentinelAddr *addr; /* Master host. */
// 用於傳送命令的非同步連線
redisAsyncContext *cc; /* Hiredis context for commands. */
// 用於執行 SUBSCRIBE 命令、接收頻道資訊的非同步連線
// 僅在例項為主伺服器時使用
redisAsyncContext *pc; /* Hiredis context for Pub / Sub. */
// 已傳送但尚未回覆的命令數量
int pending_commands; /* Number of commands sent waiting for a reply. */
// cc 連線的建立時間
mstime_t cc_conn_time; /* cc connection time. */
// pc 連線的建立時間
mstime_t pc_conn_time; /* pc connection time. */
// 最後一次從這個例項接收資訊的時間
mstime_t pc_last_activity; /* Last time we received any message. */
// 例項最後一次返回正確的 PING 命令回覆的時間
mstime_t last_avail_time; /* Last time the instance replied to ping with
a reply we consider valid. */
// 例項最後一次傳送 PING 命令的時間
mstime_t last_ping_time; /* Last time a pending ping was sent in the
context of the current command connection
with the instance. 0 if still not sent or
if pong already received. */
// 例項最後一次返回 PING 命令的時間,無論內容正確與否
mstime_t last_pong_time; /* Last time the instance replied to ping,
whatever the reply was. That's used to check
if the link is idle and must be reconnected. */
// 最後一次向頻道傳送問候資訊的時間
// 只在當前例項為 sentinel 時使用
mstime_t last_pub_time; /* Last time we sent hello via Pub/Sub. */
// 最後一次接收到這個 sentinel 發來的問候資訊的時間
// 只在當前例項為 sentinel 時使用
mstime_t last_hello_time; /* Only used if SRI_SENTINEL is set. Last time
we received a hello from this Sentinel
via Pub/Sub. */
// 最後一次回覆 SENTINEL is-master-down-by-addr 命令的時間
// 只在當前例項為 sentinel 時使用
mstime_t last_master_down_reply_time; /* Time of last reply to
SENTINEL is-master-down command. */
// 例項被判斷為 SDOWN 狀態的時間
mstime_t s_down_since_time; /* Subjectively down since time. */
// 例項被判斷為 ODOWN 狀態的時間
mstime_t o_down_since_time; /* Objectively down since time. */
// SENTINEL down-after-milliseconds 選項所設定的值
// 例項無響應多少毫秒之後才會被判斷為主觀下線(subjectively down)
mstime_t down_after_period; /* Consider it down after that period. */
// 從例項獲取 INFO 命令的回覆的時間
mstime_t info_refresh; /* Time at which we received INFO output from it. */
/* Role and the first time we observed it.
* This is useful in order to delay replacing what the instance reports
* with our own configuration. We need to always wait some time in order
* to give a chance to the leader to report the new configuration before
* we do silly things. */
// 例項的角色
int role_reported;
// 角色的更新時間
mstime_t role_reported_time;
// 最後一次從伺服器的主伺服器地址變更的時間
mstime_t slave_conf_change_time; /* Last time slave master addr changed. */
/* Master specific. */
/* 主伺服器例項特有的屬性 -------------------------------------------------------------*/
// 其他同樣監控這個主伺服器的所有 sentinel
dict *sentinels; /* Other sentinels monitoring the same master. */
// 如果這個例項代表的是一個主伺服器
// 那麼這個字典儲存著主伺服器屬下的從伺服器
// 字典的鍵是從伺服器的名字,字典的值是從伺服器對應的 sentinelRedisInstance 結構
dict *slaves; /* Slaves for this master instance. */
// SENTINEL monitor <master-name> <IP> <port> <quorum> 選項中的 quorum 引數
// 判斷這個例項為客觀下線(objectively down)所需的支援投票數量
int quorum; /* Number of sentinels that need to agree on failure. */
// SENTINEL parallel-syncs <master-name> <number> 選項的值
// 在執行故障轉移操作時,可以同時對新的主伺服器進行同步的從伺服器數量
int parallel_syncs; /* How many slaves to reconfigure at same time. */
// 連線主伺服器和從伺服器所需的密碼
char *auth_pass; /* Password to use for AUTH against master & slaves. */
/* Slave specific. */
/* 從伺服器例項特有的屬性 -------------------------------------------------------------*/
// 主從伺服器連線斷開的時間
mstime_t master_link_down_time; /* Slave replication link down time. */
// 從伺服器優先順序
int slave_priority; /* Slave priority according to its INFO output. */
// 執行故障轉移操作時,從伺服器傳送 SLAVEOF <new-master> 命令的時間
mstime_t slave_reconf_sent_time; /* Time at which we sent SLAVE OF <new> */
// 主伺服器的例項(在本例項為從伺服器時使用)
struct sentinelRedisInstance *master; /* Master instance if it's slave. */
// INFO 命令的回覆中記錄的主伺服器 IP
char *slave_master_host; /* Master host as reported by INFO */
// INFO 命令的回覆中記錄的主伺服器埠號
int slave_master_port; /* Master port as reported by INFO */
// INFO 命令的回覆中記錄的主從伺服器連線狀態
int slave_master_link_status; /* Master link status as reported by INFO */
// 從伺服器的複製偏移量
unsigned long long slave_repl_offset; /* Slave replication offset. */
/* Failover */
/* 故障轉移相關屬性 -------------------------------------------------------------------*/
// 如果這是一個主伺服器例項,那麼 leader 將是負責進行故障轉移的 Sentinel 的執行 ID 。
// 如果這是一個 Sentinel 例項,那麼 leader 就是被選舉出來的領頭 Sentinel 。
// 這個域只在 Sentinel 例項的 flags 屬性的 SRI_MASTER_DOWN 標誌處於開啟狀態時才有效。
char *leader; /* If this is a master instance, this is the runid of
the Sentinel that should perform the failover. If
this is a Sentinel, this is the runid of the Sentinel
that this Sentinel voted as leader. */
// 領頭的紀元
uint64_t leader_epoch; /* Epoch of the 'leader' field. */
// 當前執行中的故障轉移的紀元
uint64_t failover_epoch; /* Epoch of the currently started failover. */
// 故障轉移操作的當前狀態
int failover_state; /* See SENTINEL_FAILOVER_STATE_* defines. */
// 狀態改變的時間
mstime_t failover_state_change_time;
// 最後一次進行故障遷移的時間
mstime_t failover_start_time; /* Last failover attempt start time. */
// SENTINEL failover-timeout <master-name> <ms> 選項的值
// 重新整理故障遷移狀態的最大時限
mstime_t failover_timeout; /* Max time to refresh failover state. */
mstime_t failover_delay_logged; /* For what failover_start_time value we
logged the failover delay. */
// 指向被提升為新主伺服器的從伺服器的指標
struct sentinelRedisInstance *promoted_slave; /* Promoted slave instance. */
/* Scripts executed to notify admin or reconfigure clients: when they
* are set to NULL no script is executed. */
// 一個檔案路徑,儲存著 WARNING 級別的事件發生時執行的,
// 用於通知管理員的指令碼的地址
char *notification_script;
// 一個檔案路徑,儲存著故障轉移執行之前、之後、或者被中止時,
// 需要執行的指令碼的地址
char *client_reconfig_script;
} sentinelRedisInstance;
16.2 獲取伺服器資訊
Sentinel預設會以每十秒一次的頻率,通過命令連線向被監視的主伺服器傳送INFO命令,並通過分析INFO命令的回覆來獲取主伺服器的當前資訊
通過分析主伺服器回覆的資訊,填充主伺服器例項結構中的slaves字典,而字典的鍵為從伺服器的名字(addr:port), 值為從伺服器的例項結構,如果從伺服器的值已經存在,那麼更新從伺服器例項結構
16.3 獲取從伺服器資訊
在建立命令連線之後,Sentinel預設情況下會以每十秒一次的頻率通過命令連線向從伺服器傳送INFO命令,並獲得類似以下內容的回覆:
16.4 向主伺服器和從伺服器傳送資訊
在預設情況下,Sentinel會以每2秒一次的頻率,通過命令連線向所有被監視的主伺服器和從伺服器傳送以下格式的命令:
PUBLISH __sentinel__:hello "<s_ip>, <s_port>, <s_runid>, <s_epoch>, <m_name>, <m_ip>, <m_prot>, <m_epoch>"
int sentinelSendHello(sentinelRedisInstance *ri) {
char ip[REDIS_IP_STR_LEN];
char payload[REDIS_IP_STR_LEN+1024];
int retval;
// 如果例項是主伺服器,那麼使用此例項的資訊
// 如果例項是從伺服器,那麼使用這個從伺服器的主伺服器的資訊
sentinelRedisInstance *master = (ri->flags & SRI_MASTER) ? ri : ri->master;
// 獲取地址資訊
sentinelAddr *master_addr = sentinelGetCurrentMasterAddress(master);
/* Try to obtain our own IP address. */
// 獲取例項自身的地址
if (anetSockName(ri->cc->c.fd,ip,sizeof(ip),NULL) == -1) return REDIS_ERR;
if (ri->flags & SRI_DISCONNECTED) return REDIS_ERR;
/* Format and send the Hello message. */
// 格式化資訊
snprintf(payload,sizeof(payload),
"%s,%d,%s,%llu," /* Info about this sentinel. */
"%s,%s,%d,%llu", /* Info about current master. */
ip, server.port, server.runid,
(unsigned long long) sentinel.current_epoch,
/* --- */
master->name,master_addr->ip,master_addr->port,
(unsigned long long) master->config_epoch);
// 傳送資訊
retval = redisAsyncCommand(ri->cc,
sentinelPublishReplyCallback, NULL, "PUBLISH %s %s",
SENTINEL_HELLO_CHANNEL,payload);
if (retval != REDIS_OK) return REDIS_ERR;
ri->pending_commands++;
return REDIS_OK;
}
16.5 接收來自主伺服器和從伺服器的頻道訊息
當一個Sentinel從__sentinel__:hello
頻道收到一條資訊時,Sentinel會對這條資訊進行分析,提取出資訊中的八個引數,根據這些引數對主伺服器的例項結構進行更新
更新sentinals字典
chapter17 叢集
17.1 節點
啟動節點
Redis伺服器啟動時會根據cluster-enabled配置選項是否為yes來決定是否開啟伺服器的叢集模式
節點會繼續使用redisServer結構來儲存伺服器的狀態,使用redisClient結構來儲存客戶端的狀態,至於那些叢集模式下才會用到的資料結構,節點將它們儲存到了cluster.h/clusterNode結構、cluster.h/clusterLink結構,以及cluster.h/clusterState結構裡面
// 節點狀態
struct clusterNode {
// 建立節點的時間
mstime_t ctime; /* Node object creation time. */
// 節點的名字,由 40 個十六進位制字元組成
// 例如 68eef66df23420a5862208ef5b1a7005b806f2ff
char name[REDIS_CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */
// 節點標識
// 使用各種不同的標識值記錄節點的角色(比如主節點或者從節點),
// 以及節點目前所處的狀態(比如線上或者下線)。
int flags; /* REDIS_NODE_... */
// 節點當前的配置紀元,用於實現故障轉移
uint64_t configEpoch; /* Last configEpoch observed for this node */
// 由這個節點負責處理的槽
// 一共有 REDIS_CLUSTER_SLOTS / 8 個位元組長
// 每個位元組的每個位記錄了一個槽的儲存狀態
// 位的值為 1 表示槽正由本節點處理,值為 0 則表示槽並非本節點處理
// 比如 slots[0] 的第一個位儲存了槽 0 的儲存情況
// slots[0] 的第二個位儲存了槽 1 的儲存情況,以此類推
unsigned char slots[REDIS_CLUSTER_SLOTS/8]; /* slots handled by this node */
// 該節點負責處理的槽數量
int numslots; /* Number of slots handled by this node */
// 如果本節點是主節點,那麼用這個屬性記錄從節點的數量
int numslaves; /* Number of slave nodes, if this is a master */
// 指標陣列,指向各個從節點
struct clusterNode **slaves; /* pointers to slave nodes */
// 如果這是一個從節點,那麼指向主節點
struct clusterNode *slaveof; /* pointer to the master node */
// 最後一次傳送 PING 命令的時間
mstime_t ping_sent; /* Unix time we sent latest ping */
// 最後一次接收 PONG 回覆的時間戳
mstime_t pong_received; /* Unix time we received the pong */
// 最後一次被設定為 FAIL 狀態的時間
mstime_t fail_time; /* Unix time when FAIL flag was set */
// 最後一次給某個從節點投票的時間
mstime_t voted_time; /* Last time we voted for a slave of this master */
// 最後一次從這個節點接收到複製偏移量的時間
mstime_t repl_offset_time; /* Unix time we received offset for this node */
// 這個節點的複製偏移量
long long repl_offset; /* Last known repl offset for this node. */
// 節點的 IP 地址
char ip[REDIS_IP_STR_LEN]; /* Latest known IP address of this node */
// 節點的埠號
int port; /* Latest known port of this node */
// 儲存連線節點所需的有關資訊
clusterLink *link; /* TCP/IP link with this node */
// 一個連結串列,記錄了所有其他節點對該節點的下線報告
list *fail_reports; /* List of nodes signaling this as failing */
};
typedef struct clusterNode clusterNode;
/* clusterLink encapsulates everything needed to talk with a remote node. */
// clusterLink 包含了與其他節點進行通訊所需的全部資訊
typedef struct clusterLink {
// 連線的建立時間
mstime_t ctime; /* Link creation time */
// TCP 套接字描述符
int fd; /* TCP socket file descriptor */
// 輸出緩衝區,儲存著等待發送給其他節點的訊息(message)。
sds sndbuf; /* Packet send buffer */
// 輸入緩衝區,儲存著從其他節點接收到的訊息。
sds rcvbuf; /* Packet reception buffer */
// 與這個連線相關聯的節點,如果沒有的話就為 NULL
struct clusterNode *node; /* Node related to this link if any, or NULL */
} clusterLink;
// 叢集狀態,每個節點都儲存著一個這樣的狀態,記錄了它們眼中的叢集的樣子。
// 另外,雖然這個結構主要用於記錄叢集的屬性,但是為了節約資源,
// 有些與節點有關的屬性,比如 slots_to_keys 、 failover_auth_count
// 也被放到了這個結構裡面。
typedef struct clusterState {
// 指向當前節點的指標
clusterNode *myself; /* This node */
// 叢集當前的配置紀元,用於實現故障轉移
uint64_t currentEpoch;
// 叢集當前的狀態:是線上還是下線
int state; /* REDIS_CLUSTER_OK, REDIS_CLUSTER_FAIL, ... */
// 叢集中至少處理著一個槽的節點的數量。
int size; /* Num of master nodes with at least one slot */
// 叢集節點名單(包括 myself 節點)
// 字典的鍵為節點的名字,字典的值為 clusterNode 結構
dict *nodes; /* Hash table of name -> clusterNode structures */
// 節點黑名單,用於 CLUSTER FORGET 命令
// 防止被 FORGET 的命令重新被新增到叢集裡面
// (不過現在似乎沒有在使用的樣子,已廢棄?還是尚未實現?)
dict *nodes_black_list; /* Nodes we don't re-add for a few seconds. */
// 記錄要從當前節點遷移到目標節點的槽,以及遷移的目標節點
// migrating_slots_to[i] = NULL 表示槽 i 未被遷移
// migrating_slots_to[i] = clusterNode_A 表示槽 i 要從本節點遷移至節點 A
clusterNode *migrating_slots_to[REDIS_CLUSTER_SLOTS];
// 記錄要從源節點遷移到本節點的槽,以及進行遷移的源節點
// importing_slots_from[i] = NULL 表示槽 i 未進行匯入
// importing_slots_from[i] = clusterNode_A 表示正從節點 A 中匯入槽 i
clusterNode *importing_slots_from[REDIS_CLUSTER_SLOTS];
// 負責處理各個槽的節點
// 例如 slots[i] = clusterNode_A 表示槽 i 由節點 A 處理
clusterNode *slots[REDIS_CLUSTER_SLOTS];
// 跳躍表,表中以槽作為分值,鍵作為成員,對槽進行有序排序
// 當需要對某些槽進行區間(range)操作時,這個跳躍表可以提供方便
// 具體操作定義在 db.c 裡面
zskiplist *slots_to_keys;
/* The following fields are used to take the slave state on elections. */
// 以下這些域被用於進行故障轉移選舉
// 上次執行選舉或者下次執行選舉的時間
mstime_t failover_auth_time; /* Time of previous or next election. */
// 節點獲得的投票數量
int failover_auth_count; /* Number of votes received so far. */
// 如果值為 1 ,表示本節點已經向其他節點發送了投票請求
int failover_auth_sent; /* True if we already asked for votes. */
int failover_auth_rank; /* This slave rank for current auth request. */
uint64_t failover_auth_epoch; /* Epoch of the current election. */
/* Manual failover state in common. */
/* 共用的手動故障轉移狀態 */
// 手動故障轉移執行的時間限制
mstime_t mf_end; /* Manual failover time limit (ms unixtime).
It is zero if there is no MF in progress. */
/* Manual failover state of master. */
/* 主伺服器的手動故障轉移狀態 */
clusterNode *mf_slave; /* Slave performing the manual failover. */
/* Manual failover state of slave. */
/* 從伺服器的手動故障轉移狀態 */
long long mf_master_offset; /* Master offset the slave needs to start MF
or zero if stil not received. */
// 指示手動故障轉移是否可以開始的標誌值
// 值為非 0 時表示各個主伺服器可以開始投票
int mf_can_start; /* If non-zero signal that the manual failover
can start requesting masters vote. */
/* The followign fields are uesd by masters to take state on elections. */
/* 以下這些域由主伺服器使用,用於記錄選舉時的狀態 */
// 叢集最後一次進行投票的紀元
uint64_t lastVoteEpoch; /* Epoch of the last vote granted. */
// 在進入下個事件迴圈之前要做的事情,以各個 flag 來記錄
int todo_before_sleep; /* Things to do in clusterBeforeSleep(). */
// 通過 cluster 連線傳送的訊息數量
long long stats_bus_messages_sent; /* Num of msg sent via cluster bus. */
// 通過 cluster 接收到的訊息數量
long long stats_bus_messages_received; /* Num of msg rcvd via cluster bus.*/
} clusterState;
CLUSTER MEET 命令的實現
17.2 槽指派
Redis叢集通過分片的方式來儲存資料庫中的鍵值對:叢集的整個資料庫被分為16384個槽(slot)
資料庫中的每個鍵都屬於這16384個槽的其中一個,叢集中的每個節點可以處理0個或最多16384個槽
當資料庫中的16384個槽都有節點在處理時,叢集處於上線狀態(ok);相反,如果資料庫中有任何一個槽沒有得到處理,那麼叢集處於下線狀態(fail)
記錄節點的指派資訊
struct clusterNode{
//...
unsigned char slots[16384/8];// 用於刻畫節點儲存狀態的點陣圖,一共16384個位
int numclots;
//...
};
傳播節點的槽指派資訊
記錄叢集中所有槽的指派資訊
struct clusterState{
//...
clusterNode *slots[16384];
//...
};
CLUSTER ADDSLOTS命令的實現
17.3 在叢集中執行命令
計算鍵屬於哪個槽
def slot_number(key):
return CRC16(key) : 16383
驗證clusterState.clots[slot_number(key)]是否等於clusterState.myself
- 如果相等,說明該槽點由本節點負責,直接執行key對應命令
- 如果不相等,取出clusterState.clots[slot_number(key)]的clusterNode結構中的ip和port, 向客戶端返回MOVED <slot> <ip> <port>錯誤,指引節點指向正在負責處理key的節點
MOVED 錯誤
一個叢集客戶端通常會與叢集中的多個節點建立套接字,而所謂的節點轉向實際上就是換一個套接字來發送命令
節點資料庫的實現
typedef struct clusterState{
//...
// 跳躍表的分值為槽點值,跳躍表的鍵為鍵值對的鍵
zskiplist *slots_to_keys;
//...
}
17.4 重新分片
17.5 ASK錯誤
17.6 複製與故障轉移
設定從節點
向一個節點發送命令CLUSTER REPLICATE <note_id>
struct clusterNode{
//...
// 如果這是一個從節點,那麼指向主節點
struct clusterNode *slaveof;
//...
};
故障檢測
故障轉移
選舉新的主節點
17.7 訊息
- MEET訊息,傳送者接到客戶端傳送的CLUSTER MEET命令時,傳送者會向接收者傳送MEET訊息,請求接收者加入傳送者當前的叢集
- PING訊息,叢集的每個節點每隔一秒就會從已知節點列表選出5個節點,然後對這5個節點最長時間沒有傳送過PING訊息的節點發送PING訊息,對距離上次收到PONG訊息時間超過cluster-node-timeout選項設定時長的一半的節點也會發送PING訊息
- PONG訊息,向傳送者確認這條MEET訊息或者PING訊息已到達,接收者會向傳送者返回一條PONG訊息,一個節點可以通過傳送PONG訊息通知其它節點更新對本節點的認知
- FALL訊息,當一個節點A判斷另一個主節點B已經進入FALL狀態時,節點A會向叢集廣播一條關於節點B的FALL訊息,所有收到這條訊息的節點都會立即將節點B標記為已下線
- PUBLISH訊息:當節點接收到一個PUBLISH命令時,節點會執行這個命令,並向叢集廣播一條PUBLISH訊息,所有接收到這條PUBLISH訊息的節點都會執行相同的PUBLISH命令
訊息頭
// 用來表示叢集訊息的結構(訊息頭,header)
typedef struct {
char sig[4]; /* Siganture "RCmb" (Redis Cluster message bus). */
// 訊息的長度(包括這個訊息頭的長度和訊息正文的長度)
uint32_t totlen; /* Total length of this message */
uint16_t ver; /* Protocol version, currently set to 0. */
uint16_t notused0; /* 2 bytes not used. */
// 訊息的型別
uint16_t type; /* Message type */
// 訊息正文包含的節點資訊數量
// 只在傳送 MEET 、 PING 和 PONG 這三種 Gossip 協議訊息時使用
uint16_t count; /* Only used for some kind of messages. */
// 訊息傳送者的配置紀元
uint64_t currentEpoch; /* The epoch accordingly to the sending node. */
// 如果訊息傳送者是一個主節點,那麼這裡記錄的是訊息傳送者的配置紀元
// 如果訊息傳送者是一個從節點,那麼這裡記錄的是訊息傳送者正在複製的主節點的配置紀元
uint64_t configEpoch; /* The config epoch if it's a master, or the last
epoch advertised by its master if it is a
slave. */
// 節點的複製偏移量
uint64_t offset; /* Master replication offset if node is a master or
processed replication offset if node is a slave. */
// 訊息傳送者的名字(ID)
char sender[REDIS_CLUSTER_NAMELEN]; /* Name of the sender node */
// 訊息傳送者目前的槽指派資訊
unsigned char myslots[REDIS_CLUSTER_SLOTS/8];
// 如果訊息傳送者是一個從節點,那麼這裡記錄的是訊息傳送者正在複製的主節點的名字
// 如果訊息傳送者是一個主節點,那麼這裡記錄的是 REDIS_NODE_NULL_NAME
// (一個 40 位元組長,值全為 0 的位元組陣列)
char slaveof[REDIS_CLUSTER_NAMELEN];
char notused1[32]; /* 32 bytes reserved for future usage. */
// 訊息傳送者的埠號
uint16_t port; /* Sender TCP base port */
// 訊息傳送者的標識值
uint16_t flags; /* Sender node flags */
// 訊息傳送者所處叢集的狀態
unsigned char state; /* Cluster state from the POV of the sender */
// 訊息標誌
unsigned char mflags[3]; /* Message flags: CLUSTERMSG_FLAG[012]_... */
// 訊息的正文(或者說,內容)
union clusterMsgData data;
} clusterMsg;
MEET、PING、PONG 訊息的實現
union clusterMsgData {
/* PING, MEET and PONG */
struct {
/* Array of N clusterMsgDataGossip structures */
// 每條訊息都包含兩個 clusterMsgDataGossip 結構
clusterMsgDataGossip gossip[1];
} ping;
/* FAIL */
struct {
clusterMsgDataFail about;
} fail;
/* PUBLISH */
struct {
clusterMsgDataPublish msg;
} publish;
/* UPDATE */
struct {
clusterMsgDataUpdate nodecfg;
} update;
};
FAIL訊息的實現
typedef struct {
// 下線節點的名字
char nodename[REDIS_CLUSTER_NAMELEN];
} clusterMsgDataFail;
叢集中的節點通過傳送訊息來將一個節點標記為下線的過程。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-TaV854nv-1609686516341)(http://1e-gallery.redisbook.com/_images/graphviz-74e2b6c26627aedbdd10295cfddd6bf6ee29d9ac.png)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-pbb9OOcH-1609686516362)(http://1e-gallery.redisbook.com/_images/graphviz-e2e030e0a517bddc4c8c542b6876efeed2469941.png)]
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-7wTPrYOB-1609686516363)(http://1e-gallery.redisbook.com/_images/graphviz-c4408a7984bbcfdb233e87165044e609a7846ae2.png)]
PUBLISH 訊息的實現
當客戶端向叢集中的某個節點發送命令:
PUBLISH <channel> <message>
typedef struct {
// 頻道名長度
uint32_t channel_len;
// 訊息長度
uint32_t message_len;
// 訊息內容,格式為 頻道名+訊息
// bulk_data[0:channel_len-1] 為頻道名
// bulk_data[channel_len:channel_len+message_len-1] 為訊息
unsigned char bulk_data[8]; /* defined as 8 just for alignment concerns. */
} clusterMsgDataPublish;
17.8 重點回顧
chapter18 釋出與訂閱
客戶端訂閱頻道。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-FXVdjJVP-1609686516367)(http://1e-gallery.redisbook.com/_images/graphviz-25889daa26b8ba11043c7f08f5c96b6a0d7e1173.png)]
客戶端向頻道傳送訊息, 訊息被傳遞至各個訂閱者。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-tSvHQhlM-1609686516369)(http://1e-gallery.redisbook.com/_images/graphviz-0c8815c9d27b05c73c8c67c5e8eb59c6cf5e2e3a.png)]
匹配模式
客戶端訂閱模式。
客戶端向頻道傳送訊息, 訊息被傳遞給正在訂閱匹配模式的訂閱者。
另一個模式被匹配的例子。
18.1 頻道的訂閱與退訂
struct redisServer{
/* Pubsub */
// 字典,鍵為頻道,值為連結串列
// 連結串列中儲存了所有訂閱某個頻道的客戶端
// 新客戶端總是被新增到連結串列的表尾
dict *pubsub_channels; /* Map channels to list of subscribed clients */
};
訂閱頻道
每當客戶端執行SUBSCRIBE命令訂閱某個或某些頻道的時候,伺服器都會將客戶端與被訂閱的頻道再pubsub_cannels字典進行關聯
退訂頻道
18.2 模式的訂閱與退訂
struct redisServer{
// 連結串列,包含多個 pubsubPattern 結構
// 記錄了所有訂閱頻道的客戶端的資訊
// 新 pubsubPattern 結構總是被新增到表尾
list *pubsub_patterns; /* patterns a client is interested in (SUBSCRIBE) */
};
/*
* 記錄訂閱模式的結構
*/
typedef struct pubsubPattern {
// 訂閱模式的客戶端
redisClient *client;
// 被訂閱的模式
robj *pattern;
} pubsubPattern;
訂閱模式
退訂模式
18.3 傳送訊息
18.4 檢視訂閱資訊
PUBSB CHANNELS
返回伺服器當前被訂閱的頻道
PUBSUB NUMSUB
PUBSUB NUMSUB [channel-1 channel-2…channel-n] 子命令接受任意多個頻道作為輸入引數,並返回這些頻道的訂閱者數量
PUBSUB NUMPAT
PUBSUB NUMPAT 子命令用於返回伺服器當前被訂閱模式的數量
18.5 重點回顧
chapter19 事務
Redis通過MULTI、EXEC、WATCH等命令來實現事務功能
19.1 事務的實現
事務開始
redis> MULTI
ok
通過切換客戶端狀態的flag屬性的REDIS_MULTI標識來完成
命令入隊
事務佇列
struct redisServer{
// 事務狀態
multiState mstate; /* MULTI/EXEC state */
};
/*
* 事務命令
*/
typedef struct multiCmd {
// 引數
robj **argv;
// 引數數量
int argc;
// 命令指標
struct redisCommand *cmd;
} multiCmd;
/*
* 事務狀態
*/
typedef struct multiState {
// 事務佇列,FIFO 順序
multiCmd *commands; /* Array of MULTI commands */
// 已入隊命令計數
int count; /* Total number of MULTI commands */
int minreplicas; /* MINREPLICAS for synchronous replication */
time_t minreplicas_timeout; /* MINREPLICAS timeout as unixtime. */
} multiState;
執行事務
19.2 WATCH 命令的實現
WATCH 命令是一個樂觀鎖嗎,它可以在EXEC命令執行之前,監視任意數量的資料庫鍵,並在EXEC命令執行時,檢查被監視的鍵是否至少有一個已經被修改過了,如果是的話,伺服器將拒絕執行事務,並向客戶端返回代表事務執行失敗的空回覆
使用WATCH命令監視資料庫鍵
struct redisDb{
// 鍵正在被 WATCH 命令監視的鍵,值為客戶端連結串列
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
};
監視機制的觸發
所有對資料庫進行修改的命令在執行前都會呼叫multi.c/touchWatchKey函式對wached_keys字典進行檢查,檢視是否有客戶端正在監視剛剛被命令修改過的資料庫鍵,如果有的話,那麼函式會將監視修改鍵的客戶端REDIS_DIRTY_CAS標識開啟,標識該客戶端的事務安全性已經被破壞。
touchWatchKey函式的定義可以用以下虛擬碼來描述:
判斷事務是否安全
當客戶端接收到一個客戶端發來的EXEC命令時,伺服器會根據這個客戶端是否打開了REDIS_DIRTY_CAS標識來決定是否執行事務
一個完整的WATCH事務執行過程
19.3 事務的 ACID 性質
在REDIS中,事務總具有原子性(Atomicity)、一致性(Consistency)和隔離性(Isolation),當Redis執行在某種特定的持久化模式下時,事務也具有耐久性(Durability)
原子性
一致性
chapter20 Lua指令碼
Redis從2.6版本開始引入對Lua指令碼的支援,通過在伺服器中嵌入Lua環境,Redis客戶端可以使用Lua指令碼,直接在伺服器端原子地執行多個Redis命令
20.1 建立並修改Lua環境
建立Lua環境
載入函式庫
建立redis全域性表格
20.2 Lua環境寫作元件
偽客戶端
lua_scripts字典
struct redisServer{
/* Scripting */
// Lua 環境
lua_State *lua; /* The Lua interpreter. We use just one for all clients */
// 複製執行 Lua 指令碼中的 Redis 命令的偽客戶端
redisClient *lua_client; /* The "fake client" to query Redis from Lua */
// 當前正在執行 EVAL 命令的客戶端,如果沒有就是 NULL
redisClient *lua_caller; /* The client running EVAL right now, or NULL */
// 一個字典,值為 Lua 指令碼,鍵為指令碼的 SHA1 校驗和
dict *lua_scripts; /* A dictionary of SHA1 -> Lua scripts */
// Lua 指令碼的執行時限
mstime_t lua_time_limit; /* Script timeout in milliseconds */
// 指令碼開始執行的時間
mstime_t lua_time_start; /* Start time of script, milliseconds time */
// 指令碼是否執行過寫命令
int lua_write_dirty; /* True if a write command was called during the
execution of the current script. */
// 指令碼是否執行過帶有隨機性質的命令
int lua_random_dirty; /* True if a random command was called during the
execution of the current script. */
// 指令碼是否超時
int lua_timedout; /* True if we reached the time limit for script
execution. */
// 是否要殺死指令碼
int lua_kill; /* Kill the script if true. */
};
20.3 EVAL命令的實現
定義指令碼函式
當客戶端向伺服器傳送EVAL命令,要求執行某個Lua指令碼的時候,伺服器首先要做的就是在Lua環境中,為傳入的指令碼定義一個與指令碼相對應的Lua函式,其中,Lua函式的名字由f_字首加上指令碼的SHA1校驗和(40字元長)組成,而函式的體則是指令碼本身
執行指令碼函式
[todo]
chapter21 排序
21.1 SORT<key> 命令的實現
// 用於儲存被排序值及其權重的結構
typedef struct _redisSortObject {
// 被排序鍵的值
robj *obj;
// 權重
union {
// 排序數字值時使用
double score;
// 排序字串時使用
robj *cmpobj;
} u;
} redisSortObject;
21.5 帶有ALPHA選項的BY選項的實現
伺服器執行 SORT fruits BY *-id ALPHA
時建立的資料結構。
21.6 LIMIT 選項
LIMIT <offset> <count>
返回已排序陣列以offset作為起始索引向後count個元素
SORT alphavet ALPHA LIMIT 2 3
返回:“c” “d” “e”
21.7 GET 選項
21.8 STORE 選項的實現
將排序結果依次推入選項值指定的新鍵中
21.9 多個選項的執行順序
chapter22 二進位制位陣列
22.4 BITCOUNT命令的實現
遍歷演算法
查表演算法
variable-precision SWAP演算法
chapter23 慢查詢日誌
Redis的慢查詢日誌功能用於記錄執行時間超過給定時長的命令請求,使用者可以通過這個功能產生的日誌來監視和優化查詢速度
伺服器配置有兩個和慢查詢相關的選項:
- slowlog-log-slower-than選項指定執行時間超過多少個微秒的命令請求會被記錄到日誌上
- slowlog-max-len 選項指定伺服器最多儲存多少條慢查詢日誌
23.1 慢查詢記錄的儲存
struct redisServer{
/* slowlog */
// 儲存了所有慢查詢日誌的連結串列
list *slowlog; /* SLOWLOG list of commands */
// 下一條慢查詢日誌的 ID
long long slowlog_entry_id; /* SLOWLOG current entry ID */
// 伺服器配置 slowlog-log-slower-than 選項的值
long long slowlog_log_slower_than; /* SLOWLOG time limit (to get logged) */
// 伺服器配置 slowlog-max-len 選項的值
unsigned long slowlog_max_len; /* SLOWLOG max number of items logged */
};
/*
* 慢查詢日誌
*/
typedef struct slowlogEntry {
// 命令與命令引數
robj **argv;
// 命令與命令引數的數量
int argc;
// 唯一識別符號
long long id; /* Unique entry identifier. */
// 執行命令消耗的時間,以微秒為單位
// 註釋裡說的 nanoseconds 是錯誤的
long long duration; /* Time spent by the query, in nanoseconds. */
// 命令執行時的時間,格式為 UNIX 時間戳
time_t time; /* Unix time at which the query was executed. */
} slowlogEntry;
伺服器狀態的 slowlog
屬性。
23.2 慢查詢日誌的閱覽和刪除
23.3 新增新日誌
chapter24 監視器
24.1 成為監視器
客戶端執行命令請求:redis> MONITOR
客戶端狀態的flags欄位會被設定
client.flags |= REDIS_MONITOR
伺服器狀態的monitors欄位會被追加該客戶端
server.monitors.append(client)
24.2 向監視器傳送命令資訊
[todo]
chapter21 排序
21.1 SORT<key> 命令的實現
// 用於儲存被排序值及其權重的結構
typedef struct _redisSortObject {
// 被排序鍵的值
robj *obj;
// 權重
union {
// 排序數字值時使用
double score;
// 排序字串時使用
robj *cmpobj;
} u;
} redisSortObject;
[外鏈圖片轉存中…(img-fjf5pjNg-1609686516392)]
21.5 帶有ALPHA選項的BY選項的實現
伺服器執行 SORT fruits BY *-id ALPHA
時建立的資料結構。
[外鏈圖片轉存中…(img-qOm5YqHc-1609686516394)]
[外鏈圖片轉存中…(img-XmTJjrbT-1609686516396)]
[外鏈圖片轉存中…(img-NRI4DtUr-1609686516397)]
21.6 LIMIT 選項
LIMIT <offset> <count>
返回已排序陣列以offset作為起始索引向後count個元素
[外鏈圖片轉存中…(img-ussCbXWx-1609686516399)]
SORT alphavet ALPHA LIMIT 2 3
返回:“c” “d” “e”
21.7 GET 選項
[外鏈圖片轉存中…(img-h5aOMX95-1609686516400)]
21.8 STORE 選項的實現
將排序結果依次推入選項值指定的新鍵中
21.9 多個選項的執行順序
chapter22 二進位制位陣列
22.4 BITCOUNT命令的實現
遍歷演算法
查表演算法
variable-precision SWAP演算法
chapter23 慢查詢日誌
Redis的慢查詢日誌功能用於記錄執行時間超過給定時長的命令請求,使用者可以通過這個功能產生的日誌來監視和優化查詢速度
伺服器配置有兩個和慢查詢相關的選項:
- slowlog-log-slower-than選項指定執行時間超過多少個微秒的命令請求會被記錄到日誌上
- slowlog-max-len 選項指定伺服器最多儲存多少條慢查詢日誌
23.1 慢查詢記錄的儲存
struct redisServer{
/* slowlog */
// 儲存了所有慢查詢日誌的連結串列
list *slowlog; /* SLOWLOG list of commands */
// 下一條慢查詢日誌的 ID
long long slowlog_entry_id; /* SLOWLOG current entry ID */
// 伺服器配置 slowlog-log-slower-than 選項的值
long long slowlog_log_slower_than; /* SLOWLOG time limit (to get logged) */
// 伺服器配置 slowlog-max-len 選項的值
unsigned long slowlog_max_len; /* SLOWLOG max number of items logged */
};
/*
* 慢查詢日誌
*/
typedef struct slowlogEntry {
// 命令與命令引數
robj **argv;
// 命令與命令引數的數量
int argc;
// 唯一識別符號
long long id; /* Unique entry identifier. */
// 執行命令消耗的時間,以微秒為單位
// 註釋裡說的 nanoseconds 是錯誤的
long long duration; /* Time spent by the query, in nanoseconds. */
// 命令執行時的時間,格式為 UNIX 時間戳
time_t time; /* Unix time at which the query was executed. */
} slowlogEntry;
[外鏈圖片轉存中…(img-f4HyCEqZ-1609686516403)]
伺服器狀態的 slowlog
屬性。
[外鏈圖片轉存中…(img-8TsgK9K7-1609686516404)]
23.2 慢查詢日誌的閱覽和刪除
[外鏈圖片轉存中…(img-EuFodNHZ-1609686516408)]
23.3 新增新日誌
chapter24 監視器
24.1 成為監視器
客戶端執行命令請求:redis> MONITOR
[外鏈圖片轉存中…(img-Fzwplhre-1609686516411)]
客戶端狀態的flags欄位會被設定
client.flags |= REDIS_MONITOR
伺服器狀態的monitors欄位會被追加該客戶端
server.monitors.append(client)
24.2 向監視器傳送命令資訊
[外鏈圖片轉存中…(img-UwrZ1oL1-1609686516413)]