Redis設計與實現筆記
數據結構
SDS
- 簡單動態字符串(Simple Dynamic String)
- 字段:
len, free, []char
, len+free+1(空字符) = len([]char)
- 保留空字符是爲了重用部分C函數。
- 修改字符串時減小內存重分配次數。增加時使用free,縮短時惰性回收。若是free不夠用從新申請內存,
free = newLen < 1MB ? newLen : 1MB
(free最多1MB)
鏈表
字典
- 經過鏈表解決鍵衝突。
- 兩個數組,其中一個做爲rehash使用。
rehash
發生的條件:擴展時:負載因子(節點數/數組大小)> 1, 若是在執行BGSAVE | BGREWRITEAOF
則 > 5,收縮時:負載因子<0.1。redis
擴張後的數組大小:大於等於(2*當前節點數)的最小的2^n^。算法
收縮後的數組大小:大於等於(當前節點數)的最小的2^n^。數據庫
漸進式rehash: 用rehashidx
表示當前正在rehash的索引,rehash時對字典的操做會同時在兩個數組上進行。數組
跳躍表
。。。緩存
整數集合intset
- 有序,無重複。
- encoding:
int16(初始), int32, int64
- 升級。好比當前是int16,當添加一個int32類型的整數時就須要升級,新添加的整數要麼大於全部舊元素,要麼小於全部舊元素???
- 搜索使用二分查找。
- 添加和刪除的最壞時間複雜度爲
O(n)
。
壓縮列表ziplist
- 特殊編碼的連續內存塊。
- 主要是爲了省內存。
- 能夠保存1,2,5字節長的字節數組或者1字節長的整型。
- 連鎖更新:在表頭插入了新節點(>=254字節),致使後面全部的節點的「前節點長度」都從1字節變爲5字節了。刪除也可能會致使連鎖更新。最壞複雜度是
O(n^2)
。
- 添加的複雜度是
O(n)|O(n^2)
, 按索引查找的複雜度O(n)
, 按值查找的複雜度O(n^m)
(m是字節數組的長度,由於要比較值).刪除的複雜度O(n)|O(n^2)
。(O(n^2)
都是由於可能會連鎖更新)
對象
- type:
string, list, hash, set, zset
- encoding:
int, embstr, raw, hashtable, linkedlist, ziplist, intset, skiplist
string
可用:int, embstr, SDS
安全
保存整數而且這個整數能夠用long
來表示時,使用int
。服務器
<= 32字節
使用 embstr
網絡
> 32字節
使用 SDS
數據結構
embstr
就是 redisObject
和 sdshdr
放到了連續的內存塊中。架構
embstr
是隻讀的,若是執行了修改命令,那麼就轉換成 SDS
編碼了。
使用 long double
表示的浮點數但是使用字符串來保存的(embstr
或者 SDS
)
list
可用:linkedList, zipList
使用zipList
的條件: 全部字符串元素小於64字節 && 元素數量小於512
hash
可用:hashtable, zipList
使用zipList
: 每次將鍵和值保存到列表尾部
使用zipList
的條件:全部鍵值對的字符串小於64字節 && 鍵值對數量小於512個。
set
可用:intset, hashtable
使用intset
的條件:元素都是整數值 && 元素數量小於512個。
zset
可用:zipList, skipList
使用zipList
:元素+分值,按照分值從小到大排序
使用skipList
時:zset中有一個字典用於保存元素到分值的映射,能夠在O(1)時間內獲取元素的分值。
使用zipList
的條件:元素長度小於64字節 && 元素數量小於128個。
內存回收
對象共享
- redis初始時會建立從0到9999共一萬個字符串對象。
- 不共享複雜對象(好比列表)的緣由:使用共享對象時要先檢查共享對象和預期的是否同樣,對象越複雜校驗的複雜度越高,佔CPU越多。
空轉時長
- 記錄對象最後一次被訪問的時間。
- 空轉時長就是當前時間減去最後一次被訪問的時間,也就是存活時間。
- 若是配置了
maxmemory
而且算法是lru時,空轉時長較高的會優先被釋放。
單機數據庫
數據庫
- 默認建立16個數據庫,使用數組保存數據庫信息。
- 客戶端會保存數據庫信息的指針,
select 1
命令就是修改這個指針。
- 數據庫保存一個字典
dict
,鍵是字符串,值是各類類型的對象。
- 數據庫保存一個字典
expires
,記錄鍵的過時時間。鍵是指針,值是毫秒的unix時間戳。
- 過時鍵刪除策略:惰性刪除(使用鍵以前先檢查是否過時)和按期刪除(屢次遍歷數據庫,隨機檢查部分鍵)
- 生成rdb文件時:過時鍵不會被保存到rdb中。
- 載入rdb文件時:主服務器模式忽略過時鍵,從服務器模式都會加載。
- AOF文件寫入:過時鍵被刪除時追加一條刪除命令。
- AOF重寫:過時鍵不會被保存到AOF中。
- 複製模式:主服務器會主動發送刪除命令,若是沒有發出,從服務器會返回過時鍵的值。
RDB持久化
- 能夠手動執行,也能夠按期執行。
- rdb文件是一個通過壓縮的二進制文件。
- rdb的建立:
SAVE
阻塞進程,不處理命令。BGSAVE
派生子進程,能夠繼續處理命令。
- 載入rdb文件時服務器處於阻塞狀態。
BGSAVE
能夠配置保存的頻率,好比多少秒內執行了多少次修改就會觸發持久化。(多少次修改由dirty計數器統計)
AOF持久化
- 先將寫命令追加到
aof_buf
中,以後事件循環會在每次循環時將aof_buf
中的內容寫入到文件中,至於要不要同步到aof文件中由配置決定(老是同步,距離上次1秒以上同步-默認,不一樣步由操做系統決定)
- AOF重寫:AOF文件會愈來愈大,重寫是用新的AOF文件代替大的文件。直接從數據庫讀取值生成寫命令,過時鍵不寫。若是數量多於64個,每64個做爲一個命令。使用子進程來重寫,重寫期間新的命令被放到aof緩衝區和重寫緩衝區,重寫結束後由父進程將重寫緩衝區中的內容寫到新的aof文件中。
事件
文件事件
- 與客戶端鏈接的socket。
- I/O多路複用,產生事件的套接字被放到一個隊列中一個一個執行。
- 若是套接字同時可讀可寫,先讀,後寫。讀 - 客戶端執行write, 寫 - 客戶端執行read
時間事件
- 事件被放在一個無序鏈表中,每次檢查都遍歷全部的事件,選出須要執行的。
- 正常模式下只有
serverCron
一個事件,因此即便遍歷全部節點也不影響性能。
事件調度
- 先獲取距離最近的時間事件
- 等待文件事件,若是有就處理,沒有的話就阻塞,阻塞時間由第1步獲取的時間決定。
- 處理時間事件。
服務器不會中途中斷事件處理,也不會對時間進行搶佔,靠事件處理自覺讓出執行權。
客戶端
- 輸入緩衝區:根據輸入內容動態調整,大小超過1G時會關閉這個客戶端。
- 命令和參數都保存在argv數組中。
- 服務器有一個命令表(字典),根據命令的名稱查找命令應該執行的結構體
redisCommond
。
- 輸出緩衝區有兩個,一個固定大小(16KB)的用於緩存小回復,一個可變大小的用於緩存大回復。固定大小的緩衝區用完後或者回復太大時才用可變緩衝區?可變大小的緩衝區也是有上限的(硬性限制和軟性限制)。
服務器
命令執行的步驟
- 客戶端向服務端發送命令
- 服務端將命令保存到客戶端的輸入緩衝區
- 分析命令,提取參數,查找命令對應的command
- 預備操做(檢查參數,身份驗證,內存佔用... …)
- 調用實現函數,將響應放到客戶端的輸出緩衝區
- 執行後續工做(是否加入慢查詢日誌,調用計數器增一,AOF緩衝區,其餘從服務器......)
- 將響應發送給客戶端並清空客戶端的輸出緩衝區。
serverCron函數
- 更新服務器時間緩存,用於對時間精度要求不高的功能,好比打印日誌,更新服務器的LRU時鐘,決定是否執行持久化任務等。
- 更新服務器的LRU時鐘,對象的lru時間就是根據這個計算的。
- 更新服務器每秒執行的命令次數。使用一個環記錄每次抽樣的結果,最後取平均值。
- 更新內存使用的最大值。
- 檢查是否退出服務。SIGTERM信號的處理器會將shutdown_asap標識記爲1,serverCron會檢查這個標識,而後執行退出步驟,退出時還會進行rdb持久化。
- 管理客戶端資源。鏈接是否超時,調整輸入緩衝區大小。
- 管理數據庫。刪除過時鍵,對字典進行收縮等。
- 將AOF緩衝區寫入到AOF文件中。
初始化服務器
- 初始化服務器狀態結構
- ID
- 默認運行頻率
- 默認配置文件路徑
- 運行架構32或64
- 默認端口號
- LRU時鐘
- 命令表
- 載入配置,參數或者配置文件
- 初始化數據結構
- 設置操做
- 信號處理器
- 建立共享對象
- 監聽端口
- 爲serverCron建立時間事件
- 打印redis圖標
- 還原數據庫,AOF文件 || RDB文件
- 執行事件循環
多機數據庫
複製
127.0.0.1:12345> SLAVEOF 127.0.0.1 6379
:6370爲主服務器,:12345爲從服務器,從服務器複製主服務器。
舊版複製功能
- 從服務器向主服務器發送SYNC命令
- 主服務器執行
BGSAVE
,生成RDB文件,並用一個緩衝區記錄從如今開始的寫命令。
- 主服務器將RDB文件發給從服務器,從服務器載入RDB
- 主服務器將緩衝區的寫命令發給從服務器
- 命令傳播:主服務器將本身執行的寫命令不斷地發給從服務器。
缺陷:
斷線後重連:若是主從斷線,從鏈接上主後,將發送SYNC命令生成RDB,若是斷線時間短,缺失的命令比較少,這樣作很浪費資源。
新版複製功能
使用PSYNC
代替SYNC
- 完整重同步:和SYNC的過程同樣。
- 部分(partial)重同步:只將斷線後的寫命令發給從服務器。
PSYNC
的實現:
- 若是沒有複製過或者上次複製的主服務器ID不一致,執行完整重同步。
- 主服務器有一個固定大小的先進先出的隊列,記錄發給從服務器的寫命令。若是從服務器的複製偏移量(按byte來算的)在這個隊列範圍內,那麼執行部分重同步,不然執行完整重同步。
心跳檢測
三個做用:
- 檢查主從間的網絡鏈接狀態(超過一秒沒有回覆說明有問題)
- 實現
min-slaves
: 當從服務器少於3個或者3個從服務器的延遲都>=10秒,主服務器拒絕執行寫命令。
- 檢測命令丟失。根據從服務器的複製偏移量,主服務器知道哪些寫命令丟失了,將從緩衝區裏從新發送。
Sentinel哨兵
- 哨兵系統有一個或多個哨兵實例組成。
- 哨兵系統監聽主服務器和它的全部從服務器。
- 當主服務器下線時長超過設置值時,從 從服務器 中選擇一個做爲主服務器,並向其餘從服務器發送新的複製指令。
- 監視已下線的主服務器,當它上線後,將它設置成從服務器。
細節:
- Sentinel做爲主服務器的客戶端,建立兩個鏈接,一個用於向主服務器發送命令,一個用於訂閱。
- Sentinel每10秒向主服務器發送INFO命令,分析主服務器的信息和從服務器的信息。
- Sentinel每10秒向從服務器發送INFO命令,分析從服務器信息。也是兩個鏈接。
- Sentinel每2秒向主從發送Sentinel和主服務器的信息,經過訂閱頻道。
選舉Sentinel
- S1向其餘S發送請求,每一個S在一個紀元內只能選一個領頭者,先到先得,得到半數以上投票的當選。
從服務器中選主服務器
- 全部從服務器存到一個列表中
- 刪除下線的從服務器,保證都是正常運行的
- 刪除最近5秒沒有回覆領頭Sentinel的INFO命令的,保證都是最近成功進行通訊的。
- 刪除過早與主服務器斷開鏈接的,保證數據比較新
- 選擇優先級最高的
- 優先級相同的話,選擇複製偏移量最大的,再就是選擇ID最小的
集羣
- 16384(2048*8)個槽都有節點處理時,集羣處於上線狀態。
從新分片
獨立功能
發佈與訂閱
- 訂閱頻道:
subscribe "new.it"
- 訂閱模式:
subscribe "new.*"
- 頻道訂閱信息在redis中是一個
pubsub_channels
的字典,鍵爲頻道名稱,值爲client的列表。
- 模式訂閱信息在redis中是一個
pubsub_patterns
的列表,元素是client。當發佈信息時,須要遍歷整個列表找出匹配的模式。
事務
事務的實現
MULTI
時客戶端狀態打開REDIS_MULTI
。
- 若是是
EXEC, DISCARD, WATCH, MULTI
中的一個,就當即執行命令。
- 發送的其餘命令被放到一個事務隊列中(先進先出)。
- 遇到
EXEC
命令時就馬上執行隊列中的命令,並將結果發給客戶端。
WATCH命令的實現
- 每一個數據庫都保存着
watched_keys
字典,鍵是數據庫鍵,值是客戶端列表。
- 當執行對數據庫修改的命令時就查看有沒有監控這個鍵的客戶端,有的話就將客戶端的
REDIS_DIRTY_CAS
標識打開,標識事務的安全性被破壞了。
- 當執行客戶端發來的
EXEC
命令時,就檢測REDIS_DIRTY_CAS
是否被打開,是的話就拒絕執行事務。返回nil
事務的ACID性質
- A:原子性。若是命令入隊出錯,那麼整個事務都不會被執行。若是命令在執行時出錯,那麼整個事務都將繼續執行。
- C:一致性。事務要麼執行要麼不執行,宕機後要麼恢復(rdb, aof),要麼空白。
- I:隔離性。單線程執行,事務是串行執行的。
- D:持久性。只有在AOF模式下,而且老是同步文件時纔有持久性。
二進制數組
- 使用SDS保存。
- 逆序存儲。最低位保存在數組的開始。當擴展時不須要移動原來的數據。
1的個數
- 遍歷數組,遇到1,計數器就增一。
- 查表。表的鍵是8位或者16位二進制組合,值是其中包含的1的個數,這樣每8位只須要查一次表。
- variable-precision SWAR算法,經過位運算,先兩個兩個,再四個四個。。。,一次能夠處理32位。
redis使用查表和variable-precision SWAR算法結合。
若是未處理的二進制位數>=128位,就調用4次variable-precision SWAR算法。
若是未處理的二進制位數<128位,就查表,表的key是8位的排列。
慢查詢日誌
- 經過
slowlog-log-shower-than
指定執行時間超過多少微妙會被記錄到日誌上。
slowlog-max-len
指定最多保存多少條慢查詢日誌。
- 使用先進先出的鏈表保存日誌,達到最大值時將最開始的刪除。
監視器
MONITOR
實時接收並打印服務器當前處理的命令。
- 客戶端的
REDIS-MONITOR
標識被打開,而且被添加到monitors鏈表末尾。