1. 程式人生 > >Redis資料庫結構和持久化

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

redisDb_key_space

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;

圖解

redisDb_expires

過期鍵的刪除策略

既然鍵會過期,那肯定需要將其移除,避免其一直佔用到記憶體。那麼對於怎麼刪除過期的鍵,這個問題可能存在以下幾種不同的策略,我們先來看一看。

  • 定時刪除: 在設定鍵的過期時間的同時,建立一個定時器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檔案結構

RDB_structure

  • 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號資料庫中的所有鍵值對資料。

RDB_structure_databases

每個非空資料庫database num在RDB檔案都可以儲存為SELECTDB、db_number、key_value_pair三個部分。

RDB_database_Structure

  • SELECTDB: 常量,1位元組,當程式遇到這個值的時候,就知道即將讀入的是一個數據庫號碼。
  • db_num: 儲存著一個數據庫號碼,根據號碼的大小不同,這個部分長度科可以是1個2個或5個位元組。
  • key_value_pair: 儲存著所有的鍵值對。

當讀入資料庫號碼之後,伺服器就會呼叫SELECT命令切換資料庫,使得之後讀入的鍵值對可以載入到正確的資料庫中。

日常貼個圖:

RDB_databases_structure_keyvaluepair

key_value_pair

這個部分儲存著所有的鍵值對,如果鍵值對帶有過期時間,那麼過期時間也會被儲存在內。

不帶過期時間的鍵值對在RDB檔案中由TYPE、key、value三部分組成。

帶有過期時間的鍵值對在RDB檔案中由EXPIRETIME_MS、ms、TYPE、key、value五部分組成。

日常貼個圖。

RDB_database_keyfaluepair")

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_load

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重寫工作後,子程序會向父程序傳送一個訊號,父程序接收到訊號之後,呼叫一個訊號函式處理器,並執行以下工作:

  1. 將AOF重寫緩衝區的所有內容寫入到新AOF檔案中,這是新AOF檔案資料庫狀態和當前一致。
  2. 對新的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檔案