Redis資料庫結構和持久化
Redis資料庫,持久化
資料庫
Redis伺服器將所有的資料庫都儲存在伺服器狀態redis.h/redisServer結構的db陣列中,每個專案都是一個redis.h/redisDb結構,每個redisDb結構代表一個數據庫。
struct redisServer {
//...
// 一個數組,儲存這伺服器中的所有資料庫
redisDb *db;
// 伺服器資料庫數量
int dbnum;
// ...
}
伺服器初始化的時候,程式會根據伺服器狀態的dbnum屬性來決定建立多少個數據庫,這個屬性值由redis.conf中配置項database選項決定,值預設是16。
切換資料庫使用select +資料庫號來切換,預設是0號資料庫。例如:select 9。
在伺服器內部,客戶端狀態redisClient結構的db屬性記錄了客戶端當前的目標資料庫,這是個指向redisDb結構的指標。
typedef struct redisClient {
// ...
// 記錄客戶端當前正在使用的資料庫
redisDb *db;
// ...
} redisClient;
鍵空間
Redis是一個key-value資料庫伺服器,而每個資料庫都有redis.h/redisDb結構表示,其中dict字典(hash表)儲存了資料庫中所有的鍵值對,我們將這個字典成為鍵空間(key space)
typedef struct redisDb {
// ...
// 資料庫鍵空間,儲存資料庫中的所有鍵值對
dict *dict;
// ...
} redisDb;
鍵空間和使用者所見的資料庫是直接對應的:
鍵空間的鍵也就是資料庫的鍵,每一個鍵都是一個字串物件。
鍵空間的值也就是資料庫的值,每個值都可以是字串物件,列表物件,hash表物件,集合物件和有序物件中的其中一種。
圖解:
redis> SET message "hello world" OK redis> RPUSH alphabet a b c (integer) 3 redis> HMSET book name "Redis Book" author "xxx" publisher "yyy" (integer) 3
EXPIRE
- SETEX: 這個命令只能用於字串物件,在設定值的時候一併設定過期時間。
- EXPIRE key ttl: 用於設定key存活時間ttl秒
- PEXPIRE key ttl: 用於設定key存活時間ttl毫秒
- EXPIREAT key timestamp: 設定key在timestamp所指定的秒數時間戳過期
- PEXPIREAT key timestamp: 設定key在timestamp所指定的毫秒數時間戳過期
命令有多種,但是最終的執行在底層都會全部轉換成PEXPIRE命令,即設定key在多少時間戳的時候過期。
過期時間並沒有直接維護在鍵空間dict字典中,想想,鍵過期之後,我們肯定需要清理記憶體,這個時候如果一個一個全部遍歷所有的鍵,效率就太低了,因此redis採用了新的字典來儲存過期時間。redisDb結構的expires字典儲存了資料庫中所有鍵的過期時間。同樣也是dict(hash表)結構
typedef struct redisDb {
// ...
// 過期字典,儲存著鍵的過期時間
dict *expires;
// ...
} redisDb;
圖解
過期鍵的刪除策略
既然鍵會過期,那肯定需要將其移除,避免其一直佔用到記憶體。那麼對於怎麼刪除過期的鍵,這個問題可能存在以下幾種不同的策略,我們先來看一看。
- 定時刪除: 在設定鍵的過期時間的同時,建立一個定時器timer,讓定時器在鍵的過期時間來臨時,立即執行對鍵的刪除操作。
- 惰性刪除: 放任過期的鍵不管,但是每次從鍵空間中獲取鍵時,都檢查取得的鍵是否過期,如果過期的話就刪除,如果沒有過期就返回。
- 定期刪除: 每隔一段時間,程式就對資料庫進行一次檢查,刪除裡面的過期鍵。至於刪除多少過期鍵,以及要檢查多少個數據庫,由演算法來決定。
定時刪除
定時刪除通過使用定時器,該策略可以保證過期鍵會盡可能快的被刪除,並釋放過期鍵所佔用的記憶體。
但是缺點是它對CPU時間是最不友好的,在過期鍵比較多的情況下,刪除鍵這一行為可能會佔用相當一部分CPU時間,在記憶體不緊張但是CPU時間很緊張的情況下,將CPU時間用在刪除和當前任務無關的過期鍵上,無疑是種浪費。
除此之外,建立一個定時器需要用到Redis伺服器中的時間事件,而當前時間事件的實現方式是無需連結串列,因此並不能高效的處理大量的時間事件,而且還需要建立大量的定時器。
當過期鍵過多的時候,這種方式有點不太現實。
惰性刪除
惰性刪除策略對CPU時間來說是最又好的,程式只會在取出鍵時才對鍵進行過期檢查,這可以保證刪除過期鍵的操作只會在非做不可的情況下進行,並且刪除的目標僅限於當前鍵,該策略不會在刪除其他無關的過期鍵上話費任何CPU時間。
但是這種方式也有缺點,就是對記憶體是最不友好的。想想如果一個鍵已經過期,而這個鍵又仍然儲存在資料庫中,那麼只要我們不訪問這個鍵,那麼這個鍵就永遠不會被刪除,它所佔用的記憶體就不會釋放(預防槓精,修正一下,down機等事故不算在內)。
使用此種策略時,如果資料庫中存在非常多的過期鍵,而這些件又恰好沒有被訪問到,這時redis就呵呵了吧~~
定期刪除
從上面定時刪除和惰性刪除來看,這兩種方式在單一使用時都有明顯的缺陷。
定時刪除佔用太多CPU時間,影響伺服器的響應時間和吞吐量。
多想刪除浪費太多記憶體,有記憶體洩漏的危險。
定期刪除策略當然就是為了折中這倆東西,這種策略採用每隔一段時間執行一次刪除過期鍵的操作,並限制刪除操作執行的時長和頻率來減少刪除對CPU時間的影響。通過定期刪除,有效的見啥過期鍵帶來的記憶體浪費。
那麼問題來了,每次執行時長多少合適,頻率多少合適呢?如果執行的太頻繁或者時間太長,是不是又退化成了定時刪除。如果執行的太少,或者時間太短,那麼也會出現記憶體浪費的情況。
因此這種情況,就需要很有經驗的大佬來根據情況指定。接下來說說Redis的刪除策略。
Redis的過期鍵刪除策略
Redis伺服器在實際使用的是惰性刪除和定期刪除兩種策略,通過配合使用,伺服器可以很好的在合理使用CPU時間和避免浪費記憶體空間之間取得平衡。
其實就是在定期刪除實現的同時,在取值的時候也加上過期驗證而已,很好理解吧。
過期鍵對RDB、AOF、複製的影響
因為採用了定期刪除的策略,因此肯定存在過期了但是還來不及刪除的情況,這種情況對於Redis的持久化和主從複製有什麼影響呢?呵呵,TM的當然沒有影響,有影響就是bug了。
RDB
如果開啟了RDB功能
- 如果是master,載入RDB檔案時程式會對檔案儲存的鍵進行檢查,只載入未過期的鍵。
- 如果是slave: 載入所有,但是不影響,因為主從伺服器在進行同步的時候,從伺服器的資料庫就會被清空。
AOF
AOF模式持久化執行時,如果是資料庫某個鍵已經過期,對AOF不會產生影響,AOF一樣會將此鍵記錄,當鍵被刪除的時候,程式會向AOF檔案追加一條DEL命令,來顯示的記錄該鍵已被刪除。
AOF重寫過程中,和生成RDB檔案類似,過期的鍵不會儲存到重寫後的AOF檔案中。
複製
當執行在複製模式下時:
- 主伺服器在刪除一個過期鍵之後,會顯式的向所有從伺服器傳送一個DEL命令,告知從伺服器刪除這個過期鍵。
- 從伺服器再執行客戶端傳送的讀命令時,即使碰到過期鍵也不會將其刪除,當然如果過期了,由於會判斷,因此也不會向客戶端返回。
- 從伺服器只有在接到主服務發來的DEL命令之後,才會刪除過期鍵。
通過由主伺服器來控制從伺服器統一地刪除過期鍵,可以保證主從伺服器的資料一致性。
RDB持久化
生成RDB檔案有兩個命令。
SAVE: 阻塞伺服器程序,阻塞期間不能處理任何命令請求,直到RDB檔案建立完畢。
BGSAVE: 派生子程序,由子程序負責建立RDB檔案,主程序繼續處理命令請求。
RDB檔案的載入是在伺服器啟動的時候自動執行的,所有Redis並沒有專門用於載入RDB檔案的命令,只要伺服器再啟動的時候檢測到RDB檔案存在,就會自動載入RDB檔案(AOF未開啟的情況下)。
看日誌:
32188:M 25 Jun 2019 18:34:01.999 # Server initialized
32188:M 25 Jun 2019 18:34:01.999 * DB loaded from disk: 0.000 seconds
32188:M 25 Jun 2019 18:34:01.999 * Ready to accept connections
自動間隔儲存
Redis允許使用者通過配置伺服器的save選項,讓伺服器每隔一點時間自動執行一次BGSAVE命令。
使用者可以設定多個儲存條件,只要其中任一滿足,就會執行BGSAVE命令。
舉例:
# 在900秒之內,對資料庫至少進行了1次修改
save 900 1
# 在300秒之內,對資料庫至少進行了10次修改
save 300 10
# 在60秒之內,對資料庫至少進行了10000次修改
save 60 10000
伺服器redisServer維護了dirty和lastsave屬性,用來儲存修改計數和上一次執行儲存的時間。
struct redisServer {
// ...
// 修改計數器
long long dirty;
// 上一次執行儲存的時間
time_t lastsave;
// ...
}
每次伺服器成功執行一個修改命令之後,程式就會讀dirty計數器進行更新。
為了檢查儲存條件是否滿足,Redis的伺服器週期性操作函式serverCron預設每隔100毫秒就會執行一次,該函式用於對正在執行的伺服器進行維護,他的其中一項工作就是檢查所設定的儲存條件是否滿足,如果滿足,就執行BGSAVE命令。
RDB檔案結構
- REDIS: 這部分長5個位元組,儲存著“REDIS” 5個字元,通過這5個字元,程式可以在載入檔案時,快速檢查所載入檔案是否是RDB檔案。
- db_version: 長4個位元組,它的值是一個字串表示的整數,記錄了RDB的版本號。
- database: 這部分包含著0個或任意多個數據庫以及各個資料庫中的鍵值對資料,如果資料庫為空,那麼這個部分也為空,長度為0位元組。
- EOF: 一個常量,長度為1位元組,這個常量標誌著RDB檔案正文內容結束。
- check_sum: 一個8位元組無符號整數,儲存著一個校驗和,通過對之前4個部分的內容進行計算得出的,用啦檢查RDB檔案是否有出錯或者損壞的情況出現。
databases部分
一個RDB檔案的databases部分可以儲存任意多個非空資料庫。
比如0號和3號資料庫非空,那麼將建立如下一RDB檔案,database 0代表0好資料庫中的所有鍵值對資料,database 3代表3號資料庫中的所有鍵值對資料。
每個非空資料庫database num在RDB檔案都可以儲存為SELECTDB、db_number、key_value_pair三個部分。
- SELECTDB: 常量,1位元組,當程式遇到這個值的時候,就知道即將讀入的是一個數據庫號碼。
- db_num: 儲存著一個數據庫號碼,根據號碼的大小不同,這個部分長度科可以是1個2個或5個位元組。
- key_value_pair: 儲存著所有的鍵值對。
當讀入資料庫號碼之後,伺服器就會呼叫SELECT命令切換資料庫,使得之後讀入的鍵值對可以載入到正確的資料庫中。
日常貼個圖:
key_value_pair
這個部分儲存著所有的鍵值對,如果鍵值對帶有過期時間,那麼過期時間也會被儲存在內。
不帶過期時間的鍵值對在RDB檔案中由TYPE、key、value三部分組成。
帶有過期時間的鍵值對在RDB檔案中由EXPIRETIME_MS、ms、TYPE、key、value五部分組成。
日常貼個圖。
")
TYPE表明了當前key的型別(比如REDIS_RDB_TYPE_SET),決定了接下來redis如何讀入和解釋value資料。
key總是一個字串物件。
value會根據TYPE型別的不同以及儲存內容長度的不同而有所不同,這部分就不一一解釋了,喜歡的自己去百度吧。
AOF持久化
AOF持久化功能的實現可以分為追加(append)、檔案寫入、檔案同步(sync)三個步驟。
append
當AOF持久化功能開啟時,伺服器再執行完一個寫命令之後,會以協議的格式將其追加到伺服器狀態的aof_buf緩衝區末尾。
struct redisServer {
// ...
// AOF緩衝區
sds aof_buf;
// ...
}
比如:
redis> set mKey mValue
OK
那麼伺服器知性溫婉之後會將其轉成協議的格式,追加到aof_buf緩衝區末尾
協議格式
*3\r\n$3\r\nSET\r\n$4\r\nmKey\r\n$6\r\nmValue\r\n
檔案寫入和sync
Rdis的伺服器程序就是一個事件迴圈,這個迴圈中的檔案事件負責接收客戶端的命令請求,以及向客戶端傳送命令回覆,而時間事件則負責執行想serverCron函式這樣需要定時執行的函式。
因為伺服器在處理檔案事件時可能會執行寫命令,使得這些命令內容被追加到aof_buf緩衝區,所有在伺服器每次結束一個事件迴圈之前,他都會呼叫相應函式(flushAppendOnlyFile),考慮是否需要將aof_buf緩衝區的內容寫入和儲存到AOF檔案中。
flushAppendOnlyFile函式的行為有伺服器配置appendfsync選項的值來決定。
appendfsync值 | 函式行為 |
---|---|
always | 將aof_buf緩衝區的所有內容寫入並同步到AOF檔案 |
everysec | 【系統預設值】將aof_buf緩衝區中的內容寫入到AOF檔案,如果上次同步AOF檔案的時間已經超過一秒鐘,那麼再次對AOF檔案進行同步,並且這個同步操作是由一個執行緒專門負責執行的 |
no | 將aof_buf緩衝區中的所有內容寫入到AOF檔案,單並不對AOF檔案進行同步,合適同步由作業系統來決定 |
載入
AOF的優先順序比RDB高哦!
因為AOF檔案裡面包含了重建資料庫狀態所需的所有寫命令,所以伺服器只要讀入並重新執行一遍AOF檔案裡面儲存的寫命令,就可以還原伺服器狀態了。因為redis的命令只能在客戶端上下文中執行,所以這裡需要使用一個偽客戶端(fake client)。
流程圖:
AOF重寫
因為AOF持久化是通過儲存被執行的寫命令來記錄資料庫狀態的,隨著伺服器執行時間的增加,AOF檔案中的內容會越來越多,檔案的體積也會越來越大,還原所需的時間也就越多。
比如說一些過期的鍵,在最開始會被寫入,然後後續又會被DEL,又或者我們使用了很多條RPUSH命令來操作一個key的列表資料,等等之類的情況。
這個時候就需要重寫AOF檔案,使用新的檔案替換掉舊的檔案。檔案的重寫不需要對現有的AOF檔案進行任何讀取分析等操作,而是根據當前資料庫狀態來實現的。
當然,如果列表,hash表,集合。,有序集合這四總可能會帶有多個元素的鍵時,會先檢查數量,如果過多的話,是會分成多條命令來記錄的,而不是單單使用一條命令,REDIS_AOF_REWRITE_ITEMS_PRE_CMD常量的值(64)決定了這個數量,也就是說每條指令最多將寫入64個元素,剩下的將繼續判斷並決定用幾條指令來寫入
後臺重寫
AOF的重寫也是有子程式執行,這樣主程式可以繼續執行命令請求。這個時候伺服器接收的新的命令也有可能對資料庫進行了修改,從而導致當前資料庫狀態和重寫後的AOF檔案所儲存的資料庫狀態不一致。
因此Redis伺服器設定了一個AOF重寫緩衝區,這個緩衝區在伺服器建立子程序之後開始使用,當執行完一個寫命令之後,它會同時將這個寫命令傳送給AOF緩衝區和AOF重寫緩衝區。
這樣一來AOF緩衝區的內容會定期被寫入和同步到AOF檔案,對現有的AOF檔案的處理工作照常進行。
當完成AOF重寫工作後,子程序會向父程序傳送一個訊號,父程序接收到訊號之後,呼叫一個訊號函式處理器,並執行以下工作:
- 將AOF重寫緩衝區的所有內容寫入到新AOF檔案中,這是新AOF檔案資料庫狀態和當前一致。
- 對新的AOF檔案進行重新命名,原子的(atomic)覆蓋現有的AOF檔案,完成新舊AOF檔案的替換。
日常流程:
時間 | 伺服器程序(父程序) | 子程序 |
---|---|---|
T1 | SET k1 v1 | |
T2 | SET k1 v2 | |
T3 | SET k1 v3 | |
T4 | 建立子程序,執行AOF檔案重寫 | 開始AOF檔案重寫 |
T5 | SET k2 v2 | 執行重寫操作 |
T6 | SET k3 v3 | 執行重寫操作 |
T7 | SET k4 v4 | 完成AOF重寫,向父程序傳送訊號 |
T8 | 接收到子程序發來的訊號,將命令SET k2 v2,SET k3 v3,SET k4 v4追加到新的AOF檔案末尾 | |
T9 | 用新AOF檔案覆蓋舊AOF檔案 |