小巧、快速、可靠。
任選三項。
SQLite 中的原子提交

1. 簡介

SQLite 等交易資料庫的重要功能是「原子提交」。原子提交表示單一交易中的所有資料庫變更都會發生,或都不會發生。透過原子提交,就好像資料庫檔案的不同區段中的許多不同寫入會立即且同時發生。真正的硬體會將寫入序列化到大量儲存裝置,而寫入單一扇區需要一段有限的時間。因此,不可能真正同時和/或立即寫入資料庫檔案的許多不同扇區。但 SQLite 中的原子提交邏輯會讓交易的變更看起來像是立即且同時寫入的。

SQLite 具有重要的特性,即使交易因作業系統崩潰或電源故障而中斷,交易看起來也是原子的。

本文說明 SQLite 用來建立原子提交幻覺的技術。

本文中的資訊僅適用於 SQLite 在「回滾模式」中運作時,或換句話說,當 SQLite 未使用先行寫入記錄檔時。啟用先行寫入記錄檔時,SQLite 仍支援原子提交,但它會透過與本文中說明的機制不同的機制來達成原子提交。請參閱先行寫入記錄檔文件,以取得有關 SQLite 在該情況下如何支援原子提交的更多資訊。

2. 硬體假設

在本文中,我們將大量儲存裝置稱為「磁碟」,即使大量儲存裝置實際上可能是快閃記憶體。

我們假設磁碟是以我們稱為「區塊」的區塊寫入的。無法修改小於一個區塊的任何磁碟部分。若要變更小於一個區塊的磁碟部分,您必須讀取包含您要變更部分的完整區塊,進行變更,然後再寫回完整的區塊。

在傳統的旋轉磁碟上,區塊是讀寫兩方向的最小傳輸單位。然而,在快閃記憶體上,讀取的最小大小通常遠小於寫入的最小大小。SQLite 只關注最小的寫入量,因此在本文中,當我們說「區塊」時,我們的意思是最小量的一次寫入大量儲存的資料。

在 SQLite 版本 3.3.14 之前,所有情況下都假設磁區大小為 512 位元組。有一個編譯時期選項可以變更這個值,但程式碼從未使用較大的值進行測試。512 位元組磁區的假設看似合理,因為直到最近,所有磁碟機在內部都使用 512 位元組磁區。然而,最近有股趨勢要將磁碟的磁區大小增加到 4096 位元組。此外,快閃記憶體的磁區大小通常大於 512 位元組。基於這些原因,從 3.3.14 開始的 SQLite 版本在作業系統介面層中有一個方法,會查詢底層檔案系統以找出真正的磁區大小。目前實作(版本 3.5.0)仍會傳回硬編碼的值 512 位元組,因為在 Unix 或 Windows 上都沒有標準方法可以找出真正的磁區大小。但嵌入式裝置製造商可以使用這個方法根據自己的需求進行調整。而且我們已保留在未來在 Unix 和 Windows 上填入更有意義的實作的可能性。

SQLite 傳統上假設一個區塊寫入不是原子的。然而,SQLite 總是假設一個區塊寫入是線性的。我們所謂的「線性」是指 SQLite 假設在寫入一個區塊時,硬體會從資料的一端開始,並逐位元組寫入,直到寫到另一端。寫入可能會從頭到尾或從尾到頭。如果在區塊寫入過程中發生電源故障,區塊的一部分可能會被修改,而另一部分則保持不變。SQLite 的關鍵假設是,如果區塊的任何部分發生變化,則第一個或最後一個位元組將被更改。因此,硬體永遠不會從區塊中間開始寫入,然後朝向兩端寫入。我們不知道這個假設是否總是成立,但這似乎是合理的。

前一段落指出 SQLite 不假設區段寫入是原子性的。這在預設情況下是正確的。但是從 SQLite 版本 3.5.0 開始,有一個稱為虛擬檔案系統 (VFS) 介面的新介面。VFS 是 SQLite 與底層檔案系統通訊的唯一方式。程式碼隨附適用於 Unix 和 Windows 的預設 VFS 實作,並且有一個機制可以在執行階段建立新的自訂 VFS 實作。在此新的 VFS 介面中有一個稱為 xDeviceCharacteristics 的方法。此方法會查詢底層檔案系統以找出檔案系統可能或可能不會表現出的各種屬性和行為。xDeviceCharacteristics 方法可能會指出區段寫入是原子性的,如果確實如此指出,SQLite 會嘗試利用此事實。但是 Unix 和 Windows 的預設 xDeviceCharacteristics 方法並未指出原子區段寫入,因此通常會省略這些最佳化。

SQLite 假設作業系統會緩衝寫入,而且寫入要求會在資料實際儲存在大量儲存裝置之前傳回。SQLite 進一步假設寫入作業會由作業系統重新排序。基於這個原因,SQLite 會在關鍵點執行「快取」或「fsync」作業。SQLite 假設快取或 fsync 在快取檔案的所有待處理寫入作業完成之前不會傳回。我們得知在某些版本的 Windows 和 Linux 上,快取和 fsync 原語已損毀。這很不幸。這讓 SQLite 在提交過程中發生斷電時,有可能會損毀資料庫。然而,SQLite 無法測試或補救這個情況。SQLite 假設它執行的作業系統會如廣告宣傳般運作。如果事實並非如此,好吧,希望你不會太常斷電。

SQLite 假設當檔案長度增加時,新的檔案空間最初包含垃圾,然後再填入實際寫入的資料。換句話說,SQLite 假設檔案大小會在檔案內容之前更新。這是一個悲觀的假設,而且 SQLite 必須執行一些額外的作業,以確保在檔案大小增加和新內容寫入之間斷電時,不會造成資料庫損毀。VFS 的 xDeviceCharacteristics 方法可能會指出檔案系統會在更新檔案大小之前始終寫入資料。(對於查看程式碼的讀者來說,這是 SQLITE_IOCAP_SAFE_APPEND 屬性。)當 xDeviceCharacteristics 方法指出檔案內容會在檔案大小增加之前寫入時,SQLite 可以放棄一些繁瑣的資料庫保護步驟,從而減少執行提交所需的磁碟 I/O 量。然而,目前的實作並未對 Windows 和 Unix 的預設 VFS 做出這樣的假設。

