作為web開發的一員,相信大家的面試經歷里少不了會遇到這個問題:redis是怎麼做持久化的? ...
作為web開發的一員,相信大家的面試經歷里少不了會遇到這個問題:redis是怎麼做持久化的?
不急著給出答案,先停下來思考一下,然後再看看下麵的介紹。希望看了這邊文章後,你能夠回答這個問題。
為什麼需要持久化?
由於Redis是一種記憶體型資料庫,即伺服器在運行時,系統為其分配了一部分記憶體存儲數據,一旦伺服器掛了,或者突然宕機了,那麼資料庫裡面的數據將會丟失,為了使伺服器即使突然關機也能保存數據,必須通過持久化的方式將數據從記憶體保存到磁碟中。
對於進行持久化的程式來說,數據從程式寫到電腦的磁碟的流程如下:
1、客戶端發送一個寫指令給資料庫(此時數據在客戶端的記憶體)
2、資料庫接收到寫的指令以及數據(數據此時在服務端的記憶體)
3、資料庫發起一個系統調用,把數據寫到磁碟(此時數據在內核的記憶體)
4、操作系統把數據傳輸到磁碟控制器(數據此時在磁碟緩存中)
5、磁碟控制器執行真正寫入數據到物理媒介的操作(如磁碟)
如果只是考慮資料庫層面,數據在第三階段之後就安全了,在這個時候,系統調用已經發起了,即使資料庫進程奔潰了,系統調用會繼續進行,也能順利將數據寫入到磁碟中。
在這一步之後,在第4步內核會將數據從內核緩存保存到磁碟緩存中,但為了系統的效率問題,預設情況下不會太頻繁地執行這個動作,大概會在30s執行一次,這就意味著如果這一步失敗了或者就在進行這一步的時候伺服器突然關機了,那麼就可能會有30s的數據丟失了,這種比較普通的災難性問題也是需要考慮的。
POSIX API也提供了一個系統調用讓內核強制將緩存數據寫入到磁碟中,比較常見的就是fsync系統調用。
int fsync(int fd);
fsync函數只對由文件描述符fd指定的一個文件起作用,並且等待寫磁碟操作結束後才返回。每次調用fsync時,會初始化一個寫操作,然後把緩衝區的數據寫入到磁碟中。fsync()函數在完成寫操作的時候會阻塞進程,如果其他線程也在寫同一個文件,它也會阻塞其他線程,直到完成寫操作。
持久化
持久化是將程式數據在持久狀態和瞬時狀態間轉換的機制。對於程式來說,程式運行中數據是在記憶體的,如果沒有及時同步寫入到磁碟,那麼一旦斷電或者程式突然奔潰,數據就會丟失了,只有把數據及時同步到磁碟,數據才能永久保存,不會因為宕機影像數據的有效性。而持久化就是將數據從程式同步到磁碟的一個動作過程。
Redis的持久化
redis有RDB和AOF兩種持久化方式。RDB是快照文件的方式,redis通過執行SAVE/BGSAVE命令,執行數據的備份,將redis當前的數據保存到*.rdb
文件中,文件保存了所有的數據集合。AOF是伺服器通過讀取配置,在指定的時間里,追加redis寫操作的命令到*.aof
文件中,是一種增量的持久化方式。
RDB
RDB文件通過SAVE或BGSAVE命令實現。
SAVE命令會阻塞Redis服務進程,直到RDB文件創建完成為止。
BGSAVE命令通過fork子進程,有子進程來進行創建RDB文件,父進程和子進程共用數據段,父進程繼續提供讀寫服務,子進程實現備份功能。BGSAVE階段只有在需要修改共用數據段的時候才進行拷貝,也就是COW(Copy On Write)。SAVE創建RDB文件可以通過設置多個保存條件,只要其中一個條件滿足,就可以在後臺執行SAVE操作。
SAVE和BGSAVE命令的實現代碼如下:
void saveCommand(client *c) {
// BGSAVE執行時不能執行SAVE
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
return;
}
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
// 調用rdbSave函數執行備份(阻塞當前客戶端)
if (rdbSave(server.rdb_filename,rsiptr) == C_OK) {
addReply(c,shared.ok);
} else {
addReply(c,shared.err);
}
}
/*
* BGSAVE 命令實現 [可選參數"schedule"]
*/
void bgsaveCommand(client *c) {
int schedule = 0;
/* 當AOF正在執行時,SCHEDULE參數修改BGSAVE的效果
* BGSAVE會在之後執行,而不是報錯
* 可以理解為:BGSAVE被提上日程
*/
if (c->argc > 1) {
// 參數只能是"schedule"
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) {
schedule = 1;
} else {
addReply(c,shared.syntaxerr);
return;
}
}
// BGSAVE正在執行,不操作
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
} else if (server.aof_child_pid != -1) {
// aof正在執行,如果schedule==1,BGSAVE被提上日程
if (schedule) {
server.rdb_bgsave_scheduled = 1;
addReplyStatus(c,"Background saving scheduled");
} else {
addReplyError(c,
"An AOF log rewriting in progress: can't BGSAVE right now. "
"Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever "
"possible.");
}
} else if (rdbSaveBackground(server.rdb_filename,NULL) == C_OK) {// 否則調用rdbSaveBackground執行備份操作
addReplyStatus(c,"Background saving started");
} else {
addReply(c,shared.err);
}
}
有了RDB文件之後,如果伺服器關機了,或者需要新增一個伺服器,重新啟動資料庫伺服器之後,就可以通過載入RDB文件恢復之前備份的數據。
但是bgsave會耗費較長時間,不夠實時,會導致在停機的時候丟失大量數據。
AOF(Append Only File)
RDB文件保存的是資料庫的鍵值對數據,AOF保存的是資料庫執行的寫命令。
AOF的實現流程有三步:
append->write->fsync
append追加命令到AOF緩衝區,write將緩衝區的內容寫入到程式緩衝區,fsync將程式緩衝區的內容寫入到文件。
當AOF持久化功能處於開啟狀態時,伺服器每執行完一個命令,就會將命令以協議格式追加寫入到redisServer結構體的aof_buf緩衝區,具體的協議這裡不展開闡述。
AOF的持久化發生時期有個配置選項:appendfsync。該選項有三個值:
always:所有內容寫入並同步到aof文件
everysec:將aof_buf緩衝區的內容寫入到AOF文件,如果距離上次同步AOF文件的
no:將aof_buf緩衝區中的所有內容寫入到AOF文件,但並不對AOF文件進行同步,由操作系統決定何時進行同步,一般是預設情況下的30s。
AOF持久化模式每個寫命令都會追加到AOF文件,隨著伺服器不斷運行,AOF文件會越來越大,為了避免AOF產生的文件太大,伺服器會對AOF文件進行重寫,將操作相同key的相同命令合併,從而減少文件的大小。
舉個例子,要保存一個員工的名字、性別等信息:
> hset employee_12345 name "hoohack"
> hset employee_12345 good_at "php"
> hset employee_12345 gender "male"
只是錄入這個哈希鍵的狀態,AOF文件就需要保存三條命令,如果還有其他,比如刪除,或者更新值的操作,那命令將會更多,文件會更大,有了重寫後,就可以適當地減少文件的大小。
AOF重寫的實現原理是先伺服器中的資料庫,然後遍曆數據庫,找出每個資料庫中的所有鍵對象,獲取鍵值對的鍵和值,根據鍵的類型對鍵值對進行重寫。比如上面的例子,可以合併為下麵的一條命令:
> hset employee_12345 name "hoohack" good_at "php" gender "male"。
AOF的重寫會執行大量的寫入操作,Redis是單線程的,所以如果有伺服器直接調用重寫,伺服器就不能處理其他命令了,因此Redis伺服器新起了單獨一個進程來執行AOF重寫。
Redis執行重寫的流程:
在子進程執行AOF重寫時,服務端接收到客戶端的命令之後,先執行客戶端發來的命令,然後將執行後的寫命令追加到AOF緩衝區中,同時將執行後的寫命令追加到AOF重寫緩衝區中。
等到子進程完成了重寫工作後,會發一個完成的信號給伺服器,伺服器就將AOF重寫緩衝區中的所有內容追加到AOF文件中,然後原子性地覆蓋現有的AOF文件。
RDB和AOF的優缺點
RDB持久化方式可以只通過伺服器讀取數據就能載入備份中的文件到程式中,而AOF方式必須創建一個偽客戶端才能執行。
RDB的文件較小,保存了某個時間點之前的數據,適合做災備和主從同步。
RDB備份耗時較長,如果數據量大,在遇到宕機的情況下,可能會丟失部分數據。另外,RDB是通過配置使達到某種條件的時候才執行,如果在這段時間內宕機,那麼這部分數據也會丟失。
AOF方式,在相同數據集的情況下,文件大小會比RDB方式的大。
AOF的持久化方式也是通過配置的不同,預設配置的是每秒同步,最快的模式是同步每一個命令,最壞的方式是等待系統執行fsync將緩衝同步到磁碟文件中,大部分操作系統是30s。通常情況下會配置為每秒同步一次,所以最多會有1s的數據丟失。
怎樣的同步方式更好?
RDB和AOF方式結合。起一個定時任務,每小時備份一份伺服器當前狀態的數據,以日期和小時命名,另外起一個定時任務,定時刪除無效的備份文件(比如48小時之前)。AOF配置為1s一次。這樣一來,最多會丟失1s的數據,同時如果redis發生雪崩,也能迅速恢復為前一天的狀態,不至於停止服務。
總結
Redis的持久化方案也不是一成不變的,紙上的理論還需要結合實踐成果來證明其可行性。
原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。
更多精彩內容,請關註個人公眾號。
參考文章:
http://oldblog.antirez.com/post/redis-persistence-demystified.html
http://blog.httrack.com/blog/2013/11/15/everything-you-always-wanted-to-know-about-fsync/