天天看點

PostgreSQL SQL 語言:并發控制

本文檔為postgresql 9.6.0文檔,本轉載已得到原譯者彭煜玮授權。

1. 介紹

postgresql為開發者提供了一組豐富的工具來管理對資料的并發通路。在内部,資料一緻性通過使用一種多版本模型(多版本并發控制,mvcc)來維護。這就意味着每個 sql 語句看到的都隻是一小段時間之前的資料快照(一個資料庫版本),而不管底層資料的目前狀态。這樣可以保護語句不會看到可能由其他在相同資料行上執行更新的并發事務造成的不一緻資料,為每一個資料庫會話提供事務隔離。mvcc避免了傳統的資料庫系統的鎖定方法,将鎖争奪最小化來允許多使用者環境中的合理性能。

使用mvcc并發控制模型而不是鎖定的主要優點是在mvcc中,對查詢(讀)資料的鎖請求與寫資料的鎖請求不沖突,是以讀不會阻塞寫,而寫也從不阻塞讀。甚至在通過使用革新的可序列化快照隔離(ssi)級别提供最嚴格的事務隔離級别時,postgresql也維持這個保證。

在postgresql裡也有表和行級别的鎖功能,用于那些通常不需要完整事務隔離并且想要顯式管理特定沖突點的應用。不過,恰當地使用mvcc通常會提供比鎖更好的性能。另外,由應用定義的咨詢鎖提供了一個獲得不依賴于單一事務的鎖的機制。

2. 事務隔離

sql标準定義了四種隔離級别。最嚴格的是可序列化,在标準中用了一整段來定義它,其中說到一組可序列化事務的任意并發執行被保證效果和以某種順序一個一個執行這些事務一樣。其他三種級别使用并發事務之間互動産生的現象來定義,每一個級别中都要求必須不出現一種現象。注意由于可序列化的定義,在該級别上這些現象都不可能發生(這并不令人驚訝--如果事務的效果與每個時刻隻運作一個的相同,你怎麼可能看見由于互動産生的現象?)。

在各個級别上被禁止出現的現象是:

髒讀

一個事務讀取了另一個并行未送出事務寫入的資料。

不可重複讀

一個事務重新讀取之前讀取過的資料,發現該資料已經被另一個事務(在初始讀之後送出)修改。

幻讀

一個事務重新執行一個傳回符合一個搜尋條件的行集合的查詢, 發現滿足條件的行集合因為另一個最近送出的事務而發生了改變。

序列化異常

成功送出一組事務的結果與這些事務所有可能的串行執行結果都不一緻。

sql 标準和 postgresql 實作的事務隔離級别在 table 13-1中描述。

table 13-1. 事務隔離級别

PostgreSQL SQL 語言:并發控制

在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快照不可見的事務的結果無法讓插入進行 下去。再一次,這隻是讀已送出模式中的情況。

因為上面的規則,正在更新的指令可能會看到一個不一緻的快照: 它們可以看到并發更新指令在它嘗試更新的相同行上的作用,但是卻看不到那些指令對資料庫裡其它行的作用。 這樣的行為令讀已送出模式不适合用于涉及複雜搜尋條件的指令。不過,它對于更簡單的情況是正确的。 例如,考慮用這樣的指令更新銀行餘額:

如果兩個這樣的事務同時嘗試修改帳号 12345 的餘額,那我們很明顯希望第二個事務從賬戶行的已更新版本上開始工作。 因為每個指令隻影響一個已經決定了的行,讓它看到行的已更新版本不會導緻任何麻煩的不一緻性。

在讀已送出模式中,更複雜的使用可能産生不符合需要的結果。例如: 考慮一個在資料上操作的delete指令,它操作的資料正被另一個指令從它的限制條件中移除或者加入,例如,假定website是一個兩行的表,兩行的website.hits等于9和10:

即便在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一樣: 它們将隻找到在事務開始時已經被送出的行。 不過,在被找到時,這樣的目标行可能已經被其它并發事務更新(或删除或鎖住)。在這種情況下, 可重複讀事務将等待第一個更新事務送出或者復原(如果它還在進行中)。 如果第一個更新事務復原,那麼它的作用将被忽略并且可重複讀事務可以繼續更新最初發現的行。 但是如果第一個更新事務送出(并且實際更新或删除該行,而不是隻鎖住它),則可重複讀事務将復原并帶有如下消息

因為一個可重複讀事務無法修改或者鎖住被其他在可重複讀事務開始之後的事務改變的行。

當一個應用接收到這個錯誤消息,它應該中斷目前事務并且從開頭重試整個事務。在第二次執行中,該事務将見到作為其初始資料庫視圖一部分的之前送出的改變,這樣在使用行的新版本作為新事務更新的起點時就不會有邏輯沖突。