SQLite 假設從使用者程序的角度來看,檔案刪除是原子性的。我們的用意是,如果 SQLite 要求刪除一個檔案,而刪除作業期間發生斷電,一旦恢復供電,檔案將完全存在且所有原始內容未變更,否則檔案將完全不會出現在檔案系統中。如果恢復供電後檔案只部分刪除、部分資料已變更或刪除、或檔案已截斷但未完全移除,則可能會導致資料庫損毀。

SQLite 假設由底層硬體和作業系統負責偵測和/或修正因宇宙射線、熱雜訊、量子波動、裝置驅動程式錯誤或其他機制造成的位元錯誤。SQLite 沒有為偵測損毀或 I/O 錯誤的目的,在資料庫檔案中加入任何冗餘。SQLite 假設它讀取的資料與它先前寫入的資料完全相同。

預設情況下,SQLite 假設作業系統呼叫寫入一個位元組範圍時,即使在寫入期間發生斷電或作業系統崩潰,也不會損毀或變更該範圍外的任何位元組。我們稱之為「安全電源覆寫」屬性。在 3.7.9 版(2011-11-01)之前,SQLite 沒有假設安全電源覆寫。但由於大多數磁碟機的標準扇區大小從 512 位元組增加到 4096 位元組,因此必須假設安全電源覆寫才能維持歷史效能水準,所以 SQLite 的最新版本預設假設安全電源覆寫。如果需要,可以在編譯時或執行時停用安全電源覆寫屬性的假設。請參閱 安全電源覆寫文件 以取得進一步的詳細資料。

3. 單一檔案提交

我們從 SQLite 採取的步驟概述開始,以對單一資料庫檔案執行交易的原子提交。用於防止因斷電而損毀的檔案格式詳細資料,以及跨多個資料庫執行原子提交的技術,將在後面的章節中討論。

3.1. 初始狀態

當資料庫連線第一次開啟時,電腦的狀態會以右邊的圖示概念性地顯示。圖示最右邊的區域(標示為「磁碟」)代表儲存在大量儲存裝置中的資訊。每個長方形都是一個區塊。藍色代表區塊包含原始資料。中間的區域是作業系統的磁碟快取。在我們的範例開始時,快取是冷的,這表示磁碟快取的長方形是空的。圖示最左邊的區域顯示使用 SQLite 的程序的記憶體內容。資料庫連線剛剛開啟,尚未讀取任何資訊,因此使用者空間是空的。


3.2. 取得讀取鎖定

在 SQLite 可以寫入資料庫之前,它必須先讀取資料庫以查看裡面有什麼。即使只是附加新資料,SQLite 仍必須從「sqlite_schema」表格中讀取資料庫結構,以便知道如何剖析 INSERT 陳述式,並找出新資訊應儲存在資料庫檔案中的什麼位置。

從資料庫檔案讀取的第一步是取得資料庫檔案的共用鎖定。一個「共用」鎖定允許兩個或兩個以上的資料庫連線同時從資料庫檔案中讀取。但是,共用鎖定會阻止其他資料庫連線在我們讀取資料庫檔案時寫入資料庫檔案。這是必要的,因為如果另一個資料庫連線在我們從資料庫檔案讀取的同時寫入資料庫檔案,我們可能會在變更之前讀取一些資料,在變更之後讀取其他資料。這會使其他程序所做的變更看起來不是原子的。

請注意,共享鎖定在作業系統磁碟快取中,而不是在磁碟本身。檔案鎖定通常只是作業系統核心中的旗標。(詳細資料取決於特定的作業系統層介面。)因此,如果作業系統發生故障或斷電,鎖定將立即消失。通常,建立鎖定的程序結束時,鎖定也會消失。


3.3. 從資料庫中讀取資訊

取得共享鎖定後,我們可以開始從資料庫檔案中讀取資訊。在此範例中,我們假設快取是冷的,因此必須先從大量儲存裝置讀取資訊到作業系統快取,然後再從作業系統快取傳輸到使用者空間。在後續讀取中,部分或全部資訊可能已在作業系統快取中找到,因此只需要傳輸到使用者空間即可。

通常只會讀取資料庫檔案中的一小部分頁面。在此範例中,我們顯示讀取八個頁面中的三個頁面。在典型的應用程式中,資料庫會有數千個頁面,而查詢通常只會觸及這些頁面的一小部分。


3.4. 取得保留鎖定

在對資料庫進行變更之前,SQLite 會先取得資料庫檔案的「保留」鎖定。保留鎖定類似於共用鎖定,因為保留鎖定和共用鎖定都允許其他程序從資料庫檔案中讀取。單一保留鎖定可以與其他程序的共用鎖定同時存在。然而,資料庫檔案上只能有一個保留鎖定。因此,一次只有一個程序可以嘗試寫入資料庫。

保留鎖定的概念在於,它表示一個程序打算在不久的將來修改資料庫檔案,但尚未開始進行修改。由於修改尚未開始,其他程序可以繼續從資料庫中讀取。然而,沒有其他程序也應該開始嘗試寫入資料庫。


3.5. 建立回滾記錄檔

在對資料庫檔案進行任何變更之前,SQLite 會先建立一個獨立的回滾記錄檔,並將要變更的資料庫頁面的原始內容寫入回滾記錄檔。回滾記錄檔的概念在於,它包含將資料庫還原回原始狀態所需的所有資訊。

回滾記錄檔包含一個小標頭(圖表中以綠色顯示),記錄資料庫檔案的原始大小。因此,如果變更導致資料庫檔案變大,我們仍然會知道資料庫的原始大小。頁面編號與寫入回滾記錄檔的每個資料庫頁面一起儲存。

當建立新檔案時,大部分的桌面作業系統(Windows、Linux、Mac OS X)並不會實際寫入任何資料到磁碟。新檔案只會建立在作業系統的磁碟快取中。直到稍後作業系統有空時,檔案才會建立在大量儲存裝置中。這會讓使用者產生 I/O 執行速度比實際磁碟 I/O 執行速度快的錯覺。我們在右邊的圖表中說明這個概念,顯示新的回滾記錄只會出現在作業系統的磁碟快取中,而不是磁碟本身。


3.6. 在使用者空間變更資料庫頁面

原始頁面內容儲存在回滾記錄中後,可以在使用者記憶體中修改頁面。每個資料庫連線都有自己的使用者空間私人副本,因此在使用者空間中所做的變更只會對正在做變更的資料庫連線可見。其他資料庫連線仍會看到作業系統磁碟快取緩衝區中尚未變更的資訊。因此,即使一個程序忙於修改資料庫,其他程序仍可繼續讀取其自己的原始資料庫內容副本。


