服務器之家:專注于服務器技術及軟件下載分享
分類導航

Mysql|Sql Server|Oracle|Redis|

服務器之家 - 數據庫 - Redis - 從源碼解讀redis持久化

從源碼解讀redis持久化

2019-11-16 17:12愛編程廚師 Redis

redis的持久化也就是數據落地,對于任何一個數據系統都要考慮是不是需要數據落地。在系統崩潰或是機房掉電等的情況下,將有用的數據記錄在非易失性存儲器上面,防止數據丟失,以及用來系統重啟時的數據恢復。

 

為什么需要持久化?

由于Redis是一種內存型數據庫,即服務器在運行時,系統為其分配了一部分內存存儲數據,一旦服務器掛了,或者突然宕機了,那么數據庫里面的數據將會丟失,為了使服務器即使突然關機也能保存數據,必須通過持久化的方式將數據從內存保存到磁盤中。

對于進行持久化的程序來說,數據從程序寫到計算機的磁盤的流程如下:

1、客戶端發送一個寫指令給數據庫(此時數據在客戶端的內存)

2、數據庫接收到寫的指令以及數據(數據此時在服務端的內存)

3、數據庫發起一個系統調用,把數據寫到磁盤(此時數據在內核的內存)

4、操作系統把數據傳輸到磁盤控制器(數據此時在磁盤緩存中)

5、磁盤控制器執行真正寫入數據到物理媒介的操作(如磁盤)

如果只是考慮數據庫層面,數據在第三階段之后就安全了,在這個時候,系統調用已經發起了,即使數據庫進程奔潰了,系統調用會繼續進行,也能順利將數據寫入到磁盤中。 在這一步之后,在第4步內核會將數據從內核緩存保存到磁盤緩存中,但為了系統的效率問題,默認情況下不會太頻繁地執行這個動作,大概會在30s執行一次,這就意味著如果這一步失敗了或者就在進行這一步的時候服務器突然關機了,那么就可能會有30s的數據丟失了,這種比較普通的災難性問題也是需要考慮的。

POSIX API也提供了一個系統調用讓內核強制將緩存數據寫入到磁盤中,比較常見的就是fsync系統調用。

int fsync(int fd);

fsync函數只對由文件描述符fd指定的一個文件起作用,并且等待寫磁盤操作結束后才返回。每次調用fsync時,會初始化一個寫操作,然后把緩沖區的數據寫入到磁盤中。fsync()函數在完成寫操作的時候會阻塞進程,如果其他線程也在寫同一個文件,它也會阻塞其他線程,直到完成寫操作。

持久化

持久化是將程序數據在持久狀態和瞬時狀態間轉換的機制。對于程序來說,程序運行中數據是在內存的,如果沒有及時同步寫入到磁盤,那么一旦斷電或者程序突然奔潰,數據就會丟失了,只有把數據及時同步到磁盤,數據才能永久保存,不會因為宕機影像數據的有效性。而持久化就是將數據從程序同步到磁盤的一個動作過程。

從源碼解讀redis持久化

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命令的實現代碼如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
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執行重寫的流程:

從源碼解讀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://blog.51cto.com/13902811/2164629

延伸 · 閱讀

精彩推薦
法甲赛程积分