第二部分 單機資料庫的實現
資料庫
伺服器中的資料庫
- Redis伺服器的所有資料庫都儲存在redisServer.db陣列中,而資料庫的數量使用redisServer.dbnum屬性儲存
切換資料庫
- 客戶端通過修改目標資料庫指標,讓它指向redisServer.db陣列中的不同元素來切換不同的資料庫
資料庫鍵空間
- 資料庫主要由dict和expires兩個字典域構成,其中dict字典負責儲存鍵值對,而expires字典則負責鍵的過期時間
- 因為資料庫由字典構成,因此對資料庫的操作都是建立在對字典操作之上
- 資料庫的鍵總是一個字串物件,而值則可以是任意一種Redis物件型別,包括字串物件、雜湊表物件、集合物件、列表物件、有序集合物件。
設定鍵的過期時間
- expires字典的鍵指向資料庫中的某個鍵,而值則記錄了資料庫鍵的過期時間,過期時間以毫秒為單位的UNIX時間戳
過期鍵刪除策略
-
三種不同的刪除策略
- 定時刪除
- 在設定一個鍵的同時,建立一個定時器,讓定時器在鍵過期時間來臨時,立即執行對鍵的刪除操作
- 優點: 對記憶體友好,能儘快地將過期鍵佔用的記憶體釋放
- 缺點: 對時間不友好,如果過期鍵很多,那麼會佔用大量CPU時間,影響伺服器響應時間和吞吐量
- 惰性刪除
- 放任過期鍵不管,但是每次從鍵空間中獲取鍵時,都檢查取得的鍵是否過期,如果過期,就刪除該鍵;如果沒有過期,就返回該鍵
- 優點:對時間友好,只有當取出過期鍵時,才將該鍵刪除
- 缺點:對空間不友好,大量過期無用鍵佔用記憶體,由記憶體洩露的風險
- 定期刪除
- 每個一段時間,程式就對資料庫進行一次檢查,刪除裡面的過期鍵,至於要刪除多少過期鍵,以及要檢查多少個資料庫,由演算法決定
- 優點: 對上面兩種策略的折衷。 對記憶體友好,對空間友好
- 關鍵是如何決定刪除操作執行的時常和頻率
- 定時刪除
-
Redis的過期鍵刪除策略
-
Redis使用的是
定期刪除
+惰性刪除
保證過期鍵一定能被刪除。併合理利用CPU時間和避免記憶體空間浪費 -
惰性刪除 : 在執行命令之前,對輸入的鍵進行過期檢查
-
定期刪除 : 在規定時間內,分多次遍歷伺服器中多個資料庫,從資料庫中的expires字典隨機檢查一部分鍵的過期時間,並刪除其中的過期鍵。
-
AOF、RDB和複製功能對過期鍵的處理
- 執行SAVE命令或者BGSAVE命令所產生的新RDB檔案不會包含已過期的鍵
- 執行BGREWRITEAOF命令所產生的重寫AOF檔案不會包含已過期的鍵
- 當一個過期鍵被刪除之後,伺服器會追加一條DEL命令到現有的AOF檔案末尾,顯示地刪除過期鍵
- 當載入RDB or AOF檔案時,會對檔案儲存的鍵進行檢查,過期的鍵會被忽略。
- 從伺服器即使發現過期鍵也不會主動刪除,而是等待主節點發來DEL命令,這種統一、中心化的過期鍵刪除策略可以保證主從伺服器的資料一致性。
資料庫通知
- 當Redis命令對資料庫進行修改之後 , 伺服器會根據配置向客戶端發出資料庫通知 (PUB/SUB)
- 鍵空間通知: 某個鍵執行了什麼命令(SET / EXPIRE / DEL)
- 鍵事件通知: 某個命令被哪些鍵執行了 (KEY1 / KEY2 / KEY3)
RDB持久化
前置知識: 程式和子程式
可以看出,子程式和父程式的程式碼區是共享的,而資料區和PCB塊是父程式的副本。
子PCB中的PID欄位為新分配子程式PID,資料集欄位為資料集地址。
父程式和子程式是可以並行執行的。互不干擾。
RDB檔案的建立與載入
-
RDB持久化通過儲存資料庫中的鍵值對來記錄資料庫的狀態 , 生成經過壓縮的二進位制檔案。
-
建立過程
- SAVE命令由伺服器程式直接執行儲存操作,因此該命令會阻塞伺服器
- BGSAVE由子程式執行儲存操作,所以該命令不會阻塞伺服器
-
載入過程
- 如果伺服器開啟了AOF持久化功能,那麼伺服器會優先使用AOF檔案還原資料庫狀態
- 如果AOF處於關閉狀態,伺服器才會使用RDB檔案來還原資料庫狀態(前者丟失的資料更少)
自動間隔性儲存
-
伺服器狀態中會儲存所有用save選項設定的儲存條件,當任意一個儲存條件被滿足時,伺服器會自動執行BGSAVE命令。
#redis.conf 格式: save 時間 修改次數 save 900 1 (900s內修改1次) save 300 10 save 60 10000 (60s內修改10000次) 複製程式碼
struct redisServer{ struct saveparam *saveparams; //記錄儲存條件的資料 long long dirty; //修改計數器 time_t lastsave; //上一次執行儲存的時間 } 複製程式碼
RDB檔案的結構
- 對於不同型別的鍵值對,RDB檔案會使用不同的方式來儲存他們
AOF持久化
AOF(Append Only File)持久化實現
-
RDB持久化通過儲存資料庫中的鍵值對來記錄資料庫狀態的不同
-
AOF持久化是通過儲存Redis伺服器所執行的寫命令來記錄資料庫狀態的
-
AOF檔案中所有命令都是以Redis命令請求協議的格式(文字協議)儲存的
-
命令請求會先儲存到AOF緩衝區裡面,之後再定期寫入並同步到AOF檔案中
- 由於記憶體和磁碟的輸入/輸出速度不匹配,因此會將資料先寫入緩衝區。系統提供了
fsync, fdatasync
兩個同步函式(系統呼叫),讓作業系統立即將緩衝區的資料寫入硬碟中,減少緩衝區由於當機而丟失資料的影響
- 由於記憶體和磁碟的輸入/輸出速度不匹配,因此會將資料先寫入緩衝區。系統提供了
-
appendfsync選項的不通值對AOF持久化功能的安全性和Redis伺服器的效能有很大的影響
- always : 每個事件迴圈都將aof_buf緩衝區內容寫入同步到AOF檔案
- everysec(預設) : 每個事件迴圈後,判斷上一次AOF是否間隔1S,如果是,則將aof_buf緩衝區內容寫入同步到AOF檔案。 因此就算故障停機,快取也只丟失1S的資料。
- no : 什麼時候將緩衝區內容同步到AOF檔案中,由作業系統決定
AOF檔案的載入與資料還原
- 伺服器只要載入並重新執行儲存在AOF檔案中的命令(使用偽客戶端),就可以還原資料庫本來的狀態了。
AOF重寫
- 為了解決AOF體積膨脹的問題,提供了AOF重寫機制。AOF重寫可以產生一個新的AOF檔案,這個新的AOF檔案和原有的AOF檔案儲存的資料庫狀態是一樣的,但體積更小
- AOF重寫是一個由歧義的名字,程序無需對現有AOF檔案進行任何裝入、分析和寫入操作。它是通過讀取資料庫中的鍵值對來實現的。
- AOF重寫程式放在子程式中執行,此時伺服器程式可以繼續處理命令請求
- 子程式帶有伺服器程式資料的副本(資料一致性問題),那麼如果在重寫過程中有新的寫請求更改資料庫狀態,就會產生當前資料庫狀態與重寫後的AOF檔案狀態不一致問題。
- 在執行BGREWRITEAOF命令時,Redis伺服器會維護一個AOF重寫緩衝區,該緩衝區會在子程式建立新AOF檔案期間,記錄伺服器執行的所有寫命令。當子程式完成建立新AOF檔案的工作後,伺服器會將重寫緩衝區中的所有內容追加到新的AOF檔案的末尾,使得新舊兩個AOF檔案所儲存的資料狀態一致。隨後,用新的AOF檔案替換舊的AOF檔案,以此來完成AOF檔案重寫操作
-
- 在AOF重寫期間,伺服器的執行工作
- 執行客戶端的命令
- 將執行後的寫命令追加到AOF緩衝區(保證舊的AOF檔案完整)
- 將執行後的寫命令追加到AOF重寫緩衝區(用於解決資料不一致問題)
- 在AOF重寫期間,伺服器的執行工作
事件
Redis伺服器是一個
事件驅動程式
,伺服器處理的事件分為檔案事件和時間事件兩類
檔案事件
- 檔案事件處理器是基於
Reactor模式
實現的網路通訊程式 - 檔案事件處理器使用
IO多路複用
程式來同時監聽多個套接字。並根據套接字目前執行的任務來為套接字關聯不同的事件處理器
- 當被監聽的套接字準備好執行連線應答(accept)、讀取(read)、寫入(write)、關閉(close)時,與操作對應的檔案事件就會產生,這時檔案事件處理器就會呼叫套接字之前關聯好的事件處理器來處理這些事件。
- 檔案事件是對套接字操作的抽象,每次套接字變為可應答(acceptable)、可寫(writeable)或者可讀(reable)時,相應的檔案事件就會產生
- 檔案事件分為AE_READABLE事件(讀事件)和AE_WRITEABLE事件(寫事件)兩類
-
一次完整的客戶端與服務端連線事件示例
-
Redis伺服器執行時, 將連線應答處理器與 AE_READABLE事件關聯起來
-
當Redis客戶端發起連線時,那麼監聽套接字將產生AE_READABLE事件,觸發連線應答處理器執行。處理跟客戶端建立連線,並將客戶端套接字的AE_READABLE事件與命令請求處理器關聯起來
-
當客戶端向redis發起請求的時候,那麼客戶端套接字將產生AE_READABLE事件,然後由對應的命令請求處理器來處理。讀取客戶端的命令內容,並傳給相應程式執行。
-
那麼當redis準備好給客戶端響應資料之後,服務端會將AE_WRITEABLE事件跟命令回覆處理器關聯起來。當客戶端準備嘗試讀取響應資料時,客戶端套接字就會產生AE_WRITEABLE事件,觸發命令回覆處理器執行處理,將準備好的資料返回給客戶端。 當回覆寫完時,伺服器就會解除客戶端套接字的AE_WRITABLE事件與命令回覆處理器之間的關聯。
-
時間事件
-
時間事件分為定時事件和週期性事件;定時事件只在指定時間到達一次,而週期性事件則每隔一段事件到達一次。
-
伺服器在一般情況下只執行serverCorn函式一個時間事件,並且是週期性的(100ms一次)
事件實現的三個屬性: id:時間事件全域性ID , when:事件到達時間 timeProc:事件處理函式 與一個由事件節點構成的無序連結串列 複製程式碼
事件的排程與執行
-
檔案事件和時間事件之間是合作關係,伺服器會輪流處理這兩種事件,並且處理事件過程中不會發生搶佔。
-
時間事件的實際處理事件通常會比設定的到達晚一些(因為無法中斷檔案事件)
-
ServerCron是Redis週期性事件的主要函式。 它的工作主要包括
- 更新伺服器的各類統計資訊,如時間,記憶體佔用
- 清理資料庫過期鍵值對
- 嘗試進行AOF和RDB操作等等
客戶端
-
伺服器狀態結構使用clients連結串列表示連線了多少個客戶端狀態,新新增的客戶端狀態會被放到鏈尾
-
客戶端狀態flags屬性使用不同標誌來表示客戶端的角色,以及客戶端當前所在狀態
-
輸入緩衝區記錄了客戶端傳送的命令請求,這個緩衝區大小不超過1GB
-
客戶端使用argv , argc兩個屬性記錄命令的引數和個數 , 而cmd屬性記錄了客戶端要執行命令的實現函式
-
客戶端有固定大小緩衝區和可變大小緩衝區兩種, 其中固定大小緩衝區最大大小為16KB , 而可變大小緩衝區(由多個緩衝區組成, 用連結串列連結)最大大小不能超過伺服器設定的硬性限制值
-
輸出緩衝區限制值有兩種,如果輸出緩衝區的大小超過了伺服器設定的硬性限制, 那麼客戶端會被立即關閉 ; 除此之外 ; 如果客戶端在一定時間內,一直超過伺服器設定的軟性限制,那麼客戶端也會關閉.
#設定硬性 , 軟性連結 命令名 客戶端角色 硬性連結 軟性連結 軟性連結時長 client-output-buffer-limit normal 0 0 0 client-output-buffer-limit slave 256mb 64mb 60 client-output-buffer-limit pubsub 32mb 8mb 60 複製程式碼
-
客戶端關閉的原因 : 網路連線關閉 ; 傳送了不合格時的命令請求 ; 成為CLIENT KILL目標 ; 空轉時間超時 ; 輸出緩衝區的大小超出限制.
服務端
- 一個命令請求從傳送到完成要經歷的步驟:
- 客戶端將命令請求發給伺服器
- 伺服器讀取命令請求,並分析命令引數
- 命令執行器根據引數查詢命令的實現函式,然後執行實現函式並得出命令回覆
- 執行預備操作: 如檢驗命令的格式 ; 記憶體是否足夠 ; 命令此時是否合法 ; 檢視是否開啟事務
- 呼叫命令實現函式
- 執行後續操作: 更改統計資訊,如耗費時長 ; 如果開啟了AOF還要往緩衝區寫資料 ; 如果它是master,那麼還要將資料同步到從伺服器
- 伺服器將命令回覆返回給客戶端
- ServerCron函式(每隔100ms執行一次,維護伺服器相關資源,並做統計)
- 更新伺服器時間快取
- 更新LRU時鐘 (空轉時間 = LRU時鐘 - 某個鍵上次訪問時間 )
- 更新伺服器每秒執行的命令數 (統計吞吐量) ; 更新記憶體峰值
- 處理SIGTERM訊號(中斷訊號)
- 管理資料庫資源(檢查過期鍵)
- 將AOF緩衝區內容寫入AOF (每次事件迴圈時都會做出檢查)
- 伺服器從啟動到能夠處理客戶端請求經過的步驟
- 初始化伺服器狀態
- 載入伺服器配置
- 初始化伺服器資料結構
- 還原資料庫狀態
- 執行事件迴圈