3.7. 將回滾記錄檔案快取到大量儲存裝置

下一步是將回滾記錄檔案的內容快取到非揮發性儲存裝置。正如我們稍後將看到的,這是確保資料庫可以在意外斷電時存活的關鍵步驟。此步驟也需要花費很多時間,因為寫入非揮發性儲存裝置通常是緩慢的操作。

此步驟通常比單純將回滾日誌沖洗到磁碟還要複雜。在大部分的平台上,需要兩個分開的沖洗(或 fsync())操作。第一個沖洗寫出基礎回滾日誌內容。接著修改回滾日誌的標頭,以顯示回滾日誌中的頁面數。然後將標頭沖洗到磁碟。關於我們為何執行此標頭修改和額外沖洗的詳細資訊,會在本文的後續章節中提供。


3.8. 取得獨佔鎖定

在對資料庫檔案本身進行變更之前,我們必須取得資料庫檔案的獨佔鎖定。取得獨佔鎖定實際上是一個兩步驟的程序。首先,SQLite 取得「暫定」鎖定。接著,它將暫定鎖定升級為獨佔鎖定。

暫定鎖定允許已擁有共用鎖定的其他程序繼續讀取資料庫檔案。但它會阻止建立新的共用鎖定。暫定鎖定的背後概念是防止大量讀取者造成寫入者飢餓。可能有數十個,甚至數百個其他程序嘗試讀取資料庫檔案。每個程序在開始讀取之前會取得共用鎖定,讀取它需要的內容,然後釋放共用鎖定。然而,如果有很多不同的程序都從同一個資料庫讀取,可能會發生一個新的程序總是在前一個程序釋放其共用鎖定之前取得其共用鎖定的情況。因此,資料庫檔案上永遠不會有沒共用鎖定的時刻,因此寫入者永遠沒有機會取得獨佔鎖定。暫定鎖定旨在防止這種循環,方法是允許現有的共用鎖定繼續進行,但阻止建立新的共用鎖定。最後,所有共用鎖定都會清除,然後暫定鎖定將能夠升級為獨佔鎖定。


3.9. 將變更寫入資料庫檔案

一旦取得獨佔鎖定,我們知道沒有其他程序正在從資料庫檔案中讀取,而且可以安全地將變更寫入資料庫檔案。通常這些變更只會到達作業系統的磁碟快取,而不會完全到達大量儲存空間。


3.10. 0 將變更清除到大量儲存空間

必須執行另一項清除動作,以確保所有資料庫變更都寫入非揮發性儲存空間。這是確保資料庫在斷電後仍能安全無虞的重要步驟。然而,由於寫入磁碟或快閃記憶體的固有緩慢性,此步驟加上上面第 3.7 節中的回滾日誌檔案清除動作,佔用了 SQLite 中完成交易提交所需的大部分時間。


3.11. 1 刪除回滾日誌

在所有資料庫變更都安全地儲存在大量儲存裝置上後,回滾日誌檔案會被刪除。這是交易提交的瞬間。如果在此點之前發生斷電或系統崩潰,則稍後描述的復原程序會讓資料庫檔案看起來像從未被變更過。如果在回滾日誌被刪除後發生斷電或系統崩潰,則會看起來像所有變更都已寫入磁碟。因此,SQLite 會讓資料庫檔案看起來像是沒有變更,或已變更為資料庫檔案的完整組,具體取決於回滾日誌檔案是否存在。

刪除檔案並非真正的原子操作,但從使用者程序的角度來看,它似乎是原子操作。程序始終可以詢問作業系統「此檔案是否存在?」,而程序將會收到肯定或否定的答案。在交易提交期間發生電源故障後,SQLite 會詢問作業系統回滾日誌檔是否存在。如果答案為「是」,則交易未完成並會回滾。如果答案為「否」,則表示交易已提交。

交易的存在取決於回滾日誌檔是否存在,而從使用者空間程序的角度來看,刪除檔案似乎是原子操作。因此,交易似乎是原子操作。

在許多系統上,刪除檔案的動作很昂貴。作為最佳化,SQLite 可以設定為將日誌檔截斷為長度為零的位元組,或用零覆寫日誌檔標頭。在任何一種情況下,產生的日誌檔都無法再回滾,因此交易仍會提交。將檔案截斷為長度為零(就像刪除檔案一樣),假設從使用者程序的角度來看,這是一個原子操作。用零覆寫日誌檔標頭並非原子操作,但如果標頭的任何部分格式錯誤,日誌檔將不會回滾。因此,可以說提交會在標頭變更到足以使其無效時立即發生。這通常會在標頭的第一個位元組歸零時發生。


3.12. 2 釋放鎖定

提交程序的最後一步是釋放獨佔鎖定,以便其他程序可以再次開始存取資料庫檔案。

在右圖中,我們顯示使用者空間中所保留的資訊會在鎖定解除時清除。這在較舊版本的 SQLite 中確實如此。但是較新版本的 SQLite 會將使用者空間資訊保留在記憶體中,以防在下次交易開始時需要使用。重複使用已在本地記憶體中的資訊比從作業系統磁碟快取傳回資訊或再次從磁碟機讀取資訊來得便宜。在重複使用使用者空間中的資訊之前,我們必須先重新取得共用鎖定,然後必須檢查以確保在我們未持有鎖定的同時,沒有其他程序修改資料庫檔案。資料庫第一頁有一個計數器,每次修改資料庫檔案時,這個計數器就會遞增。我們可以透過檢查這個計數器來找出是否有其他程序修改資料庫。如果資料庫已修改,則必須清除使用者空間快取並重新讀取。但通常情況下並未進行任何變更,而且可以重複使用使用者空間快取,以大幅提升效能。


4. 回復

原子提交應立即發生。但上面所述的處理顯然需要一段時間。假設電腦的電源在上面所述的提交作業進行到一半時中斷。為了維持變更是立即發生的假象,我們必須「回復」任何部分變更,並將資料庫回復到交易開始前的狀態。

4.1. 當發生錯誤時...

假設在上面 步驟 3.10 中寫入資料庫變更至磁碟時發生斷電。在電源復原後,情況可能類似於右圖所示。我們嘗試變更資料庫檔案的三個頁面,但只有一個頁面寫入成功。另一個頁面已部分寫入,而第三個頁面根本未寫入。

