redis設計與實現讀書筆記-單機數據庫的實現

1. 數據庫

redis數據庫的很多操作都是通過對鍵空間進行操作來實現的,比如添加,刪除,更新,取值操作,比如用於清空整個數據庫的FLUSHDB命令,用於返回數據庫中隨機鍵的RANDOMKEY,類似的命令還有EXISTS,RENAME,KEYS等.

當使用redis命令對數據庫進行讀寫時,服務器不僅對鍵空間執行指定的讀寫操作,還會執行一些額外的維護操作,包括:

  • 讀取一個鍵後(包括讀操作和寫操作)更新鍵的命中(hit)或不命中(miss)的次數,這兩個值可以在INFO stats命令的keyspace_hits屬性和keyspace_misses屬性中查看.
  • 更新鍵的LRU(最後一次使用)時間,這個值可以用來計算鍵的閒置時間(爲將來鍵刪除算法做準備)
  • 讀取時如果發現鍵已經過期,就刪除該鍵之後再執行剩下的操作
  • 如果有客戶端使用WATCH命令監視了某個鍵,那麼服務器在對被監視的鍵進行修改之後,會將這個鍵標記爲髒(dirty)
  • 服務器每次修改一個鍵之後,都會對髒(dirty)鍵計數器的值增1,這個計數器會觸發服務器的持久化和複製操作

設置過期時間

redis有四個命令用來設置鍵的過期時間:

EXPIRE <key> <ttl> 將鍵的生存時間設置爲ttl

PEXPIRE <key> <ttl>將鍵的生存時間設置爲ttl毫秒

EXPIRE <key> <timestamp> 將鍵的生存時間設置爲timestamp所指定的秒數時間戳

PEXPIRE <key> <timestamp>將鍵的生存時間設置爲timestamp所指定的毫秒數時間戳

以上四個命令,最終執行都是轉換爲第四個命令的執行方式執行的.

保存過期時間

redisDb結構的expires字典保存了數據庫中所有鍵的過期時間,這個字典叫做過期字典:過期字典是一個long long類型的整數,這個整數保存了鍵所指向的數據庫鍵的過期時間(毫秒精度的UNIX時間戳)

移除過期時間

PERSIST命令可以移除一個鍵的過期時間,用法 PERSIST key

計算並返回剩餘生存時間

TTL命令以秒爲單位返回鍵的剩餘生存時間,而PTTL命令則以毫秒爲單位返回鍵的剩餘生存時間,這兩個命令都是通過計算鍵的過期時間和當前時間之間的差來實現的

過期鍵刪除策略

  • 定時刪除: 在設置鍵的過期時間的同時,創建一個定時器(timer),讓定時器在鍵的過期時間來臨時,立即執行鍵刪除操作

    優點: 對內存友好,通過使用定時器保證過期鍵會盡快刪除,並釋放掉佔用的內存

    缺點: 對CPU時間不友好,比如某一時刻過期鍵比較多,那就會佔用比較多的CPU時間來刪除

  • 惰性刪除: 平時不對鍵進行操作,當從鍵空間獲取鍵的時候,檢查鍵是否過期,過期的話就刪除該鍵,沒過期就返回

    優點: 對CPU時間友好

    缺點: 對內存不友好,比如一個鍵已經過期,但是沒有及時刪除,就會一直佔用內存

  • 定期刪除: 每個一段時間對數據庫進行檢查,刪除裏面的過期鍵,至於刪除多少,檢查多少個數據庫,由具體算法確定

    定期刪除是以上兩種方案優缺點的折中,但是需要合理設定刪除操作的執行時長和頻率

redis實際採取的是惰性刪除和定期刪除兩種策略

惰性刪除策略由db.c/expireIfNeeded函數實現,所有讀寫數據庫的命令(如SET LRANGE SADD HGET KEYS等)在執行之前都會調用該函數對鍵進行檢查:如果已經過期該函數會將該鍵刪除,否則不做操作.

