小巧、快速、可靠。
任選三項。

SQLite 中的隔離

資料庫的「隔離」屬性決定由一個操作對資料庫所做的變更何時對其他並發操作可見。

資料庫連線之間的隔離

如果使用兩個不同的資料庫連線(兩個不同的sqlite3物件,由個別呼叫sqlite3_open()傳回)讀取和寫入同一個資料庫,且兩個資料庫連線沒有共用快取,則讀取端只能看到寫入端已提交的完整交易。寫入端尚未提交的部分變更對讀取端而言是不可見的。這項原則適用於兩個資料庫連線在同一個執行緒、同一個程序的不同執行緒,或不同程序中。這是 SQL 資料庫系統的常見且預期行為。

前一段落(個別資料庫連線彼此隔離)在共用快取模式中也適用,只要read_uncommitted pragma保持關閉即可。read_uncommitted pragma預設為關閉,因此如果應用程式沒有任何動作將其開啟,它將保持關閉。因此,除非使用read_uncommitted pragma變更預設行為,否則一個資料庫連線所做的變更對共用同一個快取的不同資料庫連線上的讀取端而言是不可見的,直到寫入端提交其交易。

如果兩個資料庫連線共用相同的快取,且讀取器已啟用 read_uncommitted pragma,則讀取器將能夠在寫入器交易提交之前看到寫入器所做的變更。結合使用 共用快取模式read_uncommitted pragma 是唯一讓一個資料庫連線能夠看到不同資料庫連線上的未提交變更的方法。在所有其他情況下,個別資料庫連線彼此完全隔離。

除了啟用 PRAGMA read_uncommitted共用快取 資料庫連線案例外,SQLite 中的所有交易都顯示「可序列化」隔離。SQLite 透過實際序列化寫入來實作可序列化交易。一次只能有一個寫入器寫入 SQLite 資料庫。可以同時開啟多個資料庫連線,且所有這些資料庫連線都可以寫入資料庫檔案,但它們必須輪流執行。SQLite 使用鎖定自動序列化寫入;這是使用 SQLite 的應用程式不需要擔心的部分。

隔離與並行

SQLite 使用與資料庫檔案出現在同一個目錄中的暫時性記錄檔來實作隔離和並行控制(以及原子性)。有兩種主要的「記錄模式」。較舊的「回滾模式」對應於使用「DELETE」、「PERSIST」或「TRUNCATE」選項到 journal_mode pragma。在回滾模式中,變更會直接寫入資料庫檔案,同時建構一個獨立的回滾記錄檔,如果交易回滾,它能夠將資料庫還原到其原始狀態。回滾模式(特別是 DELETE 模式,表示回滾記錄檔會在每個交易結束時從磁碟中刪除)是目前的預設行為。

3.7.0 版(2010-07-21)起,SQLite 也支援「WAL 模式」。在 WAL 模式中,變更不會寫入原始資料庫檔案。相反地,變更會進入一個獨立的「預寫式記錄檔」或「WAL」檔案。稍後,在交易提交後,這些變更會從 WAL 檔案移回原始資料庫,這個動作稱為「檢查點」。透過執行「PRAGMA journal_mode=WAL」來啟用 WAL 模式。

在回滾模式中,SQLite 透過鎖定資料庫檔案,並在每個寫入交易進行時,防止其他資料庫連線讀取,來實作隔離。讀取器可以在寫入開始時,在任何內容沖刷到磁碟之前,以及所有變更仍保留在寫入器私人記憶體空間時,保持作用中。但在對磁碟上的資料庫檔案進行任何變更之前,必須將所有讀取器(暫時)驅逐,以讓寫入器獨占存取資料庫檔案。因此,讀取器無法看到不完整的交易,因為在交易寫入磁碟時,讀取器會被鎖定在資料庫之外。只有在交易完全寫入、同步到磁碟並提交後,才會允許讀取器返回資料庫。因此,讀取器永遠不會有機會看到部分寫入的變更。

WAL 模式允許同時進行讀取和寫入。它可以做到這一點,因為變更不會覆寫原始資料庫檔案,而是進入獨立的預寫式記錄檔檔案。這表示讀取器可以繼續從原始資料庫檔案讀取舊的、原始的、未變更的內容,同時寫入器會附加到預寫式記錄檔。在 WAL 模式 中,SQLite 展現「快照隔離」。當讀取交易開始時,該讀取器會繼續看到資料庫檔案的不變「快照」,就像在讀取交易開始的那一刻一樣。在讀取交易作用中提交的任何寫入交易,對讀取交易來說仍然是不可見的,因為讀取器看到的是資料庫檔案在先前時間點的快照。

範例:假設有兩個資料庫連線 X 和 Y。X 使用 BEGIN 開始一個讀取交易,接著是一個或多個 SELECT 陳述式。然後 Y 出現並執行一個 UPDATE 陳述式來修改資料庫。X 可以在 Y 修改的記錄上執行 SELECT,但 X 會看到較舊的未修改項目,因為當 X 持有讀取交易時,Y 的變更對 X 都是不可見的。如果 X 想看到 Y 所做的變更,則 X 必須結束其讀取交易並開始一個新的交易(執行 COMMIT 接著是另一個 BEGIN)。

