SQLite 實現原子提交和回滾的預設方法是回滾日誌。從3.7.0 版(2010-07-21)開始,可以使用新的「預寫式日誌」選項(以下簡稱為「WAL」)。
使用 WAL 取代回滾日誌有優點也有缺點。優點包括:
但也有一些缺點:
傳統的回滾日誌的工作方式是將未更改的原始資料庫內容的副本寫入單獨的回滾日誌檔案,然後將更改直接寫入資料庫檔案。如果發生當機或 ROLLBACK (回滾),則回滾日誌中包含的原始內容會被寫回資料庫檔案,以將資料庫檔案恢復到其原始狀態。COMMIT (提交) 發生在回滾日誌被刪除時。
WAL 方法反轉了此流程。原始內容保留在資料庫檔案中,而更改則附加到單獨的 WAL 檔案中。當指示提交的特殊記錄附加到 WAL 時,即發生 COMMIT。因此,COMMIT 可以在不寫入原始資料庫的情況下發生,這允許讀取器在將更改同時提交到 WAL 的同時繼續從原始未更改的資料庫進行操作。多個交易可以附加到單個 WAL 檔案的末尾。
當然,最終需要將所有附加到 WAL 檔案中的交易傳輸回原始資料庫。將 WAL 檔案交易移回資料庫的動作稱為「檢查點」。
理解回滾和預寫式日誌之間差異的另一種方法是,在回滾日誌方法中,有兩個基本操作:讀取和寫入,而使用預寫式日誌則有三種基本操作:讀取、寫入和檢查點。
預設情況下,當 WAL 檔案達到 1000 頁的閾值大小時,SQLite 會自動執行檢查點。(可以使用 SQLITE_DEFAULT_WAL_AUTOCHECKPOINT 編譯時選項來指定不同的預設值。)使用 WAL 的應用程式無需執行任何操作即可執行這些檢查點。但是,如果需要,應用程式可以調整自動檢查點閾值。或者,他們可以關閉自動檢查點,並在閒置時間或在單獨的執行緒或程序中執行檢查點。
當在 WAL 模式資料庫上開始讀取操作時,它會先記住 WAL 中最後一個有效提交記錄的位置。將此點稱為「結束標記」。由於 WAL 可以在各種讀取器連接到資料庫時增長並添加新的提交記錄,因此每個讀取器可能都有其自己的結束標記。但是,對於任何特定的讀取器,結束標記在交易期間保持不變,從而確保單個讀取交易只看到資料庫在單個時間點存在的內容。
當讀取器需要內容頁面時,它會先檢查 WAL 以查看該頁面是否出現在其中,如果出現,則提取在讀取器的結束標記之前出現在 WAL 中的頁面的最後一個副本。如果在讀取器的結束標記之前 WAL 中不存在頁面的副本,則從原始資料庫檔案讀取該頁面。讀取器可以存在於不同的程序中,因此為了避免強制每個讀取器掃描整個 WAL 來查找頁面(WAL 檔案的大小可以增長到數 MB,具體取決於檢查點執行的頻率),在共享記憶體中維護一個稱為「wal-index」的資料結構,以幫助讀取器快速且以最少的 I/O 定位 WAL 中的頁面。wal-index 大大提高了讀取器的效能,但使用共享記憶體意味著所有讀取器都必須存在於同一台機器上。這就是預寫式日誌實作無法在網路檔案系統上運作的原因。
寫入器僅僅將新的內容附加到 WAL 檔案的末尾。因為寫入器的操作不會干擾讀取器的動作,所以寫入器和讀取器可以同時運行。然而,由於只有一個 WAL 檔案,因此同一時間只能有一個寫入器。
檢查點操作會將 WAL 檔案中的內容傳輸回原始資料庫檔案。檢查點可以與讀取器同時運行,但是當檢查點到達 WAL 檔案中超過任何目前讀取器結束標記的頁面時,它必須停止。檢查點必須在此停止,否則它可能會覆蓋讀取器正在使用的部分資料庫檔案。檢查點會記住(在 wal-index 中)它進行到哪裡了,並將在下一次調用時從上次停止的地方繼續將內容從 WAL 傳輸到資料庫。
因此,長時間運行的讀取事務可能會阻止檢查點的進度。但推測每個讀取事務最終都會結束,檢查點將能夠繼續。
每當發生寫入操作時,寫入器都會檢查檢查點的進度,如果整個 WAL 檔案已傳輸到資料庫並同步,並且沒有讀取器正在使用 WAL,則寫入器會將 WAL 倒回開頭,並開始將新的事務寫入 WAL 的開頭。這種機制可以防止 WAL 檔案無限增長。
寫入事務非常快,因為它們只涉及寫入內容一次(相對於回滾日誌事務的兩次),並且因為寫入都是循序的。此外,只要應用程式願意犧牲斷電或硬重啟後的持久性,就不需要將內容同步到磁碟。(如果PRAGMA synchronous設定為 FULL,寫入器會在每次事務提交時同步 WAL,但如果PRAGMA synchronous設定為 NORMAL,則會省略此同步。)
另一方面,隨著 WAL 檔案大小的增長,讀取效能會下降,因為每個讀取器都必須檢查 WAL 檔案中的內容,而檢查 WAL 檔案所需的時間與 WAL 檔案的大小成正比。wal-index 有助於更快地在 WAL 檔案中找到內容,但效能仍然會隨著 WAL 檔案大小的增加而下降。因此,為了保持良好的讀取效能,透過定期運行檢查點來保持 WAL 檔案大小較小非常重要。
檢查點需要同步操作,以避免斷電或硬重啟後可能發生的資料庫損壞。在將內容從 WAL 移至資料庫之前,必須將 WAL 同步到永久儲存裝置,並且在重設 WAL 之前,必須同步資料庫檔案。檢查點也需要更多搜尋操作。檢查點會盡可能地對資料庫進行循序頁面寫入(頁面按升序從 WAL 傳輸到資料庫),但即使如此,頁面寫入之間通常也會穿插許多搜尋操作。這些因素共同導致檢查點比寫入事務慢。
預設策略允許連續的寫入事務使 WAL 增長,直到其大小達到約 1000 頁,然後針對每個後續的 COMMIT 執行檢查點操作,直到 WAL 重設為小於 1000 頁。預設情況下,檢查點將由執行 COMMIT 並使 WAL 超過其大小限制的同一執行緒自動執行。這樣做的效果是使大多數 COMMIT 操作非常快,但偶爾的 COMMIT(觸發檢查點的那些)會慢得多。如果這種效果不理想,則應用程式可以停用自動檢查點,並在單獨的執行緒或單獨的程序中定期執行檢查點。(完成此操作的命令和介面連結如下所示。)
請注意,當 PRAGMA synchronous 設定為 NORMAL 時,檢查點是唯一發出 I/O 屏障或同步操作(在 Unix 上為 fsync(),在 Windows 上為 FlushFileBuffers())的操作。因此,如果應用程式在單獨的執行緒或程序中執行檢查點,則執行資料庫查詢和更新的主執行緒或程序將永遠不會阻塞在同步操作上。這有助於防止在繁忙磁碟機上執行的應用程式中出現「閂鎖」。此配置的缺點是事務不再持久,並且在電源故障或硬重設後可能會復原。
還要注意平均讀取效能和平均寫入效能之間的權衡。為了最大限度地提高讀取效能,人們希望 WAL 儘可能小,因此經常執行檢查點,甚至可能在每次 COMMIT 時都執行。為了最大限度地提高寫入效能,人們希望將每次檢查點的成本分攤到盡可能多的寫入操作中,這意味著人們希望不經常執行檢查點,並讓 WAL 在每次檢查點之前增長到盡可能大。因此,根據應用程式相對的讀寫效能需求,執行檢查點的頻率決定可能會因應用程式而異。預設策略是在 WAL 達到 1000 頁時執行一次檢查點,這種策略在工作站上的測試應用程式中似乎運作良好,但在不同的平台或不同的工作負載下,其他策略可能運作得更好。
SQLite 資料庫連線預設為 journal_mode=DELETE。要轉換為 WAL 模式,請使用以下語法
PRAGMA journal_mode=WAL;
journal_mode 語法會傳回一個字串,即新的日誌模式。成功時,語法將傳回字串 "wal"。如果無法完成到 WAL 的轉換(例如,如果 VFS 不支援必要的共享記憶體原語),則日誌模式將保持不變,並且從原語傳回的字串將是先前的日誌模式(例如 "delete").
預設情況下,SQLite 會在每次 COMMIT 操作導致 WAL 檔案大小達到或超過 1000 頁時,或資料庫檔案上的最後一個資料庫連線關閉時自動執行檢查點操作。預設配置旨在適用於大多數應用程式。但需要更多控制的程式可以使用 wal_checkpoint pragma 或呼叫 sqlite3_wal_checkpoint() C 介面強制執行檢查點操作。可以使用 wal_autocheckpoint pragma 或呼叫 sqlite3_wal_autocheckpoint() C 介面更改自動檢查點閾值或完全停用自動檢查點操作。程式還可以透過 sqlite3_wal_hook() 註冊一個回呼函式,以便在任何交易提交到 WAL 時呼叫。然後,此回呼函式可以根據其認為適當的任何條件呼叫 sqlite3_wal_checkpoint() 或 sqlite3_wal_checkpoint_v2()。(自動檢查點機制是作為 sqlite3_wal_hook() 的簡單包裝器實現的。)
應用程式可以透過在資料庫上的任何可寫入資料庫連線中呼叫 sqlite3_wal_checkpoint() 或 sqlite3_wal_checkpoint_v2() 來啟動檢查點。有三種不同積極程度的檢查點子類型:PASSIVE、FULL 和 RESTART。預設的檢查點類型是 PASSIVE,它會盡可能多地執行工作,而不會干擾其他資料庫連線,並且如果存在並行讀取器或寫入器,則可能不會執行到完成。由 sqlite3_wal_checkpoint() 和自動檢查點機制啟動的所有檢查點都是 PASSIVE。FULL 和 RESTART 檢查點會更努力地嘗試將檢查點執行到完成,並且只能透過呼叫 sqlite3_wal_checkpoint_v2() 來啟動。有關 FULL 和 RESET 檢查點的更多資訊,請參閱 sqlite3_wal_checkpoint_v2() 文件。
與其他日誌模式不同,PRAGMA journal_mode=WAL 是持續性的。如果一個程序設定了 WAL 模式,然後關閉並重新開啟資料庫,則資料庫將以 WAL 模式重新啟動。相反,如果一個程序設定了(例如)PRAGMA journal_mode=TRUNCATE,然後關閉並重新開啟資料庫,則資料庫將以預設的 DELETE 回滾模式(而不是先前的 TRUNCATE 設定)重新啟動。
WAL 模式的持久性意味著應用程式可以轉換為在 WAL 模式下使用 SQLite,而無需對應用程式本身進行任何更改。只需在資料庫檔案上使用 命令列 shell 或其他工具執行 "PRAGMA journal_mode=WAL;",然後重新啟動應用程式即可。
如果在任何一個連線上設定了 WAL 日誌模式,則所有連線到同一個資料庫檔案的連線都將設定該模式。
當在 WAL 模式資料庫上開啟 資料庫連線 時,SQLite 會維護一個額外的日誌檔案,稱為「預寫式日誌」或「WAL 檔案」。磁碟上此檔案的名稱通常是資料庫檔案的名稱加上額外的 "-wal"-wal 後綴,但如果 SQLite 使用 SQLITE_ENABLE_8_3_NAMES 編譯,則可能適用不同的命名規則。
只要有任何資料庫連線開啟著資料庫,WAL 檔案就會存在。通常,當最後一個連線到資料庫關閉時,WAL 檔案會自動刪除。然而,如果最後一個開啟資料庫的程序沒有乾淨地關閉資料庫連線,或者使用了 SQLITE_FCNTL_PERSIST_WAL 檔案控制,那麼在所有資料庫連線關閉後,WAL 檔案可能會保留在磁碟上。WAL 檔案是資料庫持久狀態的一部分,如果複製或移動資料庫,則應將其與資料庫一起保存。如果資料庫檔案与其 WAL 檔案分離,則先前提交到資料庫的事務可能會遺失,或者資料庫檔案可能會損毀。移除 WAL 檔案的唯一安全方法是使用其中一個 sqlite3_open() 介面開啟資料庫檔案,然後立即使用 sqlite3_close() 關閉資料庫。
WAL 檔案格式有明確的定義,並且是跨平台的。
舊版的 SQLite 無法讀取唯讀的 WAL 模式資料庫。換句話說,讀取 WAL 模式資料庫需要寫入權限。從 SQLite 3.22.0 版(2018-01-22)開始,放寬了此限制。
在較新版本的 SQLite 中,只要滿足以下一個或多個條件,就可以讀取唯讀媒體上的 WAL 模式資料庫,或缺少寫入權限的 WAL 模式資料庫:
即使可以開啟唯讀的 WAL 模式資料庫,最佳做法是在將 SQLite 資料庫映像燒錄到唯讀媒體之前,將其轉換為 PRAGMA journal_mode=DELETE。
在正常情況下,新內容會附加到 WAL 檔案,直到 WAL 檔案累積大約 1000 頁(大小約為 4MB),此時會自動執行檢查點並回收 WAL 檔案。檢查點通常不會截斷 WAL 檔案(除非設定了 journal_size_limit pragma)。相反,它只是讓 SQLite 從頭開始覆寫 WAL 檔案。這樣做的原因是覆寫現有檔案通常比附加檔案更快。當最後一個資料庫連線關閉時,該連線會執行最後一次檢查點,然後刪除 WAL 及其關聯的共享記憶體檔案,以清理磁碟。
因此,在大多數情況下,應用程式根本不需要擔心 WAL 檔案。SQLite 會自動處理它。但是,SQLite 有可能進入 WAL 檔案無限增長的状态,導致磁碟空間過度使用和查詢速度變慢。以下列點列舉了可能發生這種情況的一些方式以及如何避免它們。
停用自動檢查點機制。 在預設配置中,當 WAL 檔案超過 1000 頁時,SQLite 會在任何事務結束時對 WAL 檔案執行檢查點。但是,存在編譯時和執行時選項可以停用或延遲此自動檢查點。如果應用程式停用自動檢查點,則無法防止 WAL 檔案過度增長。
檢查點只有在沒有其他資料庫連線使用 WAL 檔案的情況下,才能完整執行並重設 WAL 檔案。如果有其他連線開啟了讀取交易,則檢查點無法重設 WAL 檔案,因為這樣做可能會刪除讀取器正在使用的內容。檢查點會盡可能在不影響讀取器的情況下執行工作,但無法完整執行。在下一次寫入交易後,檢查點會從上次停止的地方重新啟動。這個過程會重複,直到某個檢查點能夠完成。
然而,如果資料庫有許多並行重疊的讀取器,並且始終至少有一個活躍的讀取器,則任何檢查點都無法完成,因此 WAL 檔案將無限增長。
可以透過確保存在「讀取間隙」來避免這種情況:在沒有任何程序從資料庫讀取資料的時間點嘗試執行檢查點。在具有許多並行讀取器的應用程式中,也可以考慮使用 `SQLITE_CHECKPOINT_RESTART` 或 `SQLITE_CHECKPOINT_TRUNCATE` 選項手動執行檢查點,這將確保檢查點在返回之前完整執行。使用 `SQLITE_CHECKPOINT_RESTART` 和 `SQLITE_CHECKPOINT_TRUNCATE` 的缺點是,讀取器可能會在檢查點執行時被阻塞。
檢查點只能在沒有其他交易執行時完成,這意味著 WAL 檔案無法在寫入交易過程中重設。因此,對大型資料庫進行大型更改可能會導致 WAL 檔案變大。一旦寫入交易完成(假設沒有其他讀取器阻塞它),WAL 檔案就會被檢查,但在這段時間內,檔案可能會變得非常大。
從 SQLite 3.11.0 版 (2016-02-15) 開始,單個交易的 WAL 檔案大小應與交易本身成比例。交易更改的頁面應該只寫入 WAL 檔案一次。然而,在舊版本的 SQLite 中,如果交易大小超過頁面快取,則同一頁面可能會被多次寫入 WAL 檔案。
為了穩健性,wal 索引 使用一個普通的檔案來實作,並透過記憶體映射 (mmap) 進行存取。早期的(預發布)WAL 模式實作將 wal 索引儲存在揮發性共享記憶體中,例如在 Linux 上於 `/dev/shm` 中建立的檔案,或在其他 Unix 系統上於 `/tmp` 中建立的檔案。這種方法的問題在於,具有不同根目錄(透過 chroot 更改)的程序將看到不同的檔案,因此使用不同的共享記憶體區域,導致資料庫損壞。其他建立無名共享記憶體區塊的方法在各種 Unix 版本中並不具備可攜性。而且我們找不到任何在 Windows 上建立無名共享記憶體區塊的方法。我們發現的唯一能保證所有存取相同資料庫檔案的程序都使用相同共享記憶體的方法,是在與資料庫本身相同的目錄中建立一個檔案,並透過記憶體映射該檔案來建立共享記憶體。
使用普通磁碟檔案來提供共享記憶體的缺點是,它可能會透過將共享記憶體寫入磁碟而執行不必要的磁碟 I/O。然而,開發人員認為這不是一個主要問題,因為 wal 索引的大小很少超過 32 KiB,而且從不同步。此外,當最後一個資料庫連線斷開時,wal 索引的後備檔案會被刪除,這通常可以防止任何實際的磁碟 I/O 發生。
對於預設的共享記憶體實作方式無法接受的特殊應用程式,可以透過客製化的 虛擬檔案系統 (VFS) 設計替代方法。例如,如果已知特定資料庫只會被單一行程內的執行緒存取,則 wal-index 可以使用堆積記憶體而不是真正的共享記憶體來實作。
從 SQLite 3.7.4 版 (2010-12-07) 開始,即使共享記憶體不可用,只要在第一次嘗試存取之前將 locking_mode 設定為 EXCLUSIVE,就可以建立、讀取和寫入 WAL 資料庫。換句話說,如果保證某個行程是唯一存取資料庫的行程,則該行程可以在不使用共享記憶體的情況下與 WAL 資料庫互動。此功能允許在 虛擬檔案系統 (VFS) 缺乏 sqlite3_io_methods 物件上的「版本 2」共享記憶體方法 xShmMap、xShmLock、xShmBarrier 和 xShmUnmap 的情況下,建立、讀取和寫入 WAL 資料庫。
如果在第一次以 WAL 模式存取資料庫之前設定了 EXCLUSIVE 鎖定模式,那麼 SQLite 就永遠不會嘗試呼叫任何共享記憶體方法,因此也永遠不會建立共享記憶體 wal-index。在這種情況下,只要日誌模式是 WAL,資料庫連線就會保持在 EXCLUSIVE 模式;嘗試使用「PRAGMA locking_mode=NORMAL;」來更改鎖定模式將無效。唯一更改 EXCLUSIVE 鎖定模式的方法是先更改 WAL 日誌模式。
如果在第一次以 WAL 模式存取資料庫時,NORMAL 鎖定模式生效,則會建立共享記憶體 wal-index。這表示底層的 VFS 必須支援「版本 2」共享記憶體。如果 VFS 不支援共享記憶體方法,則嘗試開啟已處於 WAL 模式的資料庫,或嘗試將資料庫轉換為 WAL 模式將會失敗。只要只有一個連線正在使用共享記憶體 wal-index,就可以在 NORMAL 和 EXCLUSIVE 之間自由更改鎖定模式。只有在省略共享記憶體 wal-index,也就是在第一次以 WAL 模式存取資料庫之前鎖定模式為 EXCLUSIVE 時,鎖定模式才會卡在 EXCLUSIVE。
WAL 模式的第二個優點是寫入者不會阻塞讀取者,讀取者也不會阻塞寫入者。這大部分情況下是正確的。但在某些特殊情況下,對 WAL 模式資料庫的查詢可能會返回 SQLITE_BUSY,因此應用程式應該為這種情況做好準備。
對 WAL 模式資料庫的查詢可能返回 SQLITE_BUSY 的情況包括以下幾種:
如果另一個資料庫連線以 獨佔鎖定模式 開啟資料庫,則所有對該資料庫的查詢都將返回 SQLITE_BUSY。Chrome 和 Firefox 都以獨佔鎖定模式開啟其資料庫檔案,因此例如,在應用程式執行時嘗試讀取 Chrome 或 Firefox 資料庫就會遇到這個問題。
當到特定資料庫的最後一個連線關閉時,該連線會在清除 WAL 和共享記憶體檔案時短暫地取得獨佔鎖定。如果在第一個連線仍在清除過程中時,嘗試開啟和查詢資料庫,則第二個連線可能會收到 SQLITE_BUSY 錯誤。
如果到資料庫的最後一個連線當機,則第一個開啟資料庫的新連線將啟動復原程序。在復原期間會持有獨佔鎖定。因此,如果在第二個連線執行復原時,第三個資料庫連線嘗試介入並查詢,則第三個連線將收到 SQLITE_BUSY 錯誤。
WAL 模式並未改變資料庫檔案格式。然而,WAL 檔案和 wal-index 索引檔是新概念,因此舊版的 SQLite 無法復原在當機時以 WAL 模式運作的 SQLite 資料庫。為了防止舊版 SQLite(3.7.0 版,2010-07-22 之前的版本)嘗試復原 WAL 模式資料庫(並使情況更糟),資料庫檔案格式版本號碼(資料庫標頭 中的第 18 和 19 個位元組)在 WAL 模式下從 1 增加到 2。因此,如果舊版 SQLite 嘗試連線到以 WAL 模式運作的 SQLite 資料庫,它會回報類似「檔案已加密或不是資料庫」的錯誤。
可以使用如下 pragma 指令明確地關閉 WAL 模式:
PRAGMA journal_mode=DELETE;
刻意關閉 WAL 模式會將資料庫檔案格式版本號碼改回 1,以便舊版 SQLite 可以再次存取資料庫檔案。
本頁面最後修改時間:2024-07-14 23:07:20 UTC