如果説緩存和消息中間件處理的是流量的“流動”問題,那麼數據庫系統要解決的,則是數據的“存在”問題——即數據的最終正確性與持久性。它是整個系統的“真相之源”(Source of Truth)。
日誌技術
在考慮數據庫系統的持久性時,關鍵的考慮因素是如何在數據庫中實現數據的持久化。例如,在關係型數據庫中,數據被存儲在表中,而這些表是通過在文件系統或塊系統中的數據結構來實現的。如何保存和維護這些文件,例如在單一文件中存儲索引或在多個文件中分別存儲索引,這取決於具體的實現方式。
當開始修改或更新表中的數據時,這意味着將開始更改索引。索引需要被更新,同時,存儲在內存中的頁的數據需要被提交。
那麼,什麼是數據庫中的提交操作呢?
每當數據發生更改時,相應的頁會被標記為"髒頁"。在將這些髒頁寫回硬盤時,不僅僅是寫回單個更改的值或幾個字節的列,而是將整個頁寫回。這個頁的大小由硬盤類型決定,比如是最小的頁/塊/扇區大小。髒頁的寫回操作通常是異步進行的,其頻率由操作系統調度。
如果寫入了大量的數據,那麼將數據寫回硬盤也需要相當長的時間。這裏的成本並不在於將數據寫回硬盤所需的時間,而在於如果數據庫崩潰,數據沒有成功寫回硬盤,或者只寫回了一部分,那麼此時應該如何遵循並堅持ACID事務的原則。

預寫日誌
為了解決這個問題,可以在數據發生更改時,同時生成一個日誌或記錄,並將其作為所有更改的真實來源。這種日誌被稱為預寫日誌(Write Ahead Log,WAL),它記錄了頁何時以及如何發生的更改。有了預寫日誌,那麼就可以在內存中保留髒頁,因為在提交操作之前,硬盤上已經有一個記錄了這些更改的日誌。因此,即使頁的提交操作失敗,也可以通過回放預寫日誌來恢復頁的數據寫入和更改操作。

假設需要設計一個鍵值(Key-Value)類型的內存查詢系統,其中每次更新操作只更新一小部分數據(例如某一個鍵的值)。現在打算利用日誌技術來實現該內存查詢系統的宕機恢復。與數據庫事務不同的是,在這個問題模型中,每個成功的更新操作都會立即生效。這相當於在數據庫中,每個事務只包含一個更新操作,並且每次更新操作都會立即提交(Auto commit)。
現在可以按照以下步驟使用預寫日誌:
1)將更新操作的結果(例如SET K=V,,則記錄操作類型SET以及K和V的值)以追加寫(Append)的方式寫入硬盤的日誌文件;
2)根據更新操作修改內存中的數據;
3)返回更新成功的消息。
從預寫日誌的寫入流程可以看出,預寫的日誌記錄了更新操作本身,包括操作類型、涉及的數據等。由於是順序追加寫日誌文件,這樣可以保證較高的硬盤I/O性能。
使用預寫日誌進行宕機恢復非常簡單,只需要“回放”日誌即可。從頭讀取日誌文件中的每次更新操作的結果,用這些結果修改內存中的數據。
從預寫日誌的宕機恢復流程可以看出,只有寫入日誌文件的更新結果才能在宕機後恢復。這也解釋了為什麼在預寫日誌流程中需要先更新日誌文件再更新內存中的數據。如果先更新內存中的數據,那麼用户可能立刻就能讀到更新後的數據。如果在完成內存修改與寫入日誌之間發生宕機,那麼最後一次更新操作可能無法恢復,但用户可能已經讀取到了更新後的數據,這可能導致數據不一致的問題。
檢查點
宕機恢復過程的一個挑戰是需要回放所有預寫日誌。如果需要恢復的操作數量巨大,那麼這個恢復過程可能會非常耗時。為了解決這個問題,可以引入檢查點(Check Point)技術。
檢查點執行的過程,涉及設計一種支持快照(Snapshot)的內存數據結構,可以快速的將內存數據生成快照,然後寫入檢查點日誌再持久化到硬盤。
1)向日志文件中記錄“Begin Check Point”;
2)將內存中的數據生成快照再持久化到硬盤;
3)向日志文件中記錄“End Check Point”。
在執行檢查點過程中,數據可以繼續按照預寫日誌方式被更新。
基於檢查點的宕機恢復流程
1)將硬盤中的快照數據加載回內存;
2)從後向前掃描日誌文件,尋找最後一個“End Check Point”日誌;
3)從最後一個“End Check Point”日誌向前找到最近的一個“Begin Check Point”日誌,並回放該日誌之後的所有更新操作日誌。
快照生成過程,通常採用寫時複製(Write-once, copy-on-write)技術。例如,創建一個子進程來執行這些操作時,不立即複製父進程中的內存數據,而是讓父子進程共享同一份內存。只有當父進程需要修改某個內存頁時,才會複製這個頁,然後在複製後的頁上進行修改。這樣,只有真正需要修改的內存頁才會被複制,減少了內存的使用。
未完待續
很高興與你相遇!如果你喜歡本文內容,記得關注哦!!!