12.2. 交易隔離

SQL 標準用三個必須在並行的交易之間避免的現象定義了四個級別的交易隔離。 這些不希望發生的現象是:

髒讀(dirty reads)

一個交易讀取了另一個未提交的並行交易寫的資料。

不可重複讀(non-repeatable reads)

一個交易重新讀取前面讀取過的資料, 發現該資料已經被另一個已提交的交易修改過。

幻讀(phantom read)

一個交易重新執行一個查詢,返回一套符合查詢條件的行, 發現這些行因為其他最近提交的交易而發生了改變。

這四種隔離級別和對應的行為在Table 12-1 裡描述。

Table 12-1. SQL 交易隔離級別

隔離級別 髒讀(Dirty Read) 不可重複讀(NonRepeatable Read) 幻讀(Phantom Read)
讀未提交(Read uncommitted) 可能 可能 可能
讀已提交(Read committed) 不可能 可能 可能
可重複讀(Repeatable read) 不可能 不可能 可能
可串行化(Serializable ) 不可能 不可能 不可能

PostgreSQL 裡,您可以請求四種可能的交易隔離級別中的任意一種。 但是在內部,實際上只有兩種獨立的隔離級別,分別對應讀已提交和可串行化。 如果您選擇了讀未提交的級別,實際上您用的是讀已提交, 在您選擇可重複的讀級別的時候,實際上您用的是可串行化,所以實際的隔離級別可能比您選擇的更嚴格。 這是 SQL 標準允許的:四種隔離級別只定義了哪種現象不能發生,但是沒有定義那種現象一定發生。 PostgreSQL 只提供兩種隔離級別的原因是, 這是把標準的隔離級別與多版本並發控制架構映射相關的唯一的合理方法。 可用的隔離級別的行為在下面小節裡描述。

12.2.1. 讀已提交隔離級別

讀已提交(Read Committed)PostgreSQL 裡的預設隔離級別。 當一個交易執行在這個隔離級別時, 一個 SELECT 查詢只能看到查詢開始之前提交的資料而永遠無法看到未提交的資料或者是在查詢執行時其他並行的交易提交做的改變。 (不過 SELECT 的確看得見同一次交易中前面更新的結果。即使它們還沒提交也看得到。) 實際上,一個 SELECT 查詢看到一個在該查詢開始執行的瞬間該資料庫的一個快照。 請注意兩個相鄰的 SELECT 命令可能看到不同的資料,哪怕它們是在同一個交易裡, 因為其它交易會在第一個SELECT執行的時候提交.

UPDATEDELETE, 或者 SELECT FOR UPDATE 在搜索目標行的時候的行為和SELECT 一樣: 它們只能找到在命令開始的時候已經提交的行。 不過,這樣的目標行在被找到的時候可能已經被其它並發的交易更新(或者刪除,或者標記為更新的)。 在這種情況下,即將進行的更新將等待第一個更新交易提交或者回滾(如果它還在處理)。 如果第一個更新回滾,那麼它的作用將被忽略,而第二個更新者將繼續更新最初發現的行。 如果第一個更新者提交,那麼如果第一個更新者刪除了該行,則第二個更新者將忽略該行, 否則它將試圖在該行的已更新的版本上施加它的操作。系統將重新計算命令搜索條件(WHERE 子句), 看看該行已更新的辦不那是否仍然符合搜索條件。如果是,則第二個更新繼續其操作,從該行的已更新版本開始。

因為上面的規則,正在更新的命令可能會看到不一致的快照 — 它們可以看到影響它們試圖更新的並發更新命令的效果, 但是它們看不到那些命令對資料庫裡其它行的作用。 這樣的行為令讀已提交模式不適合用於哪種涉及複雜搜索條件的命令。 不過,它對於簡單的情況而言是正確的。比如,假設我們用類似下面這樣的命令更新銀行餘額:

BEGIN;
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 12345;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 7534;
COMMIT;

如果兩個並發交易試圖修改帳號 12345 的餘額,那我們很明顯希望第二個交易是從帳戶行的已經更新過的版本上進行更新。 因為每個命令只是影響一個已經決定了的行,因此讓它看到更新後的版本不會導致任何不一致的問題。

因為在讀已提交模式裡,每個新的命令都是從一個新的快照開始的,而這個快照包含所有到該時刻為止已經提交的交易, 因此同一個交易裡的後面的命令將看到任何已提交的並發交易的效果。 這裡要考慮的問題是我們在一個命令裡是否看到資料庫裡絕對一致的視圖。

讀已提交模式提供的部分交易隔離對於許多應用而言是足夠的,並且這個模式速度快,使用簡單。 不過,對於做複雜查詢和更新的應用,可能需要保證資料庫有比讀已提交模式提供的更加嚴格的一致性視圖。