當電源恢復時,回滾日誌會完整且完好地儲存在磁碟中。這是關鍵的一點。在 步驟 3.7 中執行快取操作的原因,是要在對資料庫檔案本身進行任何變更之前,完全確定所有回滾日誌都安全地儲存在非揮發性儲存裝置中。


4.2. 熱回滾日誌

任何 SQLite 程序第一次嘗試存取資料庫檔案時,它會取得共用鎖定,如上方的 第 3.2 節 所述。但接著它會注意到有一個回滾日誌檔案存在。然後,SQLite 會檢查回滾日誌是否為「熱日誌」。熱日誌是需要播放才能將資料庫回復到正常狀態的回滾日誌。熱日誌只會在先前程序在提交交易時發生崩潰或斷電時存在。

如果符合以下所有條件,回滾日誌就是「熱」日誌

熱日誌的存在表示先前程序嘗試提交交易,但由於某些原因在提交完成之前中止。熱日誌表示資料庫檔案處於不一致的狀態,需要在使用前修復(透過回滾)。


4.3. 取得資料庫的獨佔鎖定

處理熱日誌的第一個步驟是取得資料庫檔案的獨佔鎖定。這可以防止兩個或多個程序同時嘗試復原同一個熱日誌。


4.4. 復原未完成的變更

一旦程序取得獨佔鎖定,它就可以寫入資料庫檔案。然後它會從復原日誌中讀取頁面的原始內容,並將該內容寫回資料庫檔案中它原本的來源位置。回想一下,復原日誌的標頭會記錄在中止交易開始之前的資料庫檔案原始大小。SQLite 會使用此資訊將資料庫檔案截斷回其原始大小,在未完成的交易導致資料庫增長的情況下。在此步驟的最後,資料庫的大小和包含的資訊應與中止交易開始之前相同。


4.5. 刪除熱日誌

在所有復原日誌中的資訊都已回寫到資料庫檔案(並在我們遇到另一波停電時沖刷到磁碟中)後,可以刪除熱復原日誌。

如同在 第 3.11 節 中,日誌檔案可能會被截斷為零長度,或者它的標頭可能會被覆寫為零,作為在刪除檔案很昂貴的系統上的最佳化。無論哪種方式,日誌在此步驟後不再是熱的。


4.6. 繼續,就像未完成的寫入從未發生過一樣

最後的復原步驟是將獨佔鎖降級為共用鎖。一旦此步驟完成,資料庫就會回到已中斷交易從未開始的狀態。由於所有復原活動都是全自動且透明的,因此使用 SQLite 的程式會認為已中斷的交易從未開始。


5. 多檔案提交

SQLite 允許單一 資料庫連線 透過使用 ATTACH DATABASE 指令同時與兩個或以上的資料庫檔案對話。當多個資料庫檔案在單一交易中被修改時,所有檔案都會以原子方式更新。換句話說,所有資料庫檔案都會被更新,否則都不會被更新。在多個資料庫檔案中達成原子提交比單一檔案更為複雜。本節說明 SQLite 如何執行這項魔法。

5.1. 為每個資料庫建立獨立的回滾日誌

當多個資料庫檔案參與交易時,每個資料庫都有自己的回滾日誌,且每個資料庫都個別鎖定。右方的圖表顯示一個場景,其中三個不同的資料庫檔案已在一個交易中被修改。此步驟中的情況類似於 步驟 3.6 中的單一檔案交易場景。每個資料庫檔案都有保留鎖定。對於每個資料庫,正在變更的頁面原始內容已寫入該資料庫的回滾日誌,但日誌內容尚未刷新至磁碟。雖然使用者記憶體中可能持有變更,但資料庫檔案本身尚未進行任何變更。

為求簡潔,本節中的圖表已簡化為比先前更簡單的圖表。藍色仍表示原始內容,粉紅色仍表示新內容。但回滾日誌和資料庫檔案中的個別頁面並未顯示,我們也不區分作業系統快取中的資訊和磁碟上的資訊。所有這些因素仍適用於多檔案提交情境。它們只會在圖表中佔用大量空間,而且不會新增任何新資訊,因此在此省略。


5.2. 超級日誌檔案

多檔案提交的下一步是建立「超級日誌」檔案。超級日誌檔案的名稱與原始資料庫檔名相同(使用 sqlite3_open() 介面開啟的資料庫,而不是 ATTACH 的輔助資料庫之一),附加上文字「-mjHHHHHHHH」,其中 HHHHHHHH 是隨機的 32 位元十六進位數字。隨機的 HHHHHHHH 字尾會在每個新的超級日誌中變更。

(注意:前一段落中提供的計算超級日誌檔名的公式對應於 SQLite 版本 3.5.0 的實作。但此公式並非 SQLite 規格的一部分,且可能會在未來版本中變更。)

與回滾日誌不同,超級日誌不包含任何原始資料庫頁面內容。相反地,超級日誌包含參與交易的每個資料庫的回滾日誌的完整路徑名稱。

建立超級日誌後,其內容會在採取任何進一步動作之前先寫入磁碟。在 Unix 上,包含超級日誌的目錄也會同步,以確保超級日誌檔案會在斷電後出現在目錄中。

超級日誌的目的是確保多檔案交易在斷電時是原子性的。但如果資料庫檔案有其他會在斷電時危害完整性的設定(例如 PRAGMA synchronous=OFFPRAGMA journal_mode=MEMORY),則會略過建立超級日誌,作為最佳化。

5.3. 更新回滾日誌標頭

下一步是在每個回滾日誌的標頭中記錄超級日誌檔案的完整路徑。在建立回滾日誌時,已在每個回滾日誌的開頭預留空間來存放超級日誌檔名。

在將超級日誌檔名寫入回滾日誌標頭之前和之後,都會將每個回滾日誌的內容快取到磁碟。執行這兩個快取動作都很重要。很幸運地,第二次快取通常很便宜,因為通常只會變更日誌檔案的一頁(第一頁)。

此步驟類似於上述單一檔案提交情境中的 步驟 3.7


5.4. 更新資料庫檔案

所有回滾日誌檔案都已快取到磁碟後,就可以開始更新資料庫檔案。在寫入變更之前,我們必須取得所有資料庫檔案的獨佔鎖定。寫入所有變更後,務必將變更快取到磁碟,以便在停電或作業系統崩潰時能保留變更。

此步驟對應到先前描述的單一檔案提交情境中的步驟 3.83.93.10


5.5. 刪除超級日誌檔案

