該SQLSQL 標準定義了四種事務隔離級別。最嚴格的是序列化 (Serializable),它由標準中一段文字定義,該段文字說明:任何一組序列化事務的併發執行,其結果都保證與按某種順序逐個執行這些事務的結果相同。其他三個級別則透過併發事務互動產生的現象來定義,這些現象在每個級別上都必須避免。標準指出,由於序列化的定義,在序列化級別上不可能出現這些現象。(這並不奇怪——如果事務的結果必須與逐個執行事務的結果一致,怎麼可能看到由互動引起的任何現象?)
在不同級別上被禁止的現象是:
SQL 標準和 PostgreSQL 實現的事務隔離級別在表 13.1 中進行了描述。
表 13.1. 事務隔離級別
隔離級別 | 髒讀 | 不可重複讀 | 幻讀 | 序列化異常 |
---|---|---|---|---|
未提交讀 | 允許,但在 PG 中不實現 | 可能 | 可能 | 可能 |
讀已提交 | 不可能 | 可能 | 可能 | 可能 |
可重複讀 | 不可能 | 不可能 | 允許,但在 PG 中不實現 | 可能 |
序列化 | 不可能 | 不可能 | 不可能 | 不可能 |
在 PostgreSQL 中,您可以請求任何四種標準的事務隔離級別,但內部只實現了三種不同的隔離級別,即 PostgreSQL 的未提交讀模式的行為類似於讀已提交。這是因為這是將標準隔離級別對映到 PostgreSQL 的多版本併發控制 (MVCC) 體系結構的一種合理方式。
該表還顯示,PostgreSQL 的可重複讀實現不允許幻讀。這在 SQL 標準下是可以接受的,因為標準規定了在特定隔離級別下 不得 發生的現象;更高的保證是可以接受的。可用隔離級別的行為在以下子節中詳細介紹。
要設定事務的隔離級別,請使用 SET TRANSACTION 命令。
一些 PostgreSQL 的資料型別和函式在事務行為方面有特殊的規則。特別是,對序列(以及宣告為 serial
的列的計數器)所做的更改會立即對所有其他事務可見,並且如果進行更改的事務被回滾,則這些更改不會被回滾。請參閱第 9.17 節和第 8.1.4 節。
讀已提交 (Read Committed) 是 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
的行為類似。在讀已提交模式下,每個擬插入的行要麼插入,要麼更新。除非有無關的錯誤,否則這兩種結果中的一種是可以保證的。如果衝突源自另一個事務,而該事務的效果尚未對 INSERT
可見,那麼 UPDATE
子句將影響該行,即使通常情況下該行沒有任何版本對命令可見。
在讀已提交模式下,帶有 ON CONFLICT DO NOTHING
子句的 INSERT
可能會因為另一個事務的效果尚未對 INSERT
快照可見而導致插入操作未進行。
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
條件來避免此類錯誤。
由於上述規則,更新命令可能會看到不一致的快照:它可以看到併發更新命令對同一行(它試圖更新的行)的影響,但看不到這些命令對資料庫中其他行的影響。這種行為使得讀已提交模式不適合涉及複雜搜尋條件的命令;然而,它對於更簡單的場景來說是合適的。例如,考慮將 100 美元從一個賬戶轉到另一個賬戶
BEGIN; UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 12345; UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 7534; COMMIT;
如果另一個事務併發地嘗試更改賬戶 7534 的餘額,我們顯然希望第二個語句從賬戶行的更新版本開始。因為每個命令隻影響預定的行,允許它看到行的更新版本不會造成任何令人煩惱的不一致。
更復雜的用法可能會在讀已提交模式下產生不良結果。例如,考慮一個 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
,這不再匹配條件。
由於讀已提交模式為每個命令啟動一個新的快照,該快照包含截至該時刻所有已提交的事務,因此同一事務中的後續命令無論如何都會看到已提交的併發事務的影響。這裡的問題在於一個單個命令是否看到資料庫的完全一致的檢視。
讀已提交模式提供的部分事務隔離對於許多應用程式來說是足夠的,並且該模式快速且易於使用;然而,它並非對所有情況都足夠。執行復雜查詢和更新的應用程式可能需要比讀已提交模式提供的更嚴格的一致性資料庫檢視。
可重複讀 (Repeatable Read) 隔離級別只看到事務開始前已提交的資料;它永遠看不到併發事務在事務執行期間未提交的資料或已提交的更改。(然而,每個查詢都可以看到其自身事務中先前執行的更新的效果,即使這些更新尚未提交。)這比標準的SQL對此隔離級別的要求更強,並且除了序列化異常外,可以防止 表 13.1 中描述的所有現象。如上所述,這在標準中是允許的,標準只描述了每個隔離級別必須提供的最低保護。
此級別與讀已提交不同之處在於,可重複讀事務中的查詢看到的是事務中第一個非事務控制語句開始時的快照,而不是事務中當前語句開始時的快照。因此,在單個事務中,連續的 SELECT
命令看到相同的資料,即它們看不到在自己的事務開始後、由其他事務提交的更改。
使用此級別的應用程式必須準備好因序列化失敗而重試事務。
UPDATE
、DELETE
、MERGE
、SELECT FOR UPDATE
和 SELECT FOR SHARE
命令在搜尋目標行方面與 SELECT
的行為相同:它們只會找到在事務開始時已提交的目標行。但是,當找到這樣的目標行時,它可能已被另一個併發事務更新(或刪除或鎖定)。在這種情況下,可重複讀事務將等待第一個更新事務提交或回滾(如果它仍在進行中)。如果第一個更新程式回滾,則其效果將被撤銷,可重複讀事務可以繼續更新最初找到的行。但是,如果第一個更新程式提交(並且實際上更新或刪除了該行,而不僅僅是鎖定它),那麼可重複讀事務將被回滾,並顯示以下訊息:
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)插入到 class
為 = 2
的新行中的 value
列。同時,序列化事務 B 計算
SELECT SUM(value) FROM mytab WHERE class = 2;
並得到結果 300,然後將其插入到 class
為 = 1
的新行中的 value
列。然後兩個事務都嘗試提交。如果在可重複讀隔離級別下執行其中任何一個事務,那麼兩個事務都會被允許提交;但是,由於不存在一致的結果的序列執行順序,使用序列化事務將允許一個事務提交,並將另一個事務回滾,並顯示以下訊息:
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 中的謂詞鎖是基於事務實際訪問的資料。這些鎖將顯示在 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_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 來鼓勵索引掃描可能會有所幫助。請務必權衡事務回滾和重試的任何減少與查詢執行時間的總體變化。
序列化隔離級別是使用一種在學術資料庫文獻中稱為序列化快照隔離 (Serializable Snapshot Isolation) 的技術實現的,該技術透過新增對序列化異常的檢查來構建快照隔離。與使用傳統鎖定技術的其他系統相比,可能會觀察到行為和效能上的差異。有關詳細資訊,請參閱[ports12]。
如果您在文件中發現任何不正確之處、與您在使用特定功能時的體驗不符之處,或者需要進一步澄清之處,請使用此表單報告文件問題。