此文件最初於 2004 年初建立,當時 SQLite 2 版仍廣泛使用,並撰寫來向已熟悉 SQLite 2 版的讀者介紹 SQLite 3 版的新概念。但現今,大多數此文件的讀者可能從未見過 SQLite 2 版,僅熟悉 SQLite 3 版。儘管如此,此文件仍持續作為 SQLite 3 版中資料庫檔案鎖定運作方式的權威參考。
文件僅說明舊有回滾模式交易機制的鎖定。較新的 預寫記錄 或 WAL 模式 的鎖定另行說明。
SQLite 3.0.0 版 引進新的鎖定和記錄機制,旨在改善 SQLite 2 版的並發性,並減少寫入者飢餓問題。新機制也允許多個資料庫檔案參與交易的原子提交。此文件說明新的鎖定機制。目標讀者為想要了解和/或修改分頁程式碼的程式設計師,以及審查人員致力於驗證 SQLite 3 版的設計。
鎖定和並發控制由分頁模組處理。分頁模組負責讓 SQLite「ACID」(原子性、一致性、隔離性和持久性)。分頁模組確保變更一次全部發生,所有變更都發生或都不發生,兩個或多個程序不會同時嘗試以不兼容的方式存取資料庫,並且在變更寫入後,它們會持續存在,直到明確刪除。分頁還提供磁碟檔案部分內容的記憶體快取。
分頁與 B 樹、文字編碼、索引等的詳細資料無關。從分頁的角度來看,資料庫由一個由大小一致區塊組成的單一檔案組成。每個區塊稱為「頁面」,通常大小為 1024 位元組。頁面從 1 開始編號。因此,資料庫的前 1024 位元組稱為「頁面 1」,第二個 1024 位元組稱為「頁面 2」,依此類推。所有其他編碼詳細資料都由程式庫的較高層級處理。分頁使用多個模組(範例: os_unix.c、 os_win.c)與作業系統通訊,這些模組提供作業系統服務的統一抽象。
分頁模組有效地控制個別執行緒或個別程序的存取,或同時控制兩者。在本文檔中,每當寫入「程序」一詞時,您都可以用「執行緒」一詞替換,而不會改變陳述的真實性。
從單一程序的角度來看,資料庫檔案可以處於五種鎖定狀態之一
作業系統介面層了解並追蹤上述五種鎖定狀態。分頁模組只追蹤這五種鎖定狀態中的四種。待處理鎖定在取得獨佔鎖定的路徑上永遠只是一個暫時的踏腳石,因此分頁模組不會追蹤待處理鎖定。
當一個處理程序想要變更一個資料庫檔案(且它不在 WAL 模式中),它會先將原始未變更的資料庫內容記錄在一個回滾日誌中。回滾日誌是一個普通的磁碟檔案,它總是位於與資料庫檔案相同的目錄或資料夾中,且具有與資料庫檔案相同的名稱,並加上一個 -journal 字尾。回滾日誌也會記錄資料庫的初始大小,以便在資料庫檔案成長時,它可以在回滾時被截斷回其原始大小。
如果 SQLite 同時處理多個資料庫(使用 ATTACH 指令),則每個資料庫都有自己的回滾日誌。但還有一個名為超級日誌的獨立總和日誌。超級日誌不包含用於回滾變更的頁面資料。相反地,超級日誌包含每個 ATTACH 資料庫的個別資料庫回滾日誌的名稱。每個個別資料庫回滾日誌也包含超級日誌的名稱。如果沒有 ATTACH 的資料庫(或如果沒有 ATTACH 的資料庫參與目前的交易),則不會建立超級日誌,而正常的回滾日誌會在通常保留用於記錄超級日誌名稱的地方包含一個空字串。
如果需要回滾一個回滾日誌以還原其資料庫的完整性,則稱該回滾日誌為熱。當一個處理程序正在進行資料庫更新的過程中,而程式或作業系統崩潰或電源故障阻止更新完成時,就會建立一個熱日誌。熱日誌是一個例外情況。熱日誌存在於從崩潰和電源故障中復原。如果一切都正常運作(也就是說,如果沒有崩潰或電源故障),你永遠不會得到一個熱日誌。
如果沒有超級日誌,則如果一個日誌存在且具有非零標頭,且其對應的資料庫檔案沒有保留鎖,則該日誌是熱的。如果在檔案日誌中指定了超級日誌,則如果其超級日誌存在且對應的資料庫檔案上沒有保留鎖,則檔案日誌是熱的。瞭解何時日誌是熱的非常重要,因此以下規則將以項目符號重複
在從資料庫檔案讀取之前,SQLite 總是會檢查該資料庫檔案是否具有熱日誌。如果檔案確實有熱日誌,則在讀取檔案之前會先回滾日誌。這樣,我們可以確保在讀取資料庫檔案之前,該檔案處於一致狀態。
當一個程序想要從資料庫檔案讀取時,它會遵循以下步驟順序
在上述演算法成功完成後,可以安全地從資料庫檔案讀取。一旦所有讀取完成,SHARED 鎖定就會取消。
過時的超級日誌檔是指不再用於任何用途的超級日誌檔。沒有要求必須刪除過時的超級日誌檔。這樣做的唯一原因是釋放磁碟空間。
如果沒有個別檔案日誌檔指向超級日誌檔,則超級日誌檔就是過時的。要找出超級日誌檔是否過時,我們首先讀取超級日誌檔以取得所有檔案日誌檔的名稱。然後我們檢查這些檔案日誌檔中的每一個。如果超級日誌檔中命名的任何檔案日誌檔存在並指向超級日誌檔,則超級日誌檔並非過時。如果所有檔案日誌檔都遺失或指向其他超級日誌檔,或根本沒有指向任何超級日誌檔,則我們正在測試的超級日誌檔就是過時的,可以安全地刪除。
若要寫入資料庫,程序必須先取得 SHARED 鎖定,如上所述(如果有一個熱日誌,可能會回滾未完成的變更)。取得 SHARED 鎖定後,必須取得 RESERVED 鎖定。RESERVED 鎖定表示程序打算在未來的某個時間點寫入資料庫。一次只能有一個程序持有 RESERVED 鎖定。但是,在持有 RESERVED 鎖定的同時,其他程序可以繼續讀取資料庫。
如果想要寫入的程序無法取得 RESERVED 鎖定,這表示另一個程序已經擁有 RESERVED 鎖定。在這種情況下,寫入嘗試會失敗並傳回 SQLITE_BUSY。
取得 RESERVED 鎖定後,想要寫入的程序會建立一個回滾日誌。日誌的標頭會初始化為資料庫檔案的原始大小。日誌標頭中的空間也會保留給超級日誌名稱,儘管超級日誌名稱最初是空的。
在對資料庫的任何頁面進行變更之前,程序會將該頁面的原始內容寫入回滾日誌。對頁面的變更最初會保留在記憶體中,不會寫入磁碟。原始資料庫檔案保持不變,這表示其他程序可以繼續讀取資料庫。
最後,寫入程序會想要更新資料庫檔案,可能是因為記憶體快取已滿,或者它已準備提交變更。在發生這種情況之前,寫入器必須確保沒有其他程序正在讀取資料庫,並且回滾日誌資料安全地儲存在磁碟表面上,以便在發生斷電時可以使用它來回滾未完成的變更。步驟如下
如果寫入資料庫檔案的原因是記憶體快取已滿,則寫入器不會立即提交。相反地,寫入器可能會繼續對其他頁面進行變更。在後續變更寫入資料庫檔案之前,必須再次將回滾記錄沖洗到磁碟。另請注意,寫入器最初為了寫入資料庫而取得的 EXCLUSIVE 鎖定必須保留,直到所有變更都提交為止。這表示從記憶體快取第一次溢位到磁碟,直到交易提交為止,沒有其他程序可以存取資料庫。
當寫入器準備提交其變更時,它會執行下列步驟
一旦資料庫檔案釋放了 PENDING 鎖定,其他程序便可以開始再次讀取資料庫。在目前的實作中,RESERVED 鎖定也會被釋放,但這對正確操作而言並非必要。
如果一個交易涉及多個資料庫,則會使用更複雜的提交順序,如下所示
在 SQLite 版本 2 中,如果許多程序正在從資料庫中讀取,則可能永遠沒有沒有活動讀取器的時間。如果資料庫上始終至少有一個讀取鎖定,則沒有任何程序能夠對資料庫進行變更,因為不可能取得寫入鎖定。這種情況稱為寫入器飢餓。
SQLite 版本 3 透過使用 PENDING 鎖定來避免寫入者挨餓。PENDING 鎖定允許現有的讀取器繼續,但會阻止新的讀取器連線到資料庫。因此,當處理程序想要寫入忙碌的資料庫時,它可以設定 PENDING 鎖定,這將阻止新的讀取器進入。假設現有的讀取器最終會完成,所有 SHARED 鎖定最終都會清除,寫入器將有機會進行變更。
分頁模組非常強大,但它可能會被破壞。本節嘗試找出並說明風險。(另請參閱 可能出錯的事項 部分,有關 原子提交 一文的說明。
顯然,將不正確的資料引入資料庫檔案或日誌中段的硬體或作業系統故障將會造成問題。同樣地,如果流氓處理程序開啟資料庫檔案或日誌,並將格式錯誤的資料寫入其中段,則資料庫將會損毀。對於這類問題,沒有太多可以採取的措施,因此不再進一步說明。
SQLite 使用 POSIX 建議鎖定在 Unix 上實作鎖定。在 Windows 上,它使用 LockFile()、LockFileEx() 和 UnlockFile() 系統呼叫。SQLite 假設這些系統呼叫都如廣告宣傳般運作。如果並非如此,則可能會造成資料庫毀損。您應該注意,已知 POSIX 建議鎖定在許多 NFS 實作(包括 Mac OS X 的最新版本)上存在錯誤,甚至未實作,而且有報告指出 Windows 下的網路檔案系統存在鎖定問題。您最好的防禦措施是不要對網路檔案系統上的檔案使用 SQLite。
SQLite 使用 fsync() 系統呼叫將資料沖洗到 Unix 下的磁碟,並使用 FlushFileBuffers() 在 Windows 下執行相同的動作。SQLite 再次假設這些作業系統服務會如廣告宣傳般運作。但據報導,fsync() 和 FlushFileBuffers() 並非總是正確運作,特別是在某些網路檔案系統或便宜的 IDE 磁碟上。顯然,某些 IDE 磁碟製造商的控制器晶片會報告資料已到達磁碟表面,但實際上資料仍位於磁碟機電子設備中的揮發性快取記憶體中。也有報告指出,Windows 有時會選擇忽略 FlushFileBuffers(),原因不明。作者無法驗證這些報告的任何內容。但如果它們屬實,則表示資料庫毀損有可能在意外斷電後發生。這些是 SQLite 無法防禦的硬體和/或作業系統錯誤。
如果 Linux ext3 檔案系統在 /etc/fstab 中未加上「barrier=1」選項,且磁碟機寫入快取已啟用,則在斷電或作業系統當機後,檔案系統可能會毀損。是否會毀損,取決於磁碟控制硬體的詳細資料;便宜的消費級磁碟較容易毀損,而具備非揮發性寫入快取等進階功能的企業級儲存裝置則較不容易發生問題。多位 ext3 專家 證實此行為。我們得知,大多數 Linux 發行版並未使用 barrier=1,也未停用寫入快取,因此大多數 Linux 發行版都容易發生此問題。請注意,這是作業系統和硬體問題,SQLite 無法解決。 其他資料庫引擎 也曾發生過相同問題。
如果發生當機或斷電,導致熱記錄檔產生,但該記錄檔卻被刪除,下一個開啟資料庫的程序將不知道資料庫中包含需要還原的變更。還原不會發生,而且資料庫將維持在不一致的狀態。還原記錄檔可能會因許多原因而被刪除
上述最後 (第四) 個項目值得額外說明。當 SQLite 在 Unix 上建立日誌檔案時,它會開啟包含該檔案的目錄,並對目錄呼叫 fsync(),以將目錄資訊推送到磁碟。但假設在停電時,其他一些程序正在將不相關的檔案新增或移除到包含資料庫和日誌的目錄中。這個其他程序的假設不相關動作可能會導致日誌檔案從目錄中刪除並移至「lost+found」。這是一個不太可能發生的情況,但有可能發生。最好的防禦措施是使用日誌檔案系統或將資料庫和日誌保存在一個目錄中。
對於涉及多個資料庫和超級日誌的提交,如果各個資料庫位於不同的磁碟區,且在提交期間發生停電,則當機器重新啟動時,磁碟可能會以不同的名稱重新掛載。或者有些磁碟可能根本沒有掛載。發生這種情況時,個別檔案日誌和超級日誌可能無法找到彼此。此情況最糟糕的結果是提交不再是原子性的。有些資料庫可能會回滾,而另一些資料庫可能不會。所有資料庫都將繼續保持自我一致性。為了解決此問題,請將所有資料庫保留在同一個磁碟區上和/或在停電後使用完全相同的名稱重新掛載磁碟。
SQLite 版本 3 中對鎖定和並發控制的變更也對 SQL 語言層級的事務運作方式引入了一些細微的變更。預設情況下,SQLite 版本 3 以自動提交模式運作。在自動提交模式中,與目前資料庫連線相關的所有作業完成後,所有對資料庫的變更都會提交。
SQL 指令「BEGIN TRANSACTION」(TRANSACTION 關鍵字為選用)用於讓 SQLite 退出自動提交模式。請注意,BEGIN 指令不會取得資料庫的任何鎖定。在 BEGIN 指令之後,當第一個 SELECT 陳述式執行時,將取得 SHARED 鎖定。當第一個 INSERT、UPDATE 或 DELETE 陳述式執行時,將取得 RESERVED 鎖定。直到記憶體快取填滿且必須溢位到磁碟,或直到交易提交為止,不會取得任何 EXCLUSIVE 鎖定。藉此方式,系統會延遲封鎖對檔案的讀取存取,直到最後一刻。
SQL 指令「COMMIT」並未實際將變更提交到磁碟。它只是將自動提交重新開啟。然後,在指令結束時,常規的自動提交邏輯會接手,並導致實際提交到磁碟發生。SQL 指令「ROLLBACK」也透過重新開啟自動提交來運作,但它也會設定一個標記,告訴自動提交邏輯回滾,而不是提交。
如果 SQL COMMIT 指令開啟自動提交,而自動提交邏輯隨後嘗試提交變更,但因其他處理程序持有 SHARED 鎖定而失敗,則自動提交會自動關閉。這允許使用者在 SHARED 鎖定有機會清除後,稍後重試 COMMIT。
如果同時對同一個 SQLite 資料庫連線執行多個指令,則自動提交會延遲到最後一個指令完成為止。例如,如果正在執行 SELECT 陳述式,則指令的執行會在傳回結果的每一列時暫停。在此暫停期間,可以對資料庫中的其他表格執行其他 INSERT、UPDATE 或 DELETE 指令。但這些變更都不會提交,直到原始的 SELECT 陳述式完成為止。
此頁面最後修改於 2023-10-10 17:29:48 UTC