下一步是刪除超級日誌檔案。這是多檔案交易提交的點。此步驟對應到單一檔案提交情境中的 步驟 3.11,其中會刪除回滾日誌。

如果在此時發生電源故障或作業系統崩潰,即使存在回滾日誌,交易也不會在系統重新開機時回滾。不同之處在於回滾日誌標頭中的超級日誌路徑名稱。重新啟動時,SQLite 只會將日誌視為熱門,而且只有在標頭中沒有超級日誌檔名(這是單一檔案提交的情況)或超級日誌檔案仍存在於磁碟上時才會播放日誌。


5.6. 清除回滾日誌

多檔案提交的最後一步是刪除個別回滾日誌,並解除資料庫檔案上的獨佔鎖定,以便其他程序可以看到變更。這對應到單一檔案提交順序中的 步驟 3.12

交易已在此時機點提交,因此刪除回滾日誌的時間並非關鍵。目前的實作會刪除單一回滾日誌,然後在繼續處理下一個回滾日誌之前解除對應資料庫檔案的鎖定。但我們未來可能會變更此機制,讓所有回滾日誌在任何資料庫檔案解除鎖定之前刪除。只要在對應資料庫檔案解除鎖定之前刪除回滾日誌,回滾日誌的刪除順序或資料庫檔案的解除鎖定順序並不重要。

6. 提交程序的其他詳細資料

上述第 3.0 節說明了 SQLite 中原子提交的工作原理概觀。但它忽略了一些重要的詳細資料。下列小節將嘗試填補這些空白。

6.1. 永遠記錄完整的區塊

當資料庫頁面的原始內容寫入回滾日誌時(如第 3.5 節所示),SQLite 始終會寫入完整的資料區塊,即使資料庫的頁面大小小於區塊大小。在過去,SQLite 中的區塊大小已硬編碼為 512 位元組,由於最小頁面大小也是 512 位元組,因此這從未成為問題。但從 SQLite 版本 3.3.14 開始,SQLite 可以使用區塊大小大於 512 位元組的大容量儲存裝置。因此,從版本 3.3.14 開始,每當區塊內的任何頁面寫入日誌檔案時,該區塊中的所有頁面都會與其一起儲存。

為了避免在寫入區段時發生斷電而導致資料庫損毀,將區段的所有頁面儲存在回滾日誌中非常重要。假設頁面 1、2、3 和 4 全部儲存在區段 1 中,且頁面 2 已修改。為了將變更寫入頁面 2,底層硬體也必須重新寫入頁面 1、3 和 4 的內容,因為硬體必須寫入完整的區段。如果此寫入作業因斷電而中斷,頁面 1、3 或 4 中的一個或多個可能會留下不正確的資料。因此,為了避免資料庫永久損毀,所有這些頁面的原始內容都必須包含在回滾日誌中。

6.2. 處理寫入日誌檔案中的垃圾資料

當資料附加到回滾日誌的結尾時,SQLite 通常會悲觀地假設檔案會先以無效的「垃圾」資料擴充,然後再以正確的資料取代垃圾資料。換句話說,SQLite 假設檔案大小會先增加,然後再將內容寫入檔案。如果在檔案大小增加後但檔案內容寫入前發生斷電,回滾日誌可能會留下垃圾資料。如果在復電後,另一個 SQLite 程序看到包含垃圾資料的回滾日誌,並嘗試將其回滾到原始資料庫檔案中,它可能會將一些垃圾資料複製到資料庫檔案中,進而損毀資料庫檔案。

SQLite 使用兩種防禦措施來對抗此問題。首先,SQLite 會在回滾日誌的標頭中記錄回滾日誌中的頁面數。此數字最初為零。因此,在嘗試回滾不完整(且可能已損毀)的回滾日誌期間,執行回滾的程序將會看到日誌包含零個頁面,因此不會對資料庫進行任何變更。在提交之前,會將回滾日誌快取到磁碟,以確保所有內容都已同步到磁碟,且檔案中沒有任何「垃圾」,然後才將標頭中的頁面數從零變更為回滾日誌中的實際頁面數。回滾日誌標頭始終保留在與任何頁面資料分開的區段中,這樣一來,如果發生斷電,就可以覆寫並快取標頭,而不會有損毀資料頁面的風險。請注意,回滾日誌會快取到磁碟兩次:一次寫入頁面內容,另一次寫入標頭中的頁面數。

前一段落描述的是在同步 pragma 設定為「full」時會發生什麼情況。

PRAGMA synchronous=FULL;

預設同步設定為完整,因此上述通常會發生。但是,如果同步設定降低為「一般」,SQLite 只會在頁面計數寫入後,清除一次回滾日誌。這會帶來損毀風險,因為修改後的(非零)頁面計數可能會在所有資料之前到達磁碟表面。資料會先寫入,但 SQLite 假設基礎檔案系統可以重新排序寫入要求,而且頁面計數可以先燒錄到氧化物中,即使其寫入要求最後才發生。因此,作為第二道防線,SQLite 也會對回滾日誌中每個資料頁面使用 32 位元檢查碼。此檢查碼會在回滾期間針對每個頁面評估,同時根據 第 4.4 節 中所述,回滾日誌。如果看到不正確的檢查碼,則會放棄回滾。請注意,檢查碼無法保證頁面資料正確,因為即使資料已損毀,檢查碼也有很小但有限的機率為正確。但檢查碼至少會讓此類錯誤不太可能發生。

請注意,如果同步設定為完整,則回滾日誌中的檢查碼並非必要。我們只有在同步降低為一般時,才會依賴檢查碼。儘管如此,檢查碼永遠不會造成損害,因此無論同步設定為何,都會包含在回滾日誌中。

6.3. 提交前的快取溢位

第 3.0 節 中顯示的提交程序假設所有資料庫變更都符合記憶體,直到提交時間為止。這是常見情況。但有時較大的變更會在交易提交之前溢位到使用者空間快取。在這些情況下,快取必須在交易完成之前溢位到資料庫。

快取溢位開始時,資料庫連線狀態如 步驟 3.6 所示。原始頁面內容已儲存在回滾日誌中,而頁面的修改則存在於使用者記憶體中。若要溢位快取,SQLite 會執行步驟 3.73.9。換句話說,回滾日誌會快取到磁碟,取得獨佔鎖定,並將變更寫入資料庫。但其餘步驟會延後到交易真正提交時。新的日誌標頭會附加到回滾日誌的結尾處(在其自己的區段中),並保留獨佔資料庫鎖定,但其他處理會返回 步驟 3.6。當交易提交或發生其他快取溢位時,會重複步驟 3.73.9。(由於第一次通過已持有獨佔資料庫鎖定,因此在第二次和後續通過時會略過步驟 3.8。)

