試想一下,若是 Redis 每執行一條寫操做命令,就把該命令以追加的方式寫入到一個文件裏,而後重啓 Redis 的時候,先去讀取這個文件裏的命令,而且執行它,這不就至關於恢復了緩存數據了嗎?redis
這種保存寫操做命令到日誌的持久化方式,就是 Redis 裏的 AOF(Append Only File) 持久化功能,注意只會記錄寫操做命令,讀操做命令是不會被記錄的,由於沒意義。數據庫
在 Redis 中 AOF 持久化功能默認是不開啓的,須要咱們修改 redis.conf
配置文件中的如下參數:緩存
AOF 日誌文件其實就是普通的文本,咱們能夠經過 cat
命令查看裏面的內容,不過裏面的內容若是不知道必定的規則的話,可能會看不懂。安全
我這裏以「set name xiaolin」命令做爲例子,Redis 執行了這條命令後,記錄在 AOF 日誌裏的內容以下圖:服務器
我這裏給你們解釋下。數據結構
「*3
」表示當前命令有三個部分,每部分都是以「$+數字
」開頭,後面緊跟着具體的命令、鍵或值。而後,這裏的「數字
」表示這部分中的命令、鍵或值一共有多少字節。例如,「$3 set
」表示這部分有 3 個字節,也就是「set
」命令這個字符串的長度。多線程
不知道你們注意到沒有,Redis 是先執行寫操做命令後,纔將該命令記錄到 AOF 日誌裏的,這麼作其實有兩個好處。app
第一個好處,避免額外的檢查開銷。異步
由於若是先將寫操做命令記錄到 AOF 日誌裏,再執行該命令的話,若是當前的命令語法有問題,那麼若是不進行命令語法檢查,該錯誤的命令記錄到 AOF 日誌裏後,Redis 在使用日誌恢復數據時,就可能會出錯。函數
而若是先執行寫操做命令再記錄日誌的話,只有在該命令執行成功後,纔將命令記錄到 AOF 日誌裏,這樣就不用額外的檢查開銷,保證記錄在 AOF 日誌裏的命令都是可執行而且正確的。
第二個好處,不會阻塞當前寫操做命令的執行,由於當寫操做命令執行成功後,纔會將命令記錄到 AOF 日誌。
固然,AOF 持久化功能也不是沒有潛在風險。
第一個風險,執行寫操做命令和記錄日誌是兩個過程,那當 Redis 在還沒來得及將命令寫入到硬盤時,服務器發生宕機了,這個數據就會有丟失的風險。
第二個風險,前面說道,因爲寫操做命令執行成功後才記錄到 AOF 日誌,因此不會阻塞當前寫操做命令的執行,可是可能會給「下一個」命令帶來阻塞風險。
由於將命令寫入到日誌的這個操做也是在主進程完成的(執行命令也是在主進程),也就是說這兩個操做是同步的。
若是在將日誌內容寫入到硬盤時,服務器的硬盤的 I/O 壓力太大,就會致使寫硬盤的速度很慢,進而阻塞住了,也就會致使後續的命令沒法執行。
認真分析一下,其實這兩個風險都有一個共性,都跟「 AOF 日誌寫回硬盤的時機」有關。
Redis 寫入 AOF 日誌的過程,以下圖:
我先來具體說說:
server.aof_buf
緩衝區;Redis 提供了 3 種寫回硬盤的策略,控制的就是上面說的第三步的過程。
在 redis.conf
配置文件中的 appendfsync
配置項能夠有如下 3 種參數可填:
這 3 種寫回策略都沒法能完美解決「主進程阻塞」和「減小數據丟失」的問題,由於兩個問題是對立的,偏向於一邊的話,就會要犧牲另一邊,緣由以下:
你們根據本身的業務場景進行選擇:
我也把這 3 個寫回策略的優缺點總結成了一張表格:
你們知道這三種策略是怎麼實現的嗎?
深刻到源碼後,你就會發現這三種策略只是在控制 fsync()
函數的調用時機。
當應用程序向文件寫入數據時,內核一般先將數據複製到內核緩衝區中,而後排入隊列,而後由內核決定什麼時候寫入硬盤。
若是想要應用程序向文件寫入數據後,能立馬將數據同步到硬盤,就能夠調用 fsync()
函數,這樣內核就會將內核緩衝區的數據直接寫入到硬盤,等到硬盤寫操做完成後,該函數纔會返回。
AOF 日誌是一個文件,隨着執行的寫操做命令愈來愈多,文件的大小會愈來愈大。
若是當 AOF 日誌文件過大就會帶來性能問題,好比重啓 Redis 後,須要讀 AOF 文件的內容以恢復數據,若是文件過大,整個恢復的過程就會很慢。
因此,Redis 爲了不 AOF 文件越寫越大,提供了 AOF 重寫機制,當 AOF 文件的大小超過所設定的閾值後,Redis 就會啓用 AOF 重寫機制,來壓縮 AOF 文件。
AOF 重寫機制是在重寫時,讀取當前數據庫中的全部鍵值對,而後將每個鍵值對用一條命令記錄到「新的 AOF 文件」,等到所有記錄完後,就將新的 AOF 文件替換掉現有的 AOF 文件。
舉個例子,在沒有使用重寫機制前,假設先後執行了「set name xiaolin」和「set name xiaolincoding」這兩個命令的話,就會將這兩個命令記錄到 AOF 文件。
可是在使用重寫機制後,就會讀取 name 最新的 value(鍵值對) ,而後用一條 「set name xiaolincoding」命令記錄到新的 AOF 文件,以前的第一個命令就沒有必要記錄了,由於它屬於「歷史」命令,沒有做用了。這樣一來,一個鍵值對在重寫日誌中只用一條命令就好了。
重寫工做完成後,就會將新的 AOF 文件覆蓋現有的 AOF 文件,這就至關於壓縮了 AOF 文件,使得 AOF 文件體積變小了。
而後,在經過 AOF 日誌恢復數據時,只用執行這條命令,就能夠直接完成這個鍵值對的寫入了。
因此,重寫機制的妙處在於,儘管某個鍵值對被多條寫命令反覆修改,最終也只須要根據這個「鍵值對」當前的最新狀態,而後用一條命令去記錄鍵值對,代替以前記錄這個鍵值對的多條命令,這樣就減小了 AOF 文件中的命令數量。最後在重寫工做完成後,將新的 AOF 文件覆蓋現有的 AOF 文件。
這裏說一下爲何重寫 AOF 的時候,不直接複用現有的 AOF 文件,而是先寫到新的 AOF 文件再覆蓋過去。
由於若是 AOF 重寫過程當中失敗了,現有的 AOF 文件就會形成污染,可能沒法用於恢復使用。
因此 AOF 重寫過程,先重寫到新的 AOF 文件,重寫失敗的話,就直接刪除這個文件就好,不會對現有的 AOF 文件形成影響。
寫入 AOF 日誌的操做雖然是在主進程完成的,由於它寫入的內容很少,因此通常不太影響命令的操做。
可是在觸發 AOF 重寫時,好比當 AOF 文件大於 64M 時,就會對 AOF 文件進行重寫,這時是須要讀取全部緩存的鍵值對數據,併爲每一個鍵值對生成一條命令,而後將其寫入到新的 AOF 文件,重寫完後,就把如今的 AOF 文件替換掉。
這個過程實際上是很耗時的,因此重寫的操做不能放在主進程裏。
因此,Redis 的重寫 AOF 過程是由後臺子進程 bgrewriteaof 來完成的,這麼作能夠達到兩個好處:
子進程是怎麼擁有主進程同樣的數據副本的呢?
主進程在經過 fork
系統調用生成 bgrewriteaof 子進程時,操做系統會把主進程的「頁表」複製一份給子進程,這個頁表記錄着虛擬地址和物理地址映射關係,而不會複製物理內存,也就是說,二者的虛擬空間不一樣,但其對應的物理空間是同一個。
這樣一來,子進程就共享了父進程的物理內存數據了,這樣可以節約物理內存資源,頁表對應的頁表項的屬性會標記該物理內存的權限爲只讀。
不過,當父進程或者子進程在向這個內存發起寫操做時,CPU 就會觸發缺頁中斷,這個缺頁中斷是因爲違反權限致使的,而後操做系統會在「缺頁異常處理函數」裏進行物理內存的複製,並從新設置其內存映射關係,將父子進程的內存讀寫權限設置爲可讀寫,最後纔會對內存進行寫操做,這個過程被稱爲「寫時複製(Copy On Write)」。
寫時複製顧名思義,在發生寫操做的時候,操做系統纔會去複製物理內存,這樣是爲了防止 fork 建立子進程時,因爲物理內存數據的複製時間過長而致使父進程長時間阻塞的問題。
固然,操做系統複製父進程頁表的時候,父進程也是阻塞中的,不過頁表的大小相比實際的物理內存小不少,因此一般複製頁表的過程是比較快的。
不過,若是父進程的內存數據很是大,那天然頁表也會很大,這時父進程在經過 fork 建立子進程的時候,阻塞的時間也越久。
因此,有兩個階段會致使阻塞父進程:
觸發重寫機制後,主進程就會建立重寫 AOF 的子進程,此時父子進程共享物理內存,重寫子進程只會對這個內存進行只讀,重寫 AOF 子進程會讀取數據庫裏的全部數據,並逐一把內存數據的鍵值對轉換成一條命令,再將命令記錄到重寫日誌(新的 AOF 文件)。
可是子進程重寫過程當中,主進程依然能夠正常處理命令。
若是此時主進程修改了已經存在 key-value,就會發生寫時複製,注意這裏只會複製主進程修改的物理內存數據,沒修改物理內存仍是與子進程共享的。
因此若是這個階段修改的是一個 bigkey,也就是數據量比較大的 key-value 的時候,這時複製的物理內存數據的過程就會比較耗時,有阻塞主進程的風險。
還有個問題,重寫 AOF 日誌過程當中,若是主進程修改了已經存在 key-value,此時這個 key-value 數據在子進程的內存數據就跟主進程的內存數據不一致了,這時要怎麼辦呢?
爲了解決這種數據不一致問題,Redis 設置了一個 AOF 重寫緩衝區,這個緩衝區在建立 bgrewriteaof 子進程以後開始使用。
在重寫 AOF 期間,當 Redis 執行完一個寫命令以後,它會同時將這個寫命令寫入到 「AOF 緩衝區」和 「AOF 重寫緩衝區」。
也就是說,在 bgrewriteaof 子進程執行 AOF 重寫期間,主進程須要執行如下三個工做:
當子進程完成 AOF 重寫工做(掃描數據庫中全部數據,逐一把內存數據的鍵值對轉換成一條命令,再將命令記錄到重寫日誌)後,會向主進程發送一條信號,信號是進程間通信的一種方式,且是異步的。
主進程收到該信號後,會調用一個信號處理函數,該函數主要作如下工做:
信號函數執行完後,主進程就能夠繼續像往常同樣處理命令了。
在整個 AOF 後臺重寫過程當中,除了發生寫時複製會對主進程形成阻塞,還有信號處理函數執行時也會對主進程形成阻塞,在其餘時候,AOF 後臺重寫都不會阻塞主進程。
此次小林給你們介紹了 Redis 持久化技術中的 AOF 方法,這個方法是每執行一條寫操做命令,就將該命令以追加的方式寫入到 AOF 文件,而後在恢復時,以逐一執行命令的方式來進行數據恢復。
Redis 提供了三種將 AOF 日誌寫回硬盤的策略,分別是 Always、Everysec 和 No,這三種策略在可靠性上是從高到低,而在性能上則是從低到高。
隨着執行的命令越多,AOF 文件的體積天然也會愈來愈大,爲了不日誌文件過大, Redis 提供了 AOF 重寫機制,它會直接掃描數據中全部的鍵值對數據,而後爲每個鍵值對生成一條寫操做命令,接着將該命令寫入到新的 AOF 文件,重寫完成後,就替換掉現有的 AOF 日誌。重寫的過程是由後臺子進程完成的,這樣可使得主進程能夠繼續正常處理命令。
用 AOF 日誌的方式來恢復數據實際上是很慢的,由於 Redis 執行命令由單線程負責的,而 AOF 日誌恢復數據的方式是順序執行日誌裏的每一條命令,若是 AOF 日誌很大,這個「重放」的過程就會很慢了。