237
技術社區[雲棲]
PostgreSQL SQL 語言:並發控製
本文檔為PostgreSQL 9.6.0文檔,本轉載已得到原譯者彭煜瑋授權。
PostgreSQL為開發者提供了一組豐富的工具來管理對數據的並發訪問。在內部,數據一致性通過使用一種多版本模型(多版本並發控製,MVCC)來維護。這就意味著每個 SQL 語句看到的都隻是一小段時間之前的數據快照(一個數據庫版本),而不管底層數據的當前狀態。這樣可以保護語句不會看到可能由其他在相同數據行上執行更新的並發事務造成的不一致數據,為每一個數據庫會話提供事務隔離。MVCC避免了傳統的數據庫係統的鎖定方法,將鎖爭奪最小化來允許多用戶環境中的合理性能。
使用MVCC並發控製模型而不是鎖定的主要優點是在MVCC中,對查詢(讀)數據的鎖請求與寫數據的鎖請求不衝突,所以讀不會阻塞寫,而寫也從不阻塞讀。甚至在通過使用革新的可序列化快照隔離(SSI)級別提供最嚴格的事務隔離級別時,PostgreSQL也維持這個保證。
在PostgreSQL裏也有表和行級別的鎖功能,用於那些通常不需要完整事務隔離並且想要顯式管理特定衝突點的應用。不過,恰當地使用MVCC通常會提供比鎖更好的性能。另外,由應用定義的谘詢鎖提供了一個獲得不依賴於單一事務的鎖的機製。
SQL標準定義了四種隔離級別。最嚴格的是可序列化,在標準中用了一整段來定義它,其中說到一組可序列化事務的任意並發執行被保證效果和以某種順序一個一個執行這些事務一樣。其他三種級別使用並發事務之間交互產生的現象來定義,每一個級別中都要求必須不出現一種現象。注意由於可序列化的定義,在該級別上這些現象都不可能發生(這並不令人驚訝--如果事務的效果與每個時刻隻運行一個的相同,你怎麼可能看見由於交互產生的現象?)。
在各個級別上被禁止出現的現象是:
髒讀
一個事務讀取了另一個並行未提交事務寫入的數據。
不可重複讀
一個事務重新讀取之前讀取過的數據,發現該數據已經被另一個事務(在初始讀之後提交)修改。
幻讀
一個事務重新執行一個返回符合一個搜索條件的行集合的查詢, 發現滿足條件的行集合因為另一個最近提交的事務而發生了改變。
序列化異常
成功提交一組事務的結果與這些事務所有可能的串行執行結果都不一致。
SQL 標準和 PostgreSQL 實現的事務隔離級別在 Table 13-1中描述。
Table 13-1. 事務隔離級別
在PostgreSQL中,你可以請求四種標準事務隔離級別中的任意一種,但是內部隻實現了三種不同的隔離級別,即 PostgreSQL 的讀未提交模式的行為和讀已提交相同。這是因為把標準隔離級別映射到 PostgreSQL 的多版本並發控製架構的唯一合理的方法。
該表格也顯示 PostgreSQL 的可重複讀實現不允許幻讀。而 SQL 標準允許更嚴格的行為:四種隔離級別隻定義了哪種現像不能發生,但是沒有定義哪種現像必須發生。可用的隔離級別的行為在下麵的小節中詳細描述。
要設置一個事務的事務隔離級別,使用SET TRANSACTION命令。
Important:
某些PostgreSQL數據類型和函數關於事務的行為有特殊的規則。特別是,對一個序列的修改(以及用serial聲明的一列的計數器)是立刻對所有其他事務可見的,並且在作出該修改的事務中斷時也不會被回滾。
2.1. 讀已提交隔離級別
讀已提交是PostgreSQL中的默認隔離級別。 當一個事務運行使用這個隔離級別時, 一個查詢(沒有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快照不可見的事務的結果無法讓插入進行 下去。再一次,這隻是讀已提交模式中的情況。
因為上麵的規則,正在更新的命令可能會看到一個不一致的快照: 它們可以看到並發更新命令在它嚐試更新的相同行上的作用,但是卻看不到那些命令對數據庫裏其它行的作用。 這樣的行為令讀已提交模式不適合用於涉及複雜搜索條件的命令。不過,它對於更簡單的情況是正確的。 例如,考慮用這樣的命令更新銀行餘額:
BEGIN;
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 12345;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 7534;
COMMIT;
如果兩個這樣的事務同時嚐試修改帳號 12345 的餘額,那我們很明顯希望第二個事務從賬戶行的已更新版本上開始工作。 因為每個命令隻影響一個已經決定了的行,讓它看到行的已更新版本不會導致任何麻煩的不一致性。
在讀已提交模式中,更複雜的使用可能產生不符合需要的結果。例如: 考慮一個在數據上操作的DELETE命令,它操作的數據正被另一個命令從它的限製條件中移除或者加入,例如,假定website是一個兩行的表,兩行的website.hits等於9和10:
BEGIN;
UPDATE website SET hits = hits + 1;
-- run from another session: DELETE FROM website WHERE hits = 10;
COMMIT;
即便在UPDATE之前有一個website.hits = 10的行,DELETE將不會產生效果。這是因為更新之前的行值9被跳過,並且當UPDATE完成並且DELETE獲得一個鎖,新行值不再是10而是11,這再也不匹配條件了。
因為在讀已提交模式中,每個命令都是從一個新的快照開始的,而這個快照包含在該時刻已提交的事務, 因此同一事務中的後續命令將看到任何已提交的並行事務的效果。以上的焦點在於單個命令是否看到數據庫的絕對一致的視圖。
讀已提交模式提供的部分事務隔離對於許多應用而言是足夠的,並且這個模式速度快並且使用簡單。 不過,它不是對於所有情況都夠用。做複雜查詢和更新的應用可能需要比讀已提交模式提供的更嚴格一致的數據庫視圖。
2.2. 可重複讀隔離級別
可重複讀隔離級別隻看到在事務開始之前被提交的數據;它從來看不到未提交的數據或者並行事務在本事務執行期間提交的修改(不過,查詢能夠看見在它的事務中之前執行的更新,即使它們還沒有被提交)。這是比SQL標準對此隔離級別所要求的更強的保證,並且阻止Table 13-1中描述的除了序列化異常之外的所有現象。如上麵所提到的,這是標準特別允許的,標準隻描述了每種隔離級別必須提供的最小保護。
這個級別與讀已提交不同之處在於,一個可重複讀事務中的查詢可以看見在事務中第一個非事務控製語句開始時的一個快照,而不是事務中當前語句開始時的快照。因此,在一個單一事務中的後續SELECT命令看到的是相同的數據,即它們看不到其他事務在本事務啟動後提交的修改。
使用這個級別的應用必須準備好由於序列化失敗而重試事務。
UPDATE、DELETE、SELECT FOR UPDATE和SELECT FOR SHARE命令在搜索目標行時的行為和SELECT一樣: 它們將隻找到在事務開始時已經被提交的行。 不過,在被找到時,這樣的目標行可能已經被其它並發事務更新(或刪除或鎖住)。在這種情況下, 可重複讀事務將等待第一個更新事務提交或者回滾(如果它還在進行中)。 如果第一個更新事務回滾,那麼它的作用將被忽略並且可重複讀事務可以繼續更新最初發現的行。 但是如果第一個更新事務提交(並且實際更新或刪除該行,而不是隻鎖住它),則可重複讀事務將回滾並帶有如下消息
ERROR: could not serialize access due to concurrent update
因為一個可重複讀事務無法修改或者鎖住被其他在可重複讀事務開始之後的事務改變的行。
當一個應用接收到這個錯誤消息,它應該中斷當前事務並且從開頭重試整個事務。在第二次執行中,該事務將見到作為其初始數據庫視圖一部分的之前提交的改變,這樣在使用行的新版本作為新事務更新的起點時就不會有邏輯衝突。
注意隻有更新事務可能需要被重試;隻讀事務將永遠不會有序列化衝突。
可重複讀模式提供了一種嚴格的保證,在其中每一個事務看到數據庫的一個完全穩定的視圖。不過,這個視圖並不需要總是和同一級別上並發事務的某些序列化(一次一個)執行保持一致。例如,即使這個級別上的一個隻讀事務可能看到一個控製記錄被更新,這顯示一個批處理已經被完成但是不能看見作為該批處理的邏輯組成部分的一個細節記錄,因為它讀取空值記錄的一個較早的版本。如果不小心地使用顯式鎖來阻塞衝突事務,嚐試用運行在這個隔離級別的事務來強製業務規則不太可能正確地工作。
Note:
在PostgreSQL版本 9.1 之前,一個對於可序列化事務隔離級別的請求會提供和這裏描述的完全一樣的行為。為了保持可序列化行為,現在應該請求可重複讀。
2.3. 可序列化隔離級別
可序列化隔離級別提供了最嚴格的事務隔離。這個級別為所有已提交事務模擬序列事務執行;就好像事務被按照序列一個接著另一個被執行,而不是並行地被執行。但是,和可重複讀級別相似,使用這個級別的應用必須準備好因為序列化失敗而重試事務。事實上,這個給力級別完全像可重複讀一樣地工作,除了它會監視一些條件,這些條件可能導致一個可序列化事務的並發集合的執行產生的行為與這些事務所有可能的序列化(一次一個)執行不一致。這種監控不會引入超出可重複讀之外的阻塞,但是監控會產生一些負荷,並且對那些可能導致序列化異常的條件的檢測將觸發一次序列化失敗。
例如,考慮一個表mytab,它初始時包含:
class | value
-------+-------
1 | 10
1 | 20
2 | 100
2 | 200
假設可序列化事務 A 計算:
SELECT SUM(value) FROM mytab WHERE class = 1;
並且接著把結果(3)作為一個新行的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 計算出一個不同的合計值。
當依賴可序列化事務來阻止異常時,重要的一點是任何從一個持久化用戶表讀出數據都不被認為是有效的,直到讀它的事務已經成功提交為止。即便是對隻讀事務也是如此,除了在一個可推遲的隻讀事務中讀取的數據是讀出以後立刻有效的,因為這樣的一個事務在開始讀取任何數據之前會等待,直到它能獲得一個快照保證來避免這種問題為止。在所有其他情況下,應用不能依靠在一個後來被中斷的事務中讀取的結果;相反,它們應當重試事務直到它成功。
要保證真正的可序列化,PostgreSQL使用了謂詞鎖,這意味著它會保持鎖,這些鎖讓它能夠判斷在它先運行的情況下,什麼時候一個寫操作會對一個並發事務中之前讀取的結果產生影響。在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來避免這種情況。
- 一次順序掃描將總是需要一個關係級謂詞鎖。這可能導致序列化失敗的比例上升。通過縮減random_page_cost和/或增加cpu_tuple_cost來鼓勵使用索引掃描將有助於此。一定要在事務回滾和重啟數目的任何減少與查詢執行時間的任何全麵改變之間進行權衡。
PostgreSQL提供了多種鎖模式用於控製對表中數據的並發訪問。 這些模式可以用於在MVCC無法給出期望行為的情境中由應用控製的鎖。 同樣,大多數PostgreSQL命令會自動要求恰當的鎖以保證被引用的表在命令的執行過程中 不會以一種不兼容的方式刪除或修改(例如,TRUNCATE無法安全地與同一表中上的其他操作並發地執行,因此它在表上獲得一個排他鎖來強製這種行為)。
要檢查在一個數據庫服務器中當前未解除的鎖列表,可以使用pg_locks係統視圖。
3.1. 表級鎖
下麵的列表顯示了可用的鎖模式和PostgreSQL自動使用它們的場合。 你也可以用LOCK命令顯式獲得這些鎖。請記住所有這些鎖模式都是表級鎖,即使它們的名字包含"row"單詞(這些名稱是曆史遺產)。 在一定程度上,這些名字反應了每種鎖模式的典型用法 — 但是語意卻都是一樣的。 兩種鎖模式之間真正的區別是它們有著不同的衝突鎖模式集合(參考Table 13-2)。 兩個事務在同一時刻不能在同一個表上持有屬於相互衝突模式的鎖(但是,一個事務決不會和自身衝突。例如,它可以在同一個表上獲得ACCESS EXCLUSIVE鎖然後接著獲取ACCESS SHARE鎖)。非衝突鎖模式可以由許多事務同時持有。 請特別注意有些鎖模式是自衝突的(例如,在一個時刻ACCESS EXCLUSIVE鎖不能被多於一個事務持有)而其他鎖模式不是自衝突的(例如,ACCESS SHARE鎖可以被多個事務持有)。
表級鎖模式
ACCESS SHARE
隻與ACCESS EXCLUSIVE鎖模式衝突。
SELECT命令在被引用的表上獲得一個這種模式的鎖。通常,任何隻讀取表而不修改它的查詢都將獲得這種鎖模式。
ROW SHARE
與EXCLUSIVE和ACCESS EXCLUSIVE鎖模式衝突。
SELECT FOR UPDATE和SELECT FOR SHARE命令在目標表上取得一個這種模式的鎖 (加上在被引用但沒有選擇FOR UPDATE/FOR SHARE的任何其他表上的ACCESS SHARE鎖)。
ROW EXCLUSIVE
與SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE鎖模式衝突。
命令UPDATE、DELETE和INSERT在目標表上取得這種鎖模式(加上在任何其他被引用表上的ACCESS SHARE鎖)。通常,這種鎖模式將被任何修改表中數據的命令取得。
SHARE UPDATE EXCLUSIVE
與SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE鎖模式衝突。這種模式保護一個表不受並發模式改變和VACUUM運行的影響。
由VACUUM(不帶FULL)、ANALYZE、CREATE INDEX CONCURRENTLY和ALTER TABLE VALIDATE以及其他ALTER TABLE的變體獲得。
SHARE
與ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE鎖模式衝突。這種模式保護一個表不受並發數據改變的影響。
由CREATE INDEX(不帶CONCURRENTLY)取得。
SHARE ROW EXCLUSIVE
與ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE鎖模式衝突。這種模式保護一個表不受並發數據修改所影響,並且是自排他的,這樣在一個時刻隻能有一個會話持有它。
由CREATE TRIGGER和很多 ALTER TABLE的很多形式所獲得(見 ALTER TABLE)。
EXCLUSIVE
與ROW SHARE、ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE鎖模式衝突。這種模式隻允許並發的ACCESS SHARE鎖,即隻有來自於表的讀操作可以與一個持有該鎖模式的事務並行處理。
由REFRESH MATERIALIZED VIEW CONCURRENTLY獲得。
ACCESS EXCLUSIVE
與所有模式的鎖衝突(ACCESS SHARE、ROW SHARE、ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE和ACCESS EXCLUSIVE)。這種模式保證持有者是訪問該表的唯一事務。
由ALTER TABLE、DROP TABLE、TRUNCATE、REINDEX、CLUSTER、VACUUM FULL和REFRESH MATERIALIZED VIEW(不帶CONCURRENTLY)命令獲取。ALTER TABLE的很多形式也在這個層麵上獲得鎖(見ALTER TABLE)。這也是未顯式指定模式的LOCK TABLE命令的默認鎖模式。
Tip: 隻有一個ACCESS EXCLUSIVE鎖阻塞一個SELECT(不帶FOR UPDATE/SHARE)語句。
一旦被獲取,一個鎖通常將被持有直到事務結束。 但是如果在建立保存點之後才獲得鎖,那麼在回滾到這個保存點的時候將立即釋放該鎖。 這與ROLLBACK取消保存點之後所有的影響的原則保持一致。 同樣的原則也適用於在PL/pgSQL異常塊中獲得的鎖:一個跳出塊的錯誤將釋放在塊中獲得的鎖。
Table 13-2. 衝突的鎖模式
3.2. 行級鎖
除了表級鎖以外,還有行級鎖,在下文列出了行級鎖以及在哪些情境下PostgreSQL會自動使用它們。行級鎖的完整衝突表請見Table 13-3。注意一個事務可能會在相同的行上保持衝突的鎖,甚至是在不同的子事務中。但是除此之外,兩個事務永遠不可能在相同的行上持有衝突的鎖。行級鎖不影響數據查詢,它們隻阻塞對同一行的寫入者和加鎖者。
行級鎖模式
FOR UPDATE
FOR UPDATE會導致由SELECT語句檢索到的行被鎖定,就好像它們要被更新。這可以阻止它們被其他事務鎖定、修改或者刪除,一直到當前事務結束。也就是說其他嚐試UPDATE、DELETE、SELECT FOR UPDATE、SELECT FOR NO KEY UPDATE、SELECT FOR SHARE或者SELECT FOR KEY SHARE這些行的事務將被阻塞,直到當前事務結束。反過來,SELECT FOR UPDATE將等待已經在相同行上運行以上這些命令的並發事務,並且接著鎖定並且返回被更新的行(或者沒有行,因為行可能已被刪除)。不過,在一個REPEATABLE READ或SERIALIZABLE事務中,如果一個要被鎖定的行在事務開始後被更改,將會拋出一個錯誤。
任何在一行上的DELETE命令也會獲得FOR UPDATE鎖模式,在某些列上修改值的UPDATE也會獲得該鎖模式。當前UPDATE情況中被考慮的列集合是那些具有能用於外鍵的唯一索引的列(所以部分索引和表達式索引不被考慮),但是這種要求未來有可能會改變。
FOR NO KEY UPDATE
行為與FOR UPDATE類似,不過獲得的鎖較弱:這種鎖將不會阻塞嚐試在相同行上獲得鎖的SELECT FOR KEY SHARE命令。任何不獲取FOR UPDATE鎖的UPDATE也會獲得這種鎖模式。
FOR SHARE
行為與FOR NO KEY UPDATE類似,不過它在每個檢索到的行上獲得一個共享鎖而不是排他鎖。一個共享鎖會阻塞其他事務在這些行上執行UPDATE、DELETE、SELECT FOR UPDATE或者SELECT FOR NO KEY UPDATE,但是它不會阻止它們執行SELECT FOR SHARE或者SELECT FOR KEY SHARE。
FOR KEY SHARE
行為與FOR SHARE類似,不過鎖較弱:SELECT FOR UPDATE會被阻塞,但是SELECT FOR NO KEY UPDATE不會被阻塞。一個鍵共享鎖會阻塞其他事務執行修改鍵值的DELETE或者UPDATE,但不會阻塞其他UPDATE,也不會阻止SELECT FOR NO KEY UPDATE、SELECT FOR SHARE或者SELECT FOR KEY SHARE。
PostgreSQL不會在內存裏保存任何關於已修改行的信息,因此對一次鎖定的行數沒有限製。 不過,鎖住一行會導致一次磁盤寫,例如, SELECT FOR UPDATE將修改選中的行以標記它們被鎖住,並且因此會導致磁盤寫入。
Table 13-3. 衝突的行級鎖
3.3. 頁級鎖
除了表級別和行級別的鎖以外,頁麵級別的共享/排他鎖被用來控製對共享緩衝池中表頁麵的讀/寫。 這些鎖在行被抓取或者更新後馬上被釋放。應用開發者通常不需要關心頁級鎖,我們在這裏提到它們隻是為了完整。
3.4. 死鎖
顯式鎖定的使用可能會增加死鎖的可能性,死鎖是指兩個(或多個)事務相互持有對方想要的鎖。例如,如果事務 1 在表 A 上獲得一個排他鎖,同時試圖獲取一個在表 B 上的排他鎖, 而事務 2 已經持有表 B 的排他鎖,同時卻正在請求表 A 上的一個排他鎖,那麼兩個事務就都不能進行下去。PostgreSQL能夠自動檢測到死鎖情況並且會通過中斷其中一個事務從而允許其它事務完成來解決這個問題(具體哪個事務會被中斷是很難預測的,而且也不應該依靠這樣的預測)。
要注意死鎖也可能會作為行級鎖的結果而發生(並且因此,它們即使在沒有使用顯式鎖定的情況下也會發生)。考慮如下情況,兩個並發事務在修改一個表。第一個事務執行:
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 11111;
這樣就在指定帳號的行上獲得了一個行級鎖。然後,第二個事務執行:
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 22222;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 11111;
第一個UPDATE語句成功地在指定行上獲得了一個行級鎖,因此它成功更新了該行。 但是第二個UPDATE語句發現它試圖更新的行已經被鎖住了,因此它等待持有該鎖的事務結束。事務二現在就在等待事務一結束,然後再繼續執行。現在,事務一執行:
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 22222;
事務一試圖在指定行上獲得一個行級鎖,但是它得不到:事務二已經持有了這樣的鎖。所以它要等待事務二完成。因此,事務一被事務二阻塞,而事務二也被事務一阻塞:一個死鎖。 PostgreSQL將檢測這樣的情況並中斷其中一個事務。
防止死鎖的最好方法通常是保證所有使用一個數據庫的應用都以一致的順序在多個對象上獲得鎖。在上麵的例子裏,如果兩個事務以同樣的順序更新那些行,那麼就不會發生死鎖。 我們也應該保證一個事務中在一個對象上獲得的第一個鎖是該對象需要的最嚴格的鎖模式。如果我們無法提前驗證這些,那麼可以通過重試因死鎖而中斷的事務來即時處理死鎖。
隻要沒有檢測到死鎖情況,尋求一個表級或行級鎖的事務將無限等待衝突鎖被釋放。這意味著一個應用長時間保持事務開啟不是什麼好事(例如等待用戶輸入)。
3.5. 谘詢鎖
PostgreSQL提供了一種方法創建由應用定義其含義的鎖。這種鎖被稱為谘詢鎖,因為係統並不強迫其使用 — 而是由應用來保證其正確的使用。谘詢鎖可用於 MVCC 模型不適用的鎖定策略。例如,谘詢鎖的一種常用用法是模擬所謂"平麵文件"數據管理係統典型的悲觀鎖策略。雖然一個存儲在表中的標誌可以被用於相同目的,但谘詢鎖更快、可以避免表膨脹並且會由服務器在會話結束時自動清理。
有兩種方法在PostgreSQL中獲取一個谘詢鎖:在會話級別或在事務級別。一旦在會話級別獲得了谘詢鎖,它將被保持直到被顯式釋放或會話結束。不同於標準鎖請求,會話級谘詢鎖請求不尊重事務語義:在一個後來被回滾的事務中得到的鎖在回滾後仍然被保持,並且同樣即使調用它的事務後來失敗一個解鎖也是有效的。一個鎖在它所屬的進程中可以被獲取多次;對於每一個完成的鎖請求必須有一個相應的解鎖請求,直至鎖被真正釋放。在另一方麵,事務級鎖請求的行為更像普通鎖請求:在事務結束時會自動釋放它們,並且沒有顯式的解鎖操作。這種行為通常比會話級別的行為更方便,因為它使用一個谘詢鎖的時間更短。對於同一谘詢鎖標識符的會話級別和事務級別的鎖請求按照期望將彼此阻塞。如果一個會話已經持有了一個給定的谘詢鎖,由它發出的附加請求將總是成功,即使有其他會話在等待該鎖;不管現有的鎖和新請求是處在會話級別還是事務級別,這種說法都是真的。
和所有PostgreSQL中的鎖一樣,當前被任何會話所持有的谘詢鎖的完整列表可以在pg_locks係統視圖中找到。
谘詢鎖和普通鎖都被存儲在一個共享內存池中,它的尺寸由max_locks_per_transaction和max_connections配置變量定義。 必須當心不要耗盡這些內存,否則服務器將不能再授予任何鎖。這對服務器可以授予的谘詢鎖數量設置了一個上限,根據服務器的配置不同,這個限製通常是數萬到數十萬。
在使用谘詢鎖方法的特定情況下,特別是查詢中涉及顯式排序和LIMIT子句時,由於 SQL 表達式被計算的順序,必須小心控製鎖的獲取。例如:
SELECT pg_advisory_lock(id) FROM foo WHERE id = 12345; -- ok
SELECT pg_advisory_lock(id) FROM foo WHERE id > 12345 LIMIT 100; -- danger!
SELECT pg_advisory_lock(q.id) FROM
(
SELECT id FROM foo WHERE id > 12345 LIMIT 100
) q; -- ok
在上述查詢中,第二種形式是危險的,因為不能保證在鎖定函數被執行之前應用LIMIT。這可能導致獲得某些應用不期望的鎖,並因此在會話結束之前無法釋放。 從應用的角度來看,這樣的鎖將被掛起,雖然它們仍然在pg_locks中可見。
對於使用讀已提交事務的數據完整性強製業務規則非常困難,因為對每一個語句數據視圖都在變化,並且如果一個寫衝突發生即使一個單一語句也不能把它自己限製到該語句的快照。
雖然一個可重複讀事務在其執行期間有一個穩定的數據視圖,在使用MVCC快照進行數據一致性檢查時也有一個小問題,它涉及到被稱為讀/寫衝突的東西。如果一個事務寫數據並且一個並發事務嚐試讀相同的數據(不管是在寫之前還是之後),它不能看到其他事務的工作。讀取事務看起來是第一個執行的,不管哪個是第一個啟動或者哪個是第一個提交。如果就到此為止,則沒有問題,但是如果讀取者也寫入被一個並發事務讀取的數據,現在有一個事務好像是已經在前麵提到的任何一個事務之前運行。如果看起來最後執行的事務實際上第一個提交,在這些事務的執行順序圖中很容易出現一個環。當這樣一個環出現時,完整性檢查在沒有任何幫助的情況下將不會正確地工作。
正如Section 2.3中提到的,可序列化事務僅僅是可重複讀事務增加了對讀/寫衝突的危險模式的非阻塞監控。當檢測到一個可能導致表麵的執行順序中產生環的模式,涉及到的一個事務將被回滾來打破該環。
4.1. 用可序列化事務來強製一致性
如果可序列化事務隔離級別被用於所有需要一個一致數據視圖的寫入和讀取,不需要其他的工作來保證一致性。在PostgreSQL中,來自於其他環境的被編寫成使用可序列化事務來保證一致性的軟件應該"隻工作"在這一點上。
當使用這種技術時,如果應用軟件通過一個框架來自動重試由於序列化錯誤而回滾的事務,它將避免為應用程序員帶來不必要的負擔。把default_transaction_isolation設置為serializable可能是個好主意。通過觸發器中的事務隔離級別檢查來采取某些動作來保證沒有其他事務隔離級別被使用(由於疏忽或者為了破壞完整性檢查)也是明智的。
Warning
這個級別的使用可序列化事務的完整性保護還沒有擴展到熱備份模式。由於這個原因,那些使用熱備份的係統可能想要在主控機上使用可重複讀和顯式鎖定。
4.2. 使用顯式鎖定強製一致性
當可以使用非可序列化寫時,要保證一行的當前有效性並保護它不受並發更新的影響,我們必須使用SELECT FOR UPDATE、SELECT FOR SHARE或一個合適的LOCK TABLE 語句(SELECT FOR UPDATE和SELECT FOR SHARE鎖隻針對並發更新返回行,而LOCK TABLE會鎖住整個表)。當從其他環境移植應用到PostgreSQL時需要考慮這些。
關於這些來自其他環境的轉換還需要注意的是SELECT FOR UPDATE不保證一個並發事務將不會更新或刪除一個被選中的行。要在PostgreSQL中這樣做,你必須真正地更新該行,即便沒有值需要被改變。SELECT FOR UPDATE 臨時阻塞其他事務,讓它們不能獲取該相同的鎖或者執行一個會影響被鎖定行的UPDATE或DELETE,但是一旦正持有該所鎖的事務提交或回滾,一個被阻塞的事務將繼續執行衝突操作,除非當鎖被持有時一個該行的實際UPDATE被執行。
在非可序列化MVCC環境下,全局有效性檢查需要一些額外的考慮。例如,一個銀行應用可能會希望檢查一個表中的所有扣款總和等於另外一個表中的收款總和,同時兩個表還會被更新。比較兩個連續的在讀已提交模式下不會可靠工作的SELECT sum(...)命令, 因為第二個查詢很可能會包含沒有被第一個查詢考慮的事務提交的結果。在一個單一的可重複讀事務裏進行兩個求和則給出在可串行化事務開始之前提交的所有事務產生的準確結果 — 但有人可能會合理地置疑在結果被遞交的時候,它們是否仍然相關。 如果可重複讀事務本身在嚐試做一致性檢查之前應用了某些變更,那麼檢查的有用性就更加值得討論了, 因為現在它包含了一些(但不是全部)事務開始後的變化。 在這種情況下,一個小心的人可能希望鎖住所有需要檢查的表,這樣才能獲得一個無可置疑的當前現狀的圖像。 一個SHARE模式(或者更高)的鎖保證在被鎖定表中除了當前事務所作的更改之外,沒有未提交的更改。
還要注意如果某人正在依賴顯式鎖定來避免並發更改,那麼他應該使用讀已提交模式, 或者是在可重複讀模式裏在執行命令之前小心地獲取鎖。 在可重複讀事務裏獲取的鎖保證了不會有其它修改該表的事務正在運行,但是如果事務看到的快照在獲取鎖之前, 那麼它可能早於表中一些現在已經提交的更改。 一個可重複讀事務的快照實際上是在它的第一個查詢或者數據修改命令(SELECT、INSERT、UPDATE或DELETE)開始的時候凍結的,因此我們可以在快照凍結之前顯式地獲取鎖。
一些 DDL 命令(當前隻有TRUNCATE和表重寫形式的ALTER TABLE)對於 MVCC 不是安全的。這意味著在截斷或者重寫提交之後,該表將對並發事務(如果它們使用的快照是在 DDL 命令提交前取得的)呈現出空表的形態。這隻對沒有在該 DDL 命令開始前訪問所討論的表的事務存在問題 — 任何在 DDL 命令開始前訪問過該表的事務將持有至少一個 ACCESS SHARE 表鎖,這將阻塞該 DDL 命令直到該事務完成。因此這些命令對於目標表上的連續查詢將不會造成任何明顯的表內容不一致,但是它們可能導致目標表內容和數據庫中其他表內容之間的不一致。
對於可序列化事務隔離級別的支持還沒有被加入到熱備複製目標中。當前在熱備模式中支持的最嚴格的隔離級別是可重複讀。雖然在主控機上用可序列化事務執行所有持久化數據庫寫入將確保所有後備機將最終達到一個一致的狀態,但是運行在後備機上的一個可重複讀事務有時可能會看到一個短暫的、與主控機上事務的任何串行執行都不一致的狀態。
盡管PostgreSQL提供對表數據訪問的非阻塞讀/寫, 但並非PostgreSQL中實現的每一個索引訪問方法當前都能夠提供非阻塞讀/寫訪問。 不同的索引類型按照下麵方法操作:
B-tree、GiST和SP-GiST索引
短期的頁麵級共享/排他鎖被用於讀/寫訪問。每個鎖銀行被取得或被插入後立即釋放鎖。 這些索引類型提供了無死鎖情況的最高並發性。
Hash索引
Hash 桶級別的共享/排他鎖被用於讀/寫訪問。鎖在整個 Hash 桶處理完成後釋放。Hash 桶級鎖比索引級的鎖提供了更好的並發性但是可能產生死鎖,因為鎖持有的時間比一次索引操作時間長。
GIN索引
短期的頁麵級共享/排他鎖被用於讀/寫訪問。 鎖在索引行被插入/抓取後立即釋放。但要注意的是一個 GIN 索引值的插入通常導致對每行產生幾個索引鍵的插入,因此 GIN 可能為了插入一個單一值而做大量的工作。
目前,B-tree 索引為並發應用提供了最好的性能。因為它還有比 Hash 索引更多的特性,在那些需要對標量數據進行索引的並發應用中,我們建議使用 B-tree 索引類型。在處理非標量類型數據的時候,B-tree 就沒什麼用了,應該使用 GiST、SP-GiST 或 GIN 索引替代。
最後更新:2017-08-18 16:02:32