注意隻有更新事務可能需要被重試;隻讀事務将永遠不會有序列化沖突。

可重複讀模式提供了一種嚴格的保證,在其中每一個事務看到資料庫的一個完全穩定的視圖。不過,這個視圖并不需要總是和同一級别上并發事務的某些序列化(一次一個)執行保持一緻。例如,即使這個級别上的一個隻讀事務可能看到一個控制記錄被更新,這顯示一個批處理已經被完成但是不能看見作為該批處理的邏輯組成部分的一個細節記錄,因為它讀取空值記錄的一個較早的版本。如果不小心地使用顯式鎖來阻塞沖突事務,嘗試用運作在這個隔離級别的事務來強制業務規則不太可能正确地工作。

note:

在postgresql版本 9.1 之前,一個對于可序列化事務隔離級别的請求會提供和這裡描述的完全一樣的行為。為了保持可序列化行為,現在應該請求可重複讀。

2.3. 可序列化隔離級别

可序列化隔離級别提供了最嚴格的事務隔離。這個級别為所有已送出事務模拟序列事務執行;就好像事務被按照序列一個接着另一個被執行,而不是并行地被執行。但是,和可重複讀級别相似,使用這個級别的應用必須準備好因為序列化失敗而重試事務。事實上,這個給力級别完全像可重複讀一樣地工作,除了它會監視一些條件,這些條件可能導緻一個可序列化事務的并發集合的執行産生的行為與這些事務所有可能的序列化(一次一個)執行不一緻。這種監控不會引入超出可重複讀之外的阻塞,但是監控會産生一些負荷,并且對那些可能導緻序列化異常的條件的檢測将觸發一次序列化失敗。

例如,考慮一個表mytab,它初始時包含:

假設可序列化事務 a 計算:

并且接着把結果(3)作為一個新行的value插入,新行的class = 2。同時,可序列化事務 b 計算:

并得到結果 300,它會将其與class = 1插入到一個新行中。然後兩個事務都嘗試送出。如果其中一個事務運作在可重複讀隔離級别,兩者都被允許送出;但是由于沒有執行的序列化順序能在結果上一緻,使用可序列化事務将允許一個事務送出并且将復原另一個并伴有這個消息:

這是因為,如果 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來鼓勵使用索引掃描将有助于此。一定要在事務復原和重新開機數目的任何減少與查詢執行時間的任何全面改變之間進行權衡。

3. 顯式鎖定

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. 沖突的鎖模式

PostgreSQL SQL 語言:并發控制

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. 沖突的行級鎖

PostgreSQL SQL 語言:并發控制

3.3. 頁級鎖

除了表級别和行級别的鎖以外,頁面級别的共享/排他鎖被用來控制對共享緩沖池中表頁面的讀/寫。 這些鎖在行被抓取或者更新後馬上被釋放。應用開發者通常不需要關心頁級鎖,我們在這裡提到它們隻是為了完整。

3.4. 死鎖

顯式鎖定的使用可能會增加死鎖的可能性,死鎖是指兩個(或多個)事務互相持有對方想要的鎖。例如,如果事務 1 在表 a 上獲得一個排他鎖,同時試圖擷取一個在表 b 上的排他鎖, 而事務 2 已經持有表 b 的排他鎖,同時卻正在請求表 a 上的一個排他鎖,那麼兩個事務就都不能進行下去。postgresql能夠自動檢測到死鎖情況并且會通過中斷其中一個事務進而允許其它事務完成來解決這個問題(具體哪個事務會被中斷是很難預測的,而且也不應該依靠這樣的預測)。

要注意死鎖也可能會作為行級鎖的結果而發生(并且是以,它們即使在沒有使用顯式鎖定的情況下也會發生)。考慮如下情況,兩個并發事務在修改一個表。第一個事務執行:

這樣就在指定帳号的行上獲得了一個行級鎖。然後,第二個事務執行:

第一個update語句成功地在指定行上獲得了一個行級鎖,是以它成功更新了該行。 但是第二個update語句發現它試圖更新的行已經被鎖住了,是以它等待持有該鎖的事務結束。事務二現在就在等待事務一結束,然後再繼續執行。現在,事務一執行:

事務一試圖在指定行上獲得一個行級鎖,但是它得不到:事務二已經持有了這樣的鎖。是以它要等待事務二完成。是以,事務一被事務二阻塞,而事務二也被事務一阻塞:一個死鎖。 postgresql将檢測這樣的情況并中斷其中一個事務。

防止死鎖的最好方法通常是保證所有使用一個資料庫的應用都以一緻的順序在多個對象上獲得鎖。在上面的例子裡,如果兩個事務以同樣的順序更新那些行,那麼就不會發生死鎖。 我們也應該保證一個事務中在一個對象上獲得的第一個鎖是該對象需要的最嚴格的鎖模式。如果我們無法提前驗證這些,那麼可以通過重試因死鎖而中斷的事務來即時處理死鎖。