另一個範例:X 使用 BEGINSELECT 開始一個讀取交易,然後 Y 使用 UPDATE 對資料庫進行變更。然後 X 嘗試使用 UPDATE 對資料庫進行變更。X 嘗試將其交易從讀取交易升級為寫入交易的嘗試會失敗,並出現 SQLITE_BUSY_SNAPSHOT 錯誤,因為 X 所檢視的資料庫快照不再是資料庫的最新版本。如果允許 X 寫入,它將會分岔資料庫檔案的歷程記錄,這是 SQLite 不支援的。為了讓 X 寫入資料庫,它必須先釋放其快照(例如使用 ROLLBACK),然後使用後續的 BEGIN 開始新的交易。

如果 X 開始一個交易,一開始只會讀取,但 X 知道它最終會想要寫入,並且不希望因為另一個連線跳到它前面而產生可能的 SQLITE_BUSY_SNAPSHOT 錯誤,那麼 X 可以發出 BEGIN IMMEDIATE 來開始它的交易,而不是只是一個普通的 BEGIN。 BEGIN IMMEDIATE 命令繼續並開始一個寫入交易,因此會封鎖所有其他寫入者。如果 BEGIN IMMEDIATE 操作成功,那麼該交易中後續的操作將永遠不會失敗,並出現 SQLITE_BUSY 錯誤。

在同一個資料庫連線上的操作之間沒有隔離

SQLite 在不同資料庫連線上的操作之間提供隔離。但是,在同一個資料庫連線中發生的操作之間沒有隔離。

換句話說,如果 X 使用 BEGIN IMMEDIATE 開始一個寫入交易,然後發出一個或多個 UPDATEDELETE 和/或 INSERT 陳述式,那麼這些變更對在資料庫連線 X 中評估的後續 SELECT 陳述式是可見的。不同資料庫連線 Y 上的 SELECT 陳述式將不會顯示任何變更,直到 X 交易提交。但 X 中的 SELECT 陳述式將在提交之前顯示變更。

在單一資料庫連線 X 中,SELECT 陳述式總是會看到資料庫中在 SELECT 陳述式開始前完成的所有變更,無論是已提交或未提交。而且,SELECT 陳述式顯然不會看到 SELECT 陳述式完成後發生的任何變更。但是,在 SELECT 陳述式執行期間發生的變更呢?如果啟動 SELECT 陳述式,且 sqlite3_step() 介面逐步執行其輸出的大約一半,然後應用程式執行一些 UPDATE 陳述式來修改 SELECT 陳述式正在讀取的表格,然後再呼叫 sqlite3_step() 來完成 SELECT 陳述式?SELECT 陳述式的後續步驟會看到 UPDATE 所做的變更嗎?答案是此行為未定義。特別是,SELECT 陳述式是否會看到並行變更取決於執行 SQLite 的版本、資料庫檔案的架構、是否已執行 ANALYZE,以及查詢的詳細資料。在某些情況下,也可能取決於資料庫檔案的內容。沒有好的方法可以知道 SELECT 陳述式是否會看到在 SELECT 陳述式啟動後由相同資料庫連線對資料庫所做的變更。因此,開發人員應勤奮地避免撰寫對在這種情況下將會發生什麼事做出假設的應用程式。

如果應用程式對單一資料表發出 SELECT 陳述式,例如「SELECT rowid, * FROM table WHERE ...」,並開始使用 sqlite3_step() 逐步執行該陳述式的輸出,並檢查每一列,那麼應用程式可以安全地使用「DELETE FROM table WHERE rowid=?」刪除目前列或任何先前的列。應用程式刪除預期會在查詢中稍後出現但尚未出現的列也是安全的(在不會損害資料庫的意義上)。不過,如果未來的列被刪除,那麼該列可能會在後續的 sqlite3_step() 中出現,即使它已被聲稱刪除。或者它可能不會出現。該行為未定義。應用程式也可以在 SELECT 陳述式執行時將新列 INSERT 到資料表中,但新列是否會在查詢的後續 sqlite3_step() 中出現未定義。應用程式可以 UPDATE 目前列或任何先前的列,儘管這麼做可能會導致該列在後續的 sqlite3_step() 中重新出現。只要應用程式準備好處理這些模糊性,這些操作本身就是安全的,而且不會損害資料庫檔案。

對於前兩段的用途,兩個具有相同 共用快取 並已啟用 PRAGMA read_uncommitted 的資料庫連線被視為同一個資料庫連線。

摘要

  1. SQLite 中的交易是可序列化 (SERIALIZABLE)。

  2. 在一個資料庫連線中所做的變更在提交之前對所有其他資料庫連線都是不可見的。

  3. 查詢會看到在查詢開始前同一個資料庫連線中完成的所有變更,不論這些變更是否已提交。

  4. 如果在查詢開始執行後但查詢完成前,同一個資料庫連線發生變更,則查詢是否會看到這些變更是不確定的。

  5. 如果在查詢開始執行後但查詢完成前,同一個資料庫連線發生變更,則查詢可能會多次傳回已變更的列,或可能會傳回先前已刪除的列。

  6. 對於前四個項目而言,使用相同共用快取且啟用PRAGMA read_uncommitted的兩個資料庫連線,會被視為同一個資料庫連線,而非兩個不同的資料庫連線。