該SQL標準定義了四種交易隔離級別。最嚴格的是可序列化,該標準在一個段落中定義,該段落指出,一組可序列化交易的任何並行執行保證產生與以某種順序一次執行一個交易相同的效果。其他三個級別根據現象定義,這些現象是由並行交易之間的交互產生的,並且在每個級別都不會發生。該標準指出,由於可序列化的定義,在該級別不可能發生這些現象。(這並不奇怪——如果交易的效果必須與一次執行一個交易的效果一致,您怎麼會看到任何由交互引起的現象呢?)
在各個級別禁止的現象是
SQL 標準和 PostgreSQL 實作的交易隔離級別在表 13.1中描述。
表 13.1. 交易隔離級別
隔離級別 | 髒讀 | 不可重複讀取 | 幻讀 | 序列化異常 |
---|---|---|---|---|
讀取未提交 | 允許,但不在 PG 中 | 可能 | 可能 | 可能 |
讀取已提交 | 不可能 | 可能 | 可能 | 可能 |
可重複讀取 | 不可能 | 不可能 | 允許,但不在 PG 中 | 可能 |
可序列化 | 不可能 | 不可能 | 不可能 | 不可能 |
在PostgreSQL中,您可以請求四種標準交易隔離級別中的任何一種,但內部僅實作了三種不同的隔離級別,即 PostgreSQL 的讀取未提交模式的行為類似於讀取已提交。這是因為它是將標準隔離級別對應到 PostgreSQL 的多版本並行控制架構的唯一合理方式。
該表還顯示 PostgreSQL 的可重複讀取實作不允許幻讀。這在 SQL 標準下是可以接受的,因為該標準指定了哪些異常不得在某些隔離級別發生;更高的保證是可以接受的。可用隔離級別的行為在以下小節中詳細介紹。
要設定交易的交易隔離級別,請使用命令SET TRANSACTION。
某些PostgreSQL資料類型和函數對於交易行為有特殊規則。特別是,對序列所做的更改(因此使用serial
宣告的列的計數器)會立即對所有其他交易可見,並且如果進行更改的交易中止,則不會回滾。請參閱第 9.17 節和第 8.1.4 節。
讀取已提交是PostgreSQL中的預設隔離級別。當交易使用此隔離級別時,SELECT
查詢(沒有FOR UPDATE/SHARE
子句)僅看到查詢開始之前提交的資料;它永遠不會看到未提交的資料或並行交易在查詢執行期間提交的更改。實際上,SELECT
查詢看到資料庫在查詢開始執行時的快照。但是,SELECT
確實看到了其自身交易中先前執行的更新的效果,即使它們尚未提交。另請注意,如果其他交易在第一個SELECT
開始後且在第二個SELECT
開始之前提交更改,則即使兩個連續的SELECT
命令在單個交易中,也可以看到不同的資料。
UPDATE
、DELETE
、SELECT FOR UPDATE
和 SELECT FOR SHARE
命令在搜尋目標列的行為與 SELECT
相同:它們只會找到在命令開始時已提交的目標列。然而,這樣的目標列可能在被找到時,已經被另一個並行交易更新(或刪除或鎖定)。在這種情況下,原本要更新的交易將等待第一個更新交易提交或回滾(如果它仍在進行中)。如果第一個更新交易回滾,那麼它的影響將被撤銷,第二個更新交易可以繼續更新最初找到的列。如果第一個更新交易提交,第二個更新交易如果第一個更新交易刪除了該列,則會忽略該列,否則它將嘗試將其操作應用於該列的更新版本。命令的搜尋條件(WHERE
子句)會被重新評估,以查看該列的更新版本是否仍然符合搜尋條件。如果是,則第二個更新交易將繼續使用該列的更新版本執行其操作。對於 SELECT FOR UPDATE
和 SELECT FOR SHARE
,這表示被鎖定並返回給客戶端的是該列的更新版本。
具有 ON CONFLICT DO UPDATE
子句的 INSERT
命令行為類似。在讀取已提交 (Read Committed) 模式下,每個提議插入的列要麼插入,要麼更新。除非存在不相關的錯誤,否則保證會發生這兩個結果之一。如果衝突源自另一個交易,而該交易的影響對於 INSERT
來說尚不可見,則 UPDATE
子句將影響該列,即使可能 沒有 該列的任何版本對於該命令來說是傳統上可見的。
由於另一個交易的結果,使得該結果的影響對於 INSERT
快照來說不可見,因此具有 ON CONFLICT DO NOTHING
子句的 INSERT
命令可能不會繼續插入列。同樣,這僅在讀取已提交 (Read Committed) 模式下才會發生。
MERGE
允許使用者指定 INSERT
、UPDATE
和 DELETE
子命令的各種組合。 具有 INSERT
和 UPDATE
子命令的 MERGE
命令看起來類似於具有 ON CONFLICT DO UPDATE
子句的 INSERT
命令,但不保證會發生 INSERT
或 UPDATE
。如果 MERGE
嘗試執行 UPDATE
或 DELETE
並且該列同時被更新,但連接條件仍然適用於當前的目標和當前的來源元組,則 MERGE
的行為將與 UPDATE
或 DELETE
命令相同,並對該列的更新版本執行其操作。但是,由於 MERGE
可以指定多個操作並且它們可以是有條件的,因此每個操作的條件都會在該列的更新版本上重新評估,從第一個操作開始,即使最初匹配的操作出現在操作列表的稍後位置。另一方面,如果該列同時被更新,導致連接條件失敗,則 MERGE
將評估命令的 NOT MATCHED BY SOURCE
和 NOT MATCHED [BY TARGET]
操作,並執行每種類型中第一個成功的操作。如果該列同時被刪除,則 MERGE
將評估命令的 NOT MATCHED [BY TARGET]
操作,並執行第一個成功的操作。如果 MERGE
嘗試執行 INSERT
並且存在唯一索引,並且同時插入了重複的列,則會引發唯一性違規錯誤; MERGE
不會嘗試通過重新啟動 MATCHED
條件的評估來避免此類錯誤。
由於上述規則,更新命令可能會看到不一致的快照:它可以看到並行更新命令對其嘗試更新的相同列的影響,但看不到這些命令對資料庫中其他列的影響。 這種行為使得讀取已提交 (Read Committed) 模式不適用於涉及複雜搜尋條件的命令; 但是,它非常適合更簡單的情況。 例如,考慮使用類似以下的交易來更新銀行餘額:
BEGIN; UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 12345; UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 7534; COMMIT;
如果兩個這樣的交易同時嘗試更改帳戶 12345 的餘額,我們顯然希望第二個交易從該帳戶列的更新版本開始。 由於每個命令僅影響預先確定的列,因此讓它看到該列的更新版本不會產生任何麻煩的不一致性。
更複雜的用法可能會在讀取已提交 (Read Committed) 模式下產生不良結果。 例如,考慮一個 DELETE
命令,該命令作用於正在被另一個命令添加和從其限制條件中刪除的資料,例如,假設 website
是一個兩列的資料表,其中 website.hits
等於 9
和 10
BEGIN; UPDATE website SET hits = hits + 1; -- run from another session: DELETE FROM website WHERE hits = 10; COMMIT;
DELETE
將不起作用,即使在 UPDATE
之前和之後都存在 website.hits = 10
的列。 發生這種情況是因為跳過了更新前的列值 9
,並且當 UPDATE
完成且 DELETE
獲得鎖定時,新的列值不再是 10
而是 11
,這不再符合條件。
由於讀取已提交 (Read Committed) 模式使用一個新的快照啟動每個命令,該快照包含到該時刻為止提交的所有交易,因此同一交易中的後續命令無論如何都會看到已提交的並行交易的影響。 上述問題的關鍵在於 單個 命令是否看到資料庫的絕對一致的視圖。
讀取已提交 (Read Committed) 模式提供的部分交易隔離對於許多應用程式來說是足夠的,並且這種模式使用起來快速且簡單; 但是,它並不適用於所有情況。 執行複雜查詢和更新的應用程式可能需要比讀取已提交 (Read Committed) 模式提供的更嚴格一致的資料庫視圖。
可重複讀取隔離級別僅查看交易開始之前提交的資料; 它永遠不會看到未提交的資料,也不會看到並行交易在交易執行期間提交的更改。 (但是,每個查詢都會看到在其自身交易中執行的先前更新的影響,即使它們尚未提交。)這比標準SQL對此隔離級別的要求更強,並且防止了表格 13.1中描述的所有現象,除了序列化異常。 如上所述,標準明確允許這樣做,它僅描述了每個隔離級別必須提供的 最低 保護。
此級別與讀取已提交 (Read Committed) 的不同之處在於,可重複讀取交易中的查詢會看到一個快照,該快照是在 交易 中第一個非交易控制語句開始時的狀態,而不是在交易中當前語句開始時的狀態。 因此,單個交易中的後續 SELECT
命令會看到相同的資料,即,它們不會看到其他交易在其自身交易開始後提交的更改。
使用此級別的應用程式必須準備好因序列化失敗而重試交易。
UPDATE
、DELETE
、MERGE
、SELECT FOR UPDATE
和 SELECT FOR SHARE
指令在搜尋目標列時的行為與 SELECT
相同:它們只會找到在交易開始時已提交的目標列。但是,這樣的目標列可能在被找到時,已經被另一個並行交易更新(或刪除或鎖定)。在這種情況下,可重複讀取 (repeatable read) 交易將等待第一個更新交易提交或回滾(如果它仍在進行中)。如果第一個更新者回滾,那麼它的影響將被取消,並且可重複讀取交易可以繼續更新最初找到的列。但是,如果第一個更新者提交(並且實際上更新或刪除了該列,而不僅僅是鎖定),那麼可重複讀取交易將被回滾,並顯示以下訊息:
ERROR: could not serialize access due to concurrent update
因為可重複讀取交易無法修改或鎖定在可重複讀取交易開始後被其他交易變更的列。
當應用程式收到此錯誤訊息時,它應該中止當前交易並從頭開始重試整個交易。第二次執行時,交易會將先前提交的變更視為其資料庫初始檢視的一部分,因此使用該列的新版本作為新交易更新的起點沒有邏輯衝突。
請注意,只有更新交易可能需要重試;唯讀交易永遠不會發生序列化衝突。
可重複讀取模式提供嚴格的保證,即每個交易都看到完全穩定的資料庫檢視。但是,此檢視不一定總是與相同層級的並行交易的某些序列(一次一個)執行一致。例如,即使是此層級的唯讀交易也可能會看到控制記錄已更新以顯示批次已完成,但未看到邏輯上屬於該批次的其中一個明細記錄,因為它讀取了控制記錄的較早版本。如果沒有仔細使用明確鎖定來阻止衝突交易,嘗試透過在此隔離層級運行的交易來強制執行業務規則不太可能正常工作。
可重複讀取隔離層級是使用學術資料庫文獻和一些其他資料庫產品中稱為 快照隔離 (Snapshot Isolation) 的技術實現的。與使用降低並行性的傳統鎖定技術的系統相比,可能會觀察到行為和效能的差異。某些其他系統甚至可能提供可重複讀取和快照隔離作為具有不同行為的不同隔離層級。區分這兩種技術的允許現象直到 SQL 標準制定之後才被資料庫研究人員形式化,並且超出本手冊的範圍。有關完整處理,請參閱 [berenson95]。
在 PostgreSQL 9.1 版本之前,對可序列化 (Serializable) 交易隔離層級的請求提供了與此處描述完全相同的行為。要保留舊版可序列化行為,現在應該請求可重複讀取。
可序列化隔離層級提供最嚴格的交易隔離。此層級模擬所有已提交交易的序列交易執行;彷彿交易已一個接一個地、序列地執行,而不是並行執行。但是,與可重複讀取層級一樣,使用此層級的應用程式必須準備好因序列化失敗而重試交易。實際上,此隔離層級的工作方式與可重複讀取完全相同,只不過它還會監控可能使並行可序列化交易集的執行方式與這些交易的所有可能的序列(一次一個)執行不一致的條件。這種監控不會引入超出可重複讀取中存在的任何阻塞,但監控會產生一些額外負擔,並且檢測到可能導致序列化異常的條件將觸發序列化失敗。
例如,考慮一個表 mytab
,最初包含
class | value -------+------- 1 | 10 1 | 20 2 | 100 2 | 200
假設可序列化交易 A 計算
SELECT SUM(value) FROM mytab WHERE class = 1;
然後將結果 (30) 作為 value
插入到一個新的列中,其中 class
= 2
。同時,可序列化交易 B 計算
SELECT SUM(value) FROM mytab WHERE class = 2;
並獲得結果 300,它將其插入到一個新的列中,其中 class
= 1
。然後兩個交易都嘗試提交。如果任何一個交易以可重複讀取隔離層級運行,則兩個交易都將被允許提交;但由於沒有與結果一致的執行序列,使用可序列化交易將允許一個交易提交,並將另一個交易回滾,並顯示以下訊息:
ERROR: could not serialize access due to read/write dependencies among transactions
這是因為如果 A 在 B 之前執行,則 B 將計算總和 330,而不是 300,同樣,另一個順序將導致 A 計算出不同的總和。
當依靠可序列化交易來防止異常時,重要的是,在讀取它的交易成功提交之前,從永久使用者表讀取的任何資料都不能被視為有效。即使是唯讀交易也是如此,除非在 可延遲 (deferrable) 唯讀交易中讀取的資料在讀取後立即被認為是有效的,因為這樣的交易會等到它可以獲得保證沒有此類問題的快照,然後才開始讀取任何資料。在所有其他情況下,應用程式不得依賴在稍後中止的交易期間讀取的結果;相反,它們應該重試交易直到它成功。
為了保證真正的可序列化,PostgreSQL 使用 謂詞鎖定 (predicate locking),這意味著它保留鎖定,使其能夠確定寫入何時會影響先前從並行交易讀取的結果(如果它首先運行)。在 PostgreSQL 中,這些鎖定不會導致任何阻塞,因此不能在引起死鎖方面發揮任何作用。它們用於識別和標記並行可序列化交易之間的依賴關係,這些依賴關係在某些組合中可能導致序列化異常。相反,想要確保資料一致性的讀取已提交或可重複讀取交易可能需要鎖定整個表,這可能會阻止其他使用者嘗試使用該表,或者它可以使用 SELECT FOR UPDATE
或 SELECT FOR SHARE
,這不僅會阻止其他交易,還會導致磁碟存取。
在 PostgreSQL 中,如大多數其他資料庫系統一樣,述詞鎖定 (Predicate locks) 是基於事務實際存取的資料。這些鎖定會在 pg_locks
系統視窗中顯示,其 mode
為 SIReadLock
。查詢執行期間取得的特定鎖定取決於查詢使用的計畫,並且多個更細粒度的鎖定(例如,元組鎖定)可能會在事務過程中組合成較少的粗粒度鎖定(例如,頁面鎖定),以防止耗盡用於追蹤鎖定的記憶體。READ ONLY
事務如果偵測到不會再發生可能導致序列化異常的衝突,則可能在完成之前釋放其 SIRead 鎖定。事實上,READ ONLY
事務通常能夠在啟動時確定這個事實,並避免取得任何述詞鎖定。如果您明確請求 SERIALIZABLE READ ONLY DEFERRABLE
事務,它將會封鎖直到確定這個事實。(這是序列化事務會封鎖,但可重複讀取事務不會封鎖的唯一情況。)另一方面,SIRead 鎖定通常需要保留到事務提交之後,直到重疊的讀寫事務完成為止。
一致地使用序列化事務可以簡化開發。任何成功提交的並行序列化事務集合,都會與它們一次執行一個的效果相同,這一保證意味著如果您可以證明單個事務,按其編寫的方式,在單獨執行時會做正確的事情,您可以確信它在任何序列化事務的組合中都會做正確的事情,即使沒有關於其他事務可能做什麼的任何資訊,或者它將無法成功提交。重要的是,使用這種技術的環境必須有一種處理序列化失敗的通用方法(它總是返回 SQLSTATE 值 '40001'),因為很難準確預測哪些事務可能導致讀/寫依賴關係,並且需要回滾以防止序列化異常。監視讀/寫依賴關係會產生開銷,重新啟動因序列化失敗而終止的事務也是如此,但與使用顯式鎖定和 SELECT FOR UPDATE
或 SELECT FOR SHARE
所涉及的成本和封鎖相比,對於某些環境來說,序列化事務是最佳的效能選擇。
雖然 PostgreSQL 的序列化事務隔離級別只允許並行事務在可以證明存在一個執行順序,會產生相同效果時提交,但它並不總是阻止引發在真正序列執行中不會發生的錯誤。特別是,即使在嘗試插入之前明確檢查了鍵不存在,也可能會看到由於與重疊的序列化事務衝突而導致的唯一性約束違規。可以通過確保所有插入可能衝突的鍵的序列化事務,都先明確檢查它們是否可以這樣做來避免這種情況。例如,想像一個應用程式,要求使用者輸入一個新的鍵,然後先嘗試選擇它來檢查它是否已經存在,或者通過選擇最大的現有鍵並加一來產生一個新的鍵。如果某些序列化事務直接插入新的鍵,而不遵循這個協議,即使在並行事務的序列執行中不會發生唯一性約束違規的情況下,也可能會報告它們。
為了在依靠序列化事務進行並行控制時獲得最佳效能,應考慮以下問題
盡可能將事務宣告為 READ ONLY
。
控制活動連線的數量,必要時使用連線池。這始終是一個重要的效能考量,但在使用序列化事務的繁忙系統中,它可能尤其重要。
不要在單個事務中放入超過完整性目的所需的內容。
不要讓連線懸空「idle in transaction」超過必要的時間。可以使用配置參數 idle_in_transaction_session_timeout 來自動斷開滯留的會話。
由於序列化事務自動提供的保護,因此不再需要顯式鎖定、SELECT FOR UPDATE
和 SELECT FOR SHARE
。
當系統被迫將多個頁面級別的述詞鎖定組合到單個關係級別的述詞鎖定中,因為述詞鎖定表缺少記憶體時,可能會發生序列化失敗率的增加。您可以通過增加 max_pred_locks_per_transaction、max_pred_locks_per_relation 和/或 max_pred_locks_per_page 來避免這種情況。
循序掃描總是需要關係級別的述詞鎖定。這可能導致序列化失敗率的增加。通過降低 random_page_cost 和/或增加 cpu_tuple_cost 來鼓勵使用索引掃描可能會有所幫助。請務必權衡事務回滾和重新啟動的任何減少,與查詢執行時間的任何總體變化。
序列化隔離級別是使用學術資料庫文獻中稱為序列化快照隔離的技術實現的,它通過新增序列化異常檢查來建立在快照隔離之上。與使用傳統鎖定技術的其他系統相比,可能會觀察到一些行為和效能上的差異。請參閱 [ports12] 以獲取詳細資訊。
如果您在文件中發現任何不正確、與您對特定功能的體驗不符或需要進一步澄清的地方,請使用此表格報告文件問題。