Redis設計與實現筆記

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 就是 redisObjectsdshdr 放到了連續的內存塊中。架構

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. 先獲取距離最近的時間事件
  2. 等待文件事件,若是有就處理,沒有的話就阻塞,阻塞時間由第1步獲取的時間決定。
  3. 處理時間事件。

服務器不會中途中斷事件處理,也不會對時間進行搶佔,靠事件處理自覺讓出執行權。

客戶端

  • 輸入緩衝區:根據輸入內容動態調整,大小超過1G時會關閉這個客戶端。
  • 命令和參數都保存在argv數組中。
  • 服務器有一個命令表(字典),根據命令的名稱查找命令應該執行的結構體redisCommond
  • 輸出緩衝區有兩個,一個固定大小(16KB)的用於緩存小回復,一個可變大小的用於緩存大回復。固定大小的緩衝區用完後或者回復太大時才用可變緩衝區?可變大小的緩衝區也是有上限的(硬性限制和軟性限制)。

服務器

命令執行的步驟

  1. 客戶端向服務端發送命令
  2. 服務端將命令保存到客戶端的輸入緩衝區
  3. 分析命令,提取參數,查找命令對應的command
  4. 預備操做(檢查參數,身份驗證,內存佔用... …)
  5. 調用實現函數,將響應放到客戶端的輸出緩衝區
  6. 執行後續工做(是否加入慢查詢日誌,調用計數器增一,AOF緩衝區,其餘從服務器......)
  7. 將響應發送給客戶端並清空客戶端的輸出緩衝區。

serverCron函數

  • 每100毫秒執行一次
  1. 更新服務器時間緩存,用於對時間精度要求不高的功能,好比打印日誌,更新服務器的LRU時鐘,決定是否執行持久化任務等。
  2. 更新服務器的LRU時鐘,對象的lru時間就是根據這個計算的。
  3. 更新服務器每秒執行的命令次數。使用一個環記錄每次抽樣的結果,最後取平均值。
  4. 更新內存使用的最大值。
  5. 檢查是否退出服務。SIGTERM信號的處理器會將shutdown_asap標識記爲1,serverCron會檢查這個標識,而後執行退出步驟,退出時還會進行rdb持久化。
  6. 管理客戶端資源。鏈接是否超時,調整輸入緩衝區大小。
  7. 管理數據庫。刪除過時鍵,對字典進行收縮等。
  8. 將AOF緩衝區寫入到AOF文件中。

初始化服務器

  • 初始化服務器狀態結構
    • ID
    • 默認運行頻率
    • 默認配置文件路徑
    • 運行架構32或64
    • 默認端口號
    • LRU時鐘
    • 命令表
  • 載入配置,參數或者配置文件
  • 初始化數據結構
    • clients鏈表
    • db數組
  • 設置操做
    • 信號處理器
    • 建立共享對象
    • 監聽端口
    • 爲serverCron建立時間事件
    • 打印redis圖標
  • 還原數據庫,AOF文件 || RDB文件
  • 執行事件循環

多機數據庫

複製

127.0.0.1:12345> SLAVEOF 127.0.0.1 6379

:6370爲主服務器,:12345爲從服務器,從服務器複製主服務器。

舊版複製功能

  1. 從服務器向主服務器發送SYNC命令
  2. 主服務器執行BGSAVE,生成RDB文件,並用一個緩衝區記錄從如今開始的寫命令。
  3. 主服務器將RDB文件發給從服務器,從服務器載入RDB
  4. 主服務器將緩衝區的寫命令發給從服務器
  5. 命令傳播:主服務器將本身執行的寫命令不斷地發給從服務器。

缺陷

斷線後重連:若是主從斷線,從鏈接上主後,將發送SYNC命令生成RDB,若是斷線時間短,缺失的命令比較少,這樣作很浪費資源。

新版複製功能

使用PSYNC代替SYNC

  • 完整重同步:和SYNC的過程同樣。
  • 部分(partial)重同步:只將斷線後的寫命令發給從服務器。

PSYNC的實現:

  1. 若是沒有複製過或者上次複製的主服務器ID不一致,執行完整重同步。
  2. 主服務器有一個固定大小的先進先出的隊列,記錄發給從服務器的寫命令。若是從服務器的複製偏移量(按byte來算的)在這個隊列範圍內,那麼執行部分重同步,不然執行完整重同步。

心跳檢測

  • 從服務器發給主服務器
  • 每秒一次
  • 參數:複製偏移量

三個做用:

  1. 檢查主從間的網絡鏈接狀態(超過一秒沒有回覆說明有問題)
  2. 實現min-slaves: 當從服務器少於3個或者3個從服務器的延遲都>=10秒,主服務器拒絕執行寫命令
  3. 檢測命令丟失。根據從服務器的複製偏移量,主服務器知道哪些寫命令丟失了,將從緩衝區裏從新發送。

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. 遍歷數組,遇到1,計數器就增一。
  2. 查表。表的鍵是8位或者16位二進制組合,值是其中包含的1的個數,這樣每8位只須要查一次表。
  3. 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鏈表末尾。
相關文章
相關標籤/搜索