12.2.2. 可串行化隔離級別

可串行化(Serializable) 級別提供最嚴格的交易隔離。 這個級別模擬串行的交易執行, 就好像交易將被一個接著一個那樣串行的,而不是並行的執行。 不過,使用這個級別的應用必須準備在串行化失敗的時候重新發動交易.

當一個交易處於可串行化級別, 一個 SELECT 查詢只能看到在交易開始之前提交的資料而永遠看不到未提交的資料或交易執行中其他並行交易提交的修改。 (不過,SELECT 的確看得到同一次交易中前面的更新的效果。即使交易還沒有提交也一樣。) 這個行為和讀已提交級別是不太一樣,它的 SELECT 看到的是該交易開始時的快照,而不是該交易內部目前查詢開始時的快照。 這樣,一個交易內部後面的 SELECT 命令總是看到同樣的資料。

UPDATEDELETE,和 SELECT FOR UPDATE 在搜索目標行上的行為和 SELECT 一樣: 它們將只尋找在交易開始的時候已經提交的目標行。但是, 這樣的目標行在被發現的時候可能已經被另外一個並發的交易更新了(或者是刪除或者是標記為更新)。 在這種情況下,可串行化的交易將等待第一個正在更新的交易提交或者回滾(如果它仍然在處理中)。 如果第一個更新者回滾,那麼它的影響將被忽略, 而這個可串行化的就可以繼續更新它最初發現的行。 但是如果第一個更新者提交了(並且實際上更新或者刪除了該行,而不只是為更新選中它)那麼可串行化交易將回滾,並返回下面訊息

ERROR:  Can't serialize access due to concurrent update

因為一個可串行化的交易在可串行化交易開始之後不能更改被其他交易更改過的行。

當應用收到這樣的錯誤訊息時,它應該退出目前的交易然後從頭開始重新進行整個交易。 第二次執行時,該交易看到的前一次提交的修改是該資料庫初始的樣子中的一部分, 所以把新版本的行作為新交易更新的起點不會有邏輯衝突。

請注意只有更新交易才需要重試,只讀交易從來沒有串行化衝突.

可串行化交易級別提供了嚴格的保證:每個交易都看到一個完全一致的資料庫的視圖。 不過,如果並行更新令資料庫不能維持串行執行的樣子,那麼應用必須準備重試交易。 因為重做複雜的交易的開銷可能是非常可觀的,所以我們只建議在更新命令中包含足夠複雜的邏輯, 在讀已提交級別中可能導致錯誤的結果的情況下才使用。 最常見的是,可串行化模式只是在這樣的情況下是必要的:一個交易連續做若干個命令, 而這幾個命令必須看到資料庫完全一樣的視圖。

12.2.2.1. 可串行化隔離與真正的可串行化之比較

執行的"可串行化"的直觀含義(以及數學定義)是兩個成功提交的並發交易將顯得好像嚴格地串行執行一樣, 一個跟著一個 — 儘管我們可能無法預期哪個首先執行。我們必須明白,禁止那些在 Table 12-1 裡面列出的行為並不能保證真正的可串行化, 並且,實際上 PostgreSQL 的可串行化模式並不保證在這種含義下的可串行化。 舉例來說,假設一個資料表 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。 然後兩個交易都提交。所有列出的禁止行為都不會發生,但是我們拿到的結果是不可能在任何一種串行執行下看到的。 如果 A 在 B 之前執行,B 應該計算出總和 330,而不是 300,如果是另外一種順序,那麼 A 計算出的綜合也會不同。

為了保證真正數學上的可串行化,一個資料庫系統必須強制謂詞鎖定, 這就意味著一個交易不能插入或者更改這樣的資料行:這個資料行的資料匹配另外一個並發交易的 WHERE 條件。 比如,一旦交易 A 執行了查詢 SELECT ... WHERE class = 1,那麼一個謂詞鎖定系統將禁止交易 B 插入任何 class 為 1 的新行,直到 A 提交。 [1] 這樣的鎖系統實現起來非常複雜,並且執行起來代價高昂, 因為每個會話都必須要知道每個並發交易的每個查詢的執行細節。 並且這樣大量的開銷在大部分情況下都是浪費掉的, 因為在實際情況下大部分應用都不做會導致問題的這種事情。 (當然,上面的例子是靜心設計的,不能代資料表真實的軟件。) 因此,PostgreSQL 並未實現謂詞鎖定, 而就我們所知,沒有其它的生產中的 DBMS 實現了這個。

在那些非串行化執行真的可能有危險的場合,可以透過使用明確的鎖定來避免問題的法上。 更多的討論在下面的小節進行。

Notes

[1]

實際上,一個謂詞鎖定系統避免了幻讀,方法是約束寫入的東西,而 MVCC 避免幻讀的方法是約束它讀取的東西。