快取溢位會導致資料庫檔案的鎖定從保留升級為獨佔。這會降低同時執行性。快取溢位也會導致額外的磁碟快取或 fsync 作業發生,而這些作業很慢,因此快取溢位會嚴重降低效能。基於這些原因,應盡可能避免快取溢位。

7. 最佳化

剖析指出,對於大多數系統和在大多數情況下,SQLite 花費大部分時間進行磁碟 I/O。因此,任何我們可以執行的動作,以減少磁碟 I/O 的數量,可能會對 SQLite 的效能產生很大的正面影響。本節說明 SQLite 使用的一些技術,以嘗試將磁碟 I/O 的數量減至最低,同時仍保留原子提交。

7.1. 交易之間保留快取

步驟 3.12 提交程序顯示,一旦共用鎖定已釋放,所有資料庫內容的使用者空間快取影像都必須捨棄。這樣做的原因是,在沒有共用鎖定的情況下,其他程序可以自由修改資料庫檔案內容,因此該內容的任何使用者空間影像都可能過時。因此,每個新交易都會從重新讀取先前已讀取的資料開始。這聽起來並不像一開始那麼糟,因為正在讀取的資料仍然可能在作業系統檔案快取中。因此,「讀取」實際上只是將資料從核心空間複製到使用者空間。但即便如此,這仍然需要時間。

從 SQLite 版本 3.3.14 開始,已新增一種機制來嘗試減少不必要的資料重新讀取。在較新版本的 SQLite 中,當資料庫檔案上的鎖定被釋放時,使用者空間分頁快取中的資料會被保留。稍後,在下一筆交易開始時取得共用鎖定後,SQLite 會檢查是否有任何其他程序修改了資料庫檔案。如果資料庫自上次釋放鎖定後以任何方式被變更,使用者空間快取會在那個時間點被清除。但通常資料庫檔案並未變更,使用者空間快取可以被保留,並且可以避免一些不必要的讀取操作。

為了確定資料庫檔案是否已變更,SQLite 會使用資料庫標頭中的計數器(在第 24 到 27 個位元組),此計數器會在每次變更操作期間遞增。SQLite 會在釋放其資料庫鎖定之前儲存此計數器的副本。然後在取得下一個資料庫鎖定後,它會將儲存的計數器值與目前的計數器值進行比較,如果值不同,則清除快取;如果值相同,則重複使用快取。

7.2. 獨佔存取模式

SQLite 版本 3.3.14 新增了「獨佔存取模式」的概念。在獨佔存取模式中,SQLite 會在每個交易結束時保留獨佔資料庫鎖定。這會防止其他程序存取資料庫,但在許多部署中只有一個程序會使用資料庫,因此這不是一個嚴重問題。獨佔存取模式的優點是可以透過三種方式減少磁碟 I/O

  1. 在第一個交易之後,不需要在資料庫標頭中增加變更計數器。這通常會節省將第一頁寫入回滾記錄檔和主資料庫檔案的動作。

  2. 沒有其他程序可以變更資料庫,因此在交易開始時不需要檢查變更計數器並清除使用者空間快取。

  3. 每個交易都可以透過用零覆寫回滾記錄檔標頭,而不是刪除記錄檔檔案來提交。這避免了必須修改記錄檔檔案的目錄項目,並避免了必須解除與記錄檔相關聯的磁碟區段。此外,下一個交易會覆寫現有的記錄檔檔案內容,而不是附加新內容,而且在多數系統中,覆寫的速度比附加快很多。

第三個最佳化,將記錄檔檔案標頭歸零,而不是刪除回滾記錄檔檔案,並非隨時都依賴於持有獨佔鎖定。此最佳化可以使用 journal_mode pragma 獨立於獨佔鎖定模式設定,如下面的 第 7.6 節 所述。

7.3. 不要記錄空閒清單頁面

當從 SQLite 資料庫中刪除資訊時,用於儲存已刪除資訊的頁面會新增到「空閒清單」。後續的插入會從這個空閒清單中取得頁面,而不是擴充資料庫檔案。

有些空閒清單頁面包含重要資料;特別是其他空閒清單頁面的位置。但大多數空閒清單頁面不包含任何有用的內容。後者空閒清單頁面稱為「葉子」頁面。我們可以自由修改資料庫中葉子空閒清單頁面的內容,而不會以任何方式變更資料庫的意義。

由於葉子空閒清單頁面的內容不重要,SQLite 會在提交程序的 步驟 3.5 中避免將葉子空閒清單頁面內容儲存在回滾記錄檔中。如果葉子空閒清單頁面變更,且該變更未在交易復原期間回滾,資料庫不會因遺漏而受損。類似地,新空閒清單頁面的內容永遠不會寫回資料庫,也不會在 步驟 3.9 中從資料庫讀取,也不會在 步驟 3.3 中從資料庫讀取。這些最佳化可以大幅減少變更包含可用空間的資料庫檔案時發生的 I/O 量。

7.4. 單頁更新和原子扇區寫入

從 SQLite 版本 3.5.0 開始,新的虛擬檔案系統 (VFS) 介面包含名為 xDeviceCharacteristics 的方法,用於報告底層大量儲存裝置可能具有的特殊屬性。xDeviceCharacteristics 可能報告的特殊屬性包括執行原子扇區寫入的能力。

請回想,SQLite 預設會假設扇區寫入是線性的,但不是原子的。線性寫入從扇區的一端開始,並逐位元組變更資訊,直到到達扇區的另一端。如果在線性寫入過程中發生斷電,扇區的一部分可能會被修改,而另一端則保持不變。在原子扇區寫入中,整個扇區會被覆寫,否則扇區中的任何內容都不會被變更。

我們相信大多數現代磁碟機都實作了原子區塊寫入。當電源中斷時,磁碟機會使用儲存在電容器和/或磁碟盤的角動量中的能量來提供電源,以完成正在進行的任何操作。儘管如此,在寫入系統呼叫和板載磁碟機電子設備之間有許多層,因此我們在 Unix 和 w32 VFS 實作中採用安全的方法,並假設區塊寫入不是原子的。另一方面,對其檔案系統有更多控制權的裝置製造商可能希望考慮啟用 xDeviceCharacteristics 的原子寫入屬性,如果他們的硬體確實執行原子寫入。