定期刪除策略由redis.c/activeExpireCycle函數實現,每當redis的服務器週期性操作redis.c/serverCron函數執行時,activeExpireCycle函數就會被調用.

AOF/RDB和複製功能對過期鍵的處理

生成RDB文件: 當執行SAVE或者BGSAVE命令創建新的RDB文件時,已過期的鍵不會被保存到新建的RDB文件中

載入RDB文件: 啓動redis服務器時,如果服務器開啓了RDB功能,那麼服務器將對RDB文件進行載入.如果服務器以主服務器模式運行,載入RDB文件時,會對文件中保存的鍵進行檢查,過期的鍵不會被載入;如果服務器以從服務器的模式運行,不論鍵是否過期,都會載入到數據庫,但是由於主從服務器進行數據同步的時候從服務器的數據庫就會清空,所以不會有什麼影響.

AOF文件寫入: 當過期鍵被惰性刪除或者定期刪除之後,程序會向AOF文件追加一條DEL命令,來顯式的記錄該鍵已被刪除

AOF重寫: 在執行AOF重寫的過程中會對數據庫中的鍵進行檢查,如果已經過期了的不會被保存到重寫後的AOF文件

複製: 當服務器運行在複製模式下時,從服務器的過期鍵刪除動作由主服務器控制:

  • 主服務器刪除一個鍵之後會顯式的向所有從服務器發送一個DEL命令,告知從服務器刪除這個過期鍵
  • 從服務器只有在接到主服務器發來的DEL命令之後,纔會刪除過期鍵
  • 從服務器接到客戶端發送的讀命令時,即使碰到過期鍵也不會處理,還是會按照正常情況返回值

數據庫通知

數據庫通知功能可以讓客戶端通過訂閱給定的頻道或模式,來獲知數據庫中鍵的變化,以及數據庫中命令的執行情況,這類通知可以分爲兩類:

鍵空間通知(key-space notification): 關注某個鍵執行了什麼命令,比如針對某個key執行了expire,set,del等

鍵事件通知(key-event notification): 關注某個命令被什麼鍵執行了,比如del命令執行在哪些鍵上了

服務器配置的notify-keyspace-events選項決定了服務器所發送通知的類型:

AKE : 發送所有類型的鍵空間和鍵事件通知

AK : 發送所有類型的鍵空間通知

AE : 發送所以類型的鍵事件通知

K$ : 只發送和字符串鍵有關的鍵空間通知

El : 只發送和list鍵有關的鍵事件通知

2. RDB持久化

RDB持久化功能所生成的RDB文件是一個經過壓縮的二進制文件,通過該文件可以還原生成RDB文件時的數據庫狀態.

RDB文件的創建與載入

有兩個Redis命令可以用於生成RDB文件,一個是SAVE,另一個是BGSAVE,SAVE 命令會阻塞服務器進程,BGSAVE命令會派生出一個子進程,然後由子進程負責創建RDB文件.

Redis並沒有專門用於載入RDB文件的命令,只要服務器啓動之後檢測到RDB文件就會自動載入,由於AOF文件的更新頻率通常比RDB文件的更新頻率高,所以:

  • 如果服務器開啓了AOF持久化功能,則優先使用AOF文件來還原數據庫狀態
  • AOF持久化功能關閉時,會採用RDB文件來還原數據

服務器在載入RDB文件期間,會一直處於阻塞狀態,直到載入工作完成爲止,BGSAVE命令可以在配置文件中配置每個一段時間自動執行,詳見save 900 1...這一組配置

具體RDB文件保存的格式和內容建議直接看原書第10章內容

3.AOF持久化

與RDB持久化通過保存數據庫中的鍵值對來記錄數據庫狀態不同,AOF持久化是通過保存Redis服務器所執行的寫命令來記錄數據庫的狀態的,AOF持久化功能的實現分爲命令追加(append),文件寫入,文件同步(sync)三個步驟