隻要沒有檢測到死鎖情況,尋求一個表級或行級鎖的事務将無限等待沖突鎖被釋放。這意味着一個應用長時間保持事務開啟不是什麼好事(例如等待使用者輸入)。

3.5. 咨詢鎖

postgresql提供了一種方法建立由應用定義其含義的鎖。這種鎖被稱為咨詢鎖,因為系統并不強迫其使用 — 而是由應用來保證其正确的使用。咨詢鎖可用于 mvcc 模型不适用的鎖定政策。例如,咨詢鎖的一種常用用法是模拟所謂"平面檔案"資料管理系統典型的悲觀鎖政策。雖然一個存儲在表中的标志可以被用于相同目的,但咨詢鎖更快、可以避免表膨脹并且會由伺服器在會話結束時自動清理。

有兩種方法在postgresql中擷取一個咨詢鎖:在會話級别或在事務級别。一旦在會話級别獲得了咨詢鎖,它将被保持直到被顯式釋放或會話結束。不同于标準鎖請求,會話級咨詢鎖請求不尊重事務語義:在一個後來被復原的事務中得到的鎖在復原後仍然被保持,并且同樣即使調用它的事務後來失敗一個解鎖也是有效的。一個鎖在它所屬的程序中可以被擷取多次;對于每一個完成的鎖請求必須有一個相應的解鎖請求,直至鎖被真正釋放。在另一方面,事務級鎖請求的行為更像普通鎖請求:在事務結束時會自動釋放它們,并且沒有顯式的解鎖操作。這種行為通常比會話級别的行為更友善,因為它使用一個咨詢鎖的時間更短。對于同一咨詢鎖辨別符的會話級别和事務級别的鎖請求按照期望将彼此阻塞。如果一個會話已經持有了一個給定的咨詢鎖,由它發出的附加請求将總是成功,即使有其他會話在等待該鎖;不管現有的鎖和新請求是處在會話級别還是事務級别,這種說法都是真的。

和所有postgresql中的鎖一樣,目前被任何會話所持有的咨詢鎖的完整清單可以在pg_locks系統視圖中找到。

咨詢鎖和普通鎖都被存儲在一個共享記憶體池中,它的尺寸由max_locks_per_transaction和max_connections配置變量定義。 必須當心不要耗盡這些記憶體,否則伺服器将不能再授予任何鎖。這對伺服器可以授予的咨詢鎖數量設定了一個上限,根據伺服器的配置不同,這個限制通常是數萬到數十萬。

在使用咨詢鎖方法的特定情況下,特别是查詢中涉及顯式排序和limit子句時,由于 sql 表達式被計算的順序,必須小心控制鎖的擷取。例如:

在上述查詢中,第二種形式是危險的,因為不能保證在鎖定函數被執行之前應用limit。這可能導緻獲得某些應用不期望的鎖,并是以在會話結束之前無法釋放。 從應用的角度來看,這樣的鎖将被挂起,雖然它們仍然在pg_locks中可見。

4. 應用級别的資料完整性檢查

對于使用讀已送出事務的資料完整性強制業務規則非常困難,因為對每一個語句資料視圖都在變化,并且如果一個寫沖突發生即使一個單一語句也不能把它自己限制到該語句的快照。

雖然一個可重複讀事務在其執行期間有一個穩定的資料視圖,在使用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)開始的時候當機的,是以我們可以在快照當機之前顯式地擷取鎖。

5. 提醒

一些 ddl 指令(目前隻有truncate和表重寫形式的alter table)對于 mvcc 不是安全的。這意味着在截斷或者重寫送出之後,該表将對并發事務(如果它們使用的快照是在 ddl 指令送出前取得的)呈現出空表的形态。這隻對沒有在該 ddl 指令開始前通路所讨論的表的事務存在問題 — 任何在 ddl 指令開始前通路過該表的事務将持有至少一個 access share 表鎖,這将阻塞該 ddl 指令直到該事務完成。是以這些指令對于目标表上的連續查詢将不會造成任何明顯的表内容不一緻,但是它們可能導緻目标表内容和資料庫中其他表内容之間的不一緻。

對于可序列化事務隔離級别的支援還沒有被加入到熱備複制目标中。目前在熱備模式中支援的最嚴格的隔離級别是可重複讀。雖然在主要機上用可序列化事務執行所有持久化資料庫寫入将確定所有後備機将最終達到一個一緻的狀态,但是運作在後備機上的一個可重複讀事務有時可能會看到一個短暫的、與主要機上事務的任何串行執行都不一緻的狀态。

6. 鎖定和索引

盡管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 索引替代。