當區塊寫入是原子的,而且資料庫的頁面大小與區塊大小相同,而且當資料庫變更只觸及單一資料庫頁面時,SQLite 會略過整個日誌記錄和同步處理,並直接將修改後的頁面寫入資料庫檔案。資料庫檔案第一個頁面的變更計數器會單獨修改,因為如果在變更計數器更新之前電源中斷,不會造成任何損害。

7.5. 具有安全追加語意的檔案系統

SQLite 3.5.0 中引入的另一項最佳化利用了底層磁碟的「安全追加」行為。回想一下,SQLite 假設當資料附加到檔案(特別是回滾日誌)時,檔案的大小會先增加,然後內容才會寫入。因此,如果在檔案大小增加後但內容寫入之前電源中斷,檔案將會包含無效的「垃圾」資料。VFS 的 xDeviceCharacteristics 方法可能會指出檔案系統實作了「安全追加」語意。這表示內容會在檔案大小增加之前寫入,因此電源中斷或系統崩潰不可能將垃圾引入回滾日誌中。

當檔案系統指示安全附加語意時,SQLite 總會將 -1 的特殊值儲存在回滾日誌標頭的頁面計數中。-1 頁面計數值會告知任何嘗試回滾日誌的程序,日誌中的頁面數應從日誌大小計算。此 -1 值永不變更。因此,當提交發生時,我們會儲存單一快取操作和日誌檔案第一頁的區塊寫入。此外,當快取溢位發生時,我們不再需要將新的日誌標頭附加到日誌的結尾;我們可以簡單地繼續將新頁面附加到現有日誌的結尾。

7.6. 持久性回滾日誌

在許多系統上,刪除檔案是一項昂貴的操作。因此,作為最佳化,SQLite 可以設定為避免 第 3.11 節 的刪除操作。為了提交交易,檔案並非刪除日誌檔案,而是將檔案截斷為零位元組長度或將其標頭覆寫為零。將檔案截斷為零長度可避免對包含該檔案的目錄進行修改,因為檔案並未從目錄中移除。覆寫標頭還有額外的節省,包括不必更新檔案的長度(在許多系統上的「inode」中)和不必處理新釋放的磁碟區塊。此外,在下一個交易中,日誌將透過覆寫現有內容而不是附加新內容到檔案結尾來建立,而覆寫通常比附加快得多。

SQLite 可以設定為透過使用 journal_mode PRAGMA 將「PERSIST」日誌模式設定為覆寫日誌標頭為零而不是刪除日誌檔案來提交交易。例如

PRAGMA journal_mode=PERSIST;

在許多系統上,使用持久性日誌模式會提供顯著的效能提升。當然,缺點是日誌檔案會留在磁碟上,使用磁碟空間並在交易提交後很長一段時間內使目錄雜亂。刪除持久性日誌檔案的唯一安全方法是提交交易,並將日誌模式設定為 DELETE

PRAGMA journal_mode=DELETE;
BEGIN EXCLUSIVE;
COMMIT;

請注意,不要透過任何其他方式刪除持續性日誌檔案,因為日誌檔案可能會是熱的,這種情況下刪除它會損毀相對應的資料庫檔案。

從 SQLite 版本 3.6.4(2008-10-15)開始,也支援 TRUNCATE 日誌模式

PRAGMA journal_mode=TRUNCATE;

在截斷日誌模式中,交易會透過將日誌檔案截斷為零長度,而不是刪除日誌檔案(如 DELETE 模式)或將標頭歸零(如 PERSIST 模式)來提交。TRUNCATE 模式與 PERSIST 模式有相同的優點,即不需要更新包含日誌檔案和資料庫的目錄。因此,截斷檔案通常比刪除它更快。TRUNCATE 的另一個優點是,它不會接著進行系統呼叫(例如:fsync())來同步變更到磁碟。如果它這樣做,可能會更安全。但在許多現代檔案系統上,截斷是一個原子性和同步操作,因此我們認為 TRUNCATE 通常會在面對電源故障時是安全的。如果您不確定 TRUNCATE 是否會在您的檔案系統上同步和原子化,並且您的資料庫在截斷操作期間能存活電源損失或作業系統崩潰對您很重要,那麼您可能會考慮使用不同的日誌模式。

在具有同步檔案系統的嵌入式系統上,TRUNCATE 會比 PERSIST 導致更慢的行為。提交操作的速度相同。但在 TRUNCATE 之後,後續交易會更慢,因為覆寫現有內容比附加到檔案結尾更快。新的日誌檔案條目在 TRUNCATE 之後將始終附加,但在 PERSIST 中通常會覆寫。

8. 測試原子提交行為

SQLite 的開發人員確信它在面對電源故障和系統崩潰時是強健的,因為自動測試程序會廣泛檢查 SQLite 從模擬電源損失中復原的能力。我們稱這些為「崩潰測試」。

SQLite 中的崩潰測試使用已修改的 VFS,它可以模擬在斷電或作業系統崩潰期間發生的檔案系統損壞類型。崩潰測試 VFS 可以模擬不完整的區段寫入、由於寫入未完成而填滿垃圾資料的頁面,以及順序錯誤的寫入,所有這些都在測試情境期間的不同時間點發生。崩潰測試會反覆執行交易,改變模擬斷電發生的時間和造成的損壞特性。然後,每個測試在模擬崩潰後重新開啟資料庫,並驗證交易是否完全發生或根本沒有發生,以及資料庫是否處於完全一致的狀態。

SQLite 中的崩潰測試發現了復原機制中許多非常細微的錯誤(現在已修正)。其中一些錯誤非常模糊,而且不太可能只使用程式碼檢查和分析技術就能找到。從這個經驗中,SQLite 的開發人員確信任何其他不使用類似崩潰測試系統的資料庫系統都可能包含未偵測到的錯誤,這些錯誤將導致系統崩潰或斷電後資料庫損毀。

9. 可能出錯的事項

SQLite 中的原子提交機制已被證明是強健的,但它可能會被足夠有創意的對手或足夠損壞的作業系統實作所規避。本節說明 SQLite 資料庫可能因斷電或系統崩潰而損毀的幾種方式。(另請參閱:如何損毀您的資料庫檔案。)

9.1. 損壞的鎖定實作