命令追加

當AOF持久化功能處於打開狀態時,服務器在執行完一個寫命令之後,會以協議格式將被執行的寫命令追加到服務器狀態的aof_buf緩衝區的末尾:

struct redisServer{
	// ...
    
    //AOF緩衝區
    sds aof_buf;
    //...
}

例如:當客戶端向服務器發送如下命令:

redis > SET KEY VALUE

OK

服務器在執行完命令之後,會將以下協議內容追加到aof_buf緩衝區的末尾:

*3\r\n$3\r\nSET\r\nKEY\r\n$5\r\nVALUE\r\n

文件的寫入與同步

reids服務器進程就是一個事件循環(loop).這個循環中的文件事件負責接收客戶端的命令請求,以及向客戶端發送命令回覆,而時間事件則負責執行像serverCron函數這樣需要定時運行的函數.

由於服務器在處理文件事件時可能會執行寫命令.在此期間一些內容被追加到aof_buf緩衝區中,所以在服務器每次結束一個事件循環之前,都會調用一個flushAppendOnlyFile函數,考慮是否將aof_buf緩衝區中內容寫入和保存進AOF文件中,該函數的行爲有服務器配置文件中的appendfsync選項中的值來決定,各個值的含義如下:

appendfsync選項的值 flushAppendOnlyFile函數的行爲
always 將緩衝區中的所有內容寫入並同步到AOF文件
everysec (默認) 將緩衝區中的所有內容寫入到AOF文件,如果上次同步距離現在時間超過1秒,
那麼再次對AOF文件進行同步,該同步行爲有一個專門負責的線程
no 將緩衝區中的所有內容寫入到AOF文件,但並不進行同步,何時同步由操作系統決定

AOF文件載入與數據還原

因爲AOF文件中記錄了重建數據庫所需要的所有寫命令,所以服務器只要讀入並重新執行一遍AOF文件中保存的寫命令,就可以還原服務器關閉之前的數據庫狀態.

AOF重寫

隨着AOF文件中保存的指令越來越多,文件體積越來越大,這個時候需要對文件進行重寫來縮小文件所佔存儲空間.

AOF文件重寫並不需要對現有AOF文件進行讀取或者分析,是直接通過讀取服務器當前的數據庫狀態實現的,比如當前數據庫存在一個鍵值對,老的AOF文件中可能存儲了對這個鍵值對的各種歷史操作命令,重寫的時候只要讀取該鍵值對最新的狀態將之前的多個指令合併成最後的一條指令保存即可,這就是AOF重寫的原理

有個特殊的點是對於除了字符串之外的另外四種數據類型,如果一個鍵對應的元素個數很多,超過某個配置的值,會使用多條記錄保存命令,而不只是一條.

AOF文件的重寫是通過一個子進程來執行的,使用子進程而不是線程,主要是爲了在避免使用鎖的情況下還能保證數據安全性.由於在子進程執行重寫任務的過程中,可能主進程依然會執行新的命令.Redis設置了一個AOF重寫緩衝區來存儲在這段時間中主進程執行的新的指令.該緩衝區當子進程建立的時候開始使用

因此在子進程進行重寫的過程中,如果主線程接收到新的命令,會將其同時保存在aof_buf緩衝區及AOF重寫緩衝區中;而子進程在完成AOF重寫工作後會給主進程發送一個信號,之後主進程收到信號並調用一個函數,執行以下工作:

  • 將AOF重寫緩衝區中的所有內容寫入到新的AOF文件中,這時新的AOF文件所保存的數據庫狀態將和當前服務器的狀態一致.
  • 對新的AOF文件進行改名,原子地覆蓋現有的AOF文件,完成新舊文件的替換

這兩個動作完成之後,主線程就可以繼續接受新的指令了.

在整個AOF後臺重寫的過程中,只有該過程會對主進程造成阻塞,其他時間都是在後臺進行,不影響主進程執行任務.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章