SQLite 使用檔案系統鎖定來確保一次只有一個程序和資料庫連線嘗試修改資料庫。檔案系統鎖定機制實作在 VFS 層中,並且對於每個作業系統都不同。SQLite 依賴於此實作是正確的。如果出現問題,並且兩個或多個程序能夠同時寫入同一個資料庫檔案,可能會造成嚴重的損壞。

我們已收到關於 Windows 網路檔案系統和 NFS 的實作報告,其中鎖定功能已微妙地中斷。我們無法驗證這些報告,但由於在網路檔案系統上難以正確鎖定,因此我們沒有理由懷疑它們。建議您避免在網路檔案系統上使用 SQLite,因為效能會很慢。但是,如果您必須使用網路檔案系統來儲存 SQLite 資料庫檔案,請考慮使用次要鎖定機制,以防止同時寫入同一個資料庫,即使原生檔案系統鎖定機制發生故障也是如此。

預先安裝在 Apple Mac OS X 電腦上的 SQLite 版本包含已延伸的 SQLite 版本,可使用替代鎖定策略,適用於 Apple 支援的所有網路檔案系統。只要所有程序都以相同方式存取資料庫檔案,Apple 使用的這些延伸功能就能順利運作。遺憾的是,鎖定機制並不會互相排除,因此如果一個程序使用 (例如) AFP 鎖定存取檔案,而另一個程序 (可能在不同的機器上) 使用點檔案鎖定,這兩個程序可能會發生衝突,因為 AFP 鎖定不會排除點檔案鎖定,反之亦然。

9.2. 磁碟沖寫不完全

SQLite 在 Unix 上使用 fsync() 系統呼叫,在 w32 上使用 FlushFileBuffers() 系統呼叫,以將檔案系統緩衝區同步到磁碟氧化物,如 步驟 3.7步驟 3.10 所示。不幸的是,我們收到報告指出,這些介面在許多系統上並未如廣告所述運作。我們聽說在某些 Windows 版本上,可以使用登錄設定完全停用 FlushFileBuffers()。我們被告知,某些歷史版本的 Linux 包含 fsync() 版本,在某些檔案系統上是無操作的。即使在據稱 FlushFileBuffers() 和 fsync() 正在運作的系統上,IDE 磁碟控制也常常說謊,表示資料已到達氧化物,而實際上資料仍只保存在易失性控制快取中。

在 Mac 上,您可以設定這個 pragma

PRAGMA fullfsync=ON;

在 Mac 上設定 fullfsync 將保證資料在快取時確實會被推送到磁碟區。但 fullfsync 的實作涉及重設磁碟控制。因此,它不僅非常慢,還會減慢其他不相關的磁碟 I/O。因此不建議使用。

9.3. 部分檔案刪除

SQLite 假設從使用者程序的角度來看,檔案刪除是一個原子操作。如果在檔案刪除過程中斷電,則在電源復原後,SQLite 預期會看到整個檔案及其所有原始資料完好無缺,或者完全找不到檔案。在不這樣運作的系統上,交易可能不是原子的。

9.4. 寫入檔案的垃圾

SQLite 資料庫檔案是普通的磁碟檔案,可以由普通的使用者程序開啟和寫入。流氓程序可以開啟 SQLite 資料庫並填入損毀的資料。損毀的資料也可能因作業系統或磁碟控制器的錯誤而引入 SQLite 資料庫中;特別是因斷電而觸發的錯誤。SQLite 無法防禦這些類型的問題。

9.5. 刪除或重新命名熱日誌

如果發生當機或斷電,而熱日誌仍然留在磁碟上,則在資料庫檔案由另一個 SQLite 程序開啟並回滾之前,原始資料庫檔案和熱日誌必須以其原始名稱保留在磁碟上。在 步驟 4.2 的復原過程中,SQLite 會尋找與正在開啟的資料庫位於同一個目錄中的檔案,並且其名稱是從正在開啟的檔案名稱衍生而來,藉此找到熱日誌。如果原始資料庫檔案或熱日誌已被移動或重新命名,則熱日誌將不會被看到,而資料庫也不會被回滾。

我們懷疑 SQLite 復原的常見失敗模式如下:發生斷電。在電力恢復後,一個好心的使用者或系統管理員開始在磁碟上尋找損壞。他們看到他們的資料庫檔案名為「important.data」。這個檔案對他們來說可能很熟悉。但在當機後,還有一個名為「important.data-journal」的熱日誌。然後使用者刪除熱日誌,認為他們正在幫助清理系統。我們不知道除了使用者教育之外,還有什麼方法可以防止這種情況發生。

如果有多個(硬或符號)連結到資料庫檔案,則將使用開啟檔案的連結名稱建立日誌。如果發生當機,並且使用不同的連結再次開啟資料庫,則不會找到熱日誌,也不會發生回滾。

有時,電源故障會導致檔案系統損毀,導致最近變更的檔案名稱遺失,而檔案會移至「/lost+found」目錄。當發生這種情況時,將找不到熱日誌,且不會執行復原。SQLite 會嘗試在同步日誌檔案的同時開啟並同步包含回滾日誌的目錄,以防止這種情況發生。然而,檔案移至 /lost+found 的情況可能是因為不相關的程序在與主資料庫檔案相同的目錄中建立不相關的檔案所造成的。由於這不受 SQLite 控制,因此 SQLite 無法採取任何措施來防止這種情況發生。如果您在容易發生這種檔案系統名稱空間損毀的系統上執行(我們認為大多數現代的日誌檔案系統都具有免疫力),則您可能需要考慮將每個 SQLite 資料庫檔案放在其自己的私人子目錄中。

10. 未來方向和結論

時不時地,有人會發現 SQLite 中原子提交機制的新的失敗模式,而開發人員必須進行修補。這種情況發生的次數越來越少,而失敗模式也變得越來越不明顯。但仍然愚蠢地認為 SQLite 的原子提交邏輯完全沒有錯誤。開發人員致力於盡快修正這些錯誤。

開發人員也在尋找優化提交機制的新方法。Unix(Linux 和 Mac OS X)和 Windows 的現行 VFS 實作對這些系統的行為做出了悲觀的假設。在諮詢了這些系統運作方式的專家後,我們可能能夠放寬對這些系統的一些假設,並讓它們執行得更快。特別是,我們懷疑大多數現代檔案系統都表現出安全的附加屬性,而其中許多檔案系統可能支援原子扇區寫入。但在確定這一點之前,SQLite 將採取保守的方式,並假設最壞的情況。

此頁面最後修改於 2022-12-31 21:51:03 UTC