淺析Postgres中的併發控制(Concurrency Control)與事務特性(下)

上文咱們討論了PostgreSQL的MVCC相關的基礎知識以及實現機制。關於PostgreSQL中的MVCC,咱們只講了元組可見性的問題,還剩下兩個問題沒講。一個是"Lost Update"問題,另外一個是PostgreSQL中的序列化快照隔離機制(SSI,Serializable Snapshot Isolation)。今天咱們就來繼續討論。html


3.2 Lost Update

所謂"Lost Update"就是寫寫衝突。當兩個併發事務同時更新同一條數據時發生。"Lost Update"必須在REPEATABLE READ 和 SERIALIZABLE 隔離級別上被避免,即拒絕併發地更新同一條數據。下面咱們看看在PostgreSQL上如何處理"Lost Update"sql

有關PostgreSQL的UPDATE操做,咱們能夠看看ExecUpdate()這個函數。然而今天咱們不講具體的函數,咱們形而上一點。只從理論出發。咱們只討論下UPDATE執行時的情形,這意味着,咱們不討論什麼觸發器啊,查詢重寫這些雜七雜八的,只看最"乾淨"的UPDATE操做。並且,咱們討論的是兩個併發事務的UPDATE操做。併發

請看下圖,下圖顯示了兩個併發事務中UPDATE同一個tuple時的處理。函數

  • [1]目標tuple處於正在更新的狀態

咱們看到Tx_A和Tx_B在併發執行,Tx_A先更新了tuple,這時Tx_B準備去更新tuple,發現Tx_A更新了tuple,可是尚未提交。因而,Tx_B處於等待狀態,等待Tx_A結束(commit或者abort)。post

當Tx_A提交時,Tx_B解除等待狀態,準備更新tuple,這時分兩個狀況:若是Tx_B的隔離級別是READ COMMITTED,那麼OK,Tx_B進行UPDATE(能夠看出,此時發生了Lost Update)。若是Tx_B的隔離級別是REPEATABLE READ或者是SERIALIZABLE,那麼Tx_B會當即被abort,放棄更新。從而避免了Lost Update的發生。this

當Tx_A和Tx_B的隔離級別都爲READ COMMITTED時的例子:操作系統

Tx_A Tx_B
postgres=# START TRANSACTION ISOLATION LEVEL READ COMMITTED ;
START TRANSACTION
postgres=# update test set b = b+1 where a =1;
UPDATE 1
postgres=# commit;
COMMIT


postgres=# START TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION
postgres=# update test set b = b+1;

↓this transaction is being blocked

UPDATE 1

當Tx_A的隔離級別爲READ COMMITTED,Tx_B的隔離級別爲REPEATABLE READ時的例子:postgresql

Tx_A Tx_B
postgres=# START TRANSACTION ISOLATION LEVEL READ COMMITTED ;
START TRANSACTION
postgres=# update test set b = b+1 where a =1;
UPDATE 1
postgres=# commit;
COMMIT


postgres=# START TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION
postgres=# update test set b = b+1;

↓this transaction is being blocked

ERROR:couldn't serialize access due to concurrent update
  • [2]目標tuple已經被併發的事務更新

咱們看到Tx_A和Tx_B在併發執行,Tx_A先更新了tuple而且已經commit,Tx_B再去更新tuple時發現它已經被更新過了而且已經提交。若是Tx_B的隔離級別是READ COMMITTED,根據咱們前面說的,,Tx_B在執行UPDATE前會從新獲取snapshot,發現Tx_A的此次更新對於Tx_B是可見的,所以Tx_B繼續更新Tx_A更新過得元組(Lost Update)。而若是Tx_B的隔離級別是REPEATABLE READ或者是SERIALIZABLE,那麼顯然咱們會終止當前事務來避免Lost Update。code

當Tx_A的隔離級別爲READ COMMITTED,Tx_B的隔離級別爲REPEATABLE READ時的例子:htm

Tx_A Tx_B
postgres=# START TRANSACTION ISOLATION LEVEL READ COMMITTED ;
START TRANSACTION
postgres=# update test set b = b+1 where a =1;
UPDATE 1
postgres=# commit;
COMMIT
postgres=# START TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION
postgres=# select * from test ;
a b
---+---
1 5
(1 row)
postgres=# update test set b = b+1
ERROR: could not serialize access due to concurrent update
  • [3]更新無衝突

這個很顯然,沒有衝突就沒有傷害。Tx_A和Tx_B照常更新,不會有Lost Update。

從上面咱們也能夠看出,在使用SI(Snapshot Isolation)機制時,兩個併發事務同時更新一條記錄時,先更新的那一方得到更新的優先權。可是在下面提到的SSI機制中會有所不一樣,先提交的事務得到更新的優先權。


3.3 SSI(Serializable Snapshot Isolation)

SSI,可序列化快照隔離,是PostgreSQL在9.1以後,爲了實現真正的SERIALIZABLE(可序列化)隔離級別而引入的。

對於SERIALIZABLE隔離級別,官方介紹以下:

可序列化隔離級別提供了最嚴格的事務隔離。這個級別爲全部已提交事務模擬序列事務執行;就好像事務被按照序列一個接着另外一個被執行,而不是並行地被執行。可是,和可重複讀級別類似,使用這個級別的應用必須準備好由於序列化失敗而重試事務。事實上,這個隔離級別徹底像可重複讀同樣地工做,除了它會監視一些條件,這些條件可能致使一個可序列化事務的併發集合的執行產生的行爲與這些事務全部可能的序列化(一次一個)執行不一致。這種監控不會引入超出可重複讀以外的阻塞,可是監控會產生一些負荷,而且對那些可能致使序列化異常的條件的檢測將觸發一次序列化失敗。

講的比較繁瑣,個人理解是:

1.只針對隔離級別爲SERIALIZABLE的事務;
2.併發的SERIALIZABLE事務與按某一個順序單獨的一個一個執行的結果相同。

條件1很好理解,系統只判斷併發的SERIALIZABLE的事務之間的衝突;
條件2個人理解就是併發的SERIALIZABLE的事務不能同時修改和讀取同一個數據,不然由併發執行和前後按序列執行就會不一致。

可是這個不能同時修改和讀取同一個數據要限制在多大的粒度呢?
咱們分狀況討論下。

  • [1] 讀寫同一條數據

彷佛沒啥問題嘛,根據前面的論述,這裏的一致性在REPEATABLE READ階段就保證了,不會有問題。

以此類推,咱們同時讀寫2,3,4....n條數據,沒問題。

  • [2]讀寫閉環

啥是讀寫閉環?這我我造的概念,相似於操做系統中的死鎖,即事務Tx_A讀tuple1,更新tuple2,而Tx_B偏偏相反,讀tuple2, 更新tuple1.

咱們假設事務開始前的tuple1,tuple2爲tuple1_1,tuple2_1,Tx_A和Tx_B更新後的tuple1,tuple2爲tuple1_2,tuple2_2。

這樣在併發下:

Tx_A讀到的tuple1是tuple1_1,tuple2是tuple2_1。
同理,Tx_B讀到的tuple1是tuple1_1,tuple2是tuple2_1。

而若是咱們以Tx_A,Tx_B的順序串行執行時,結果爲:

Tx_A讀到的tuple1是tuple1_1,tuple2是tuple2_1。
Tx_B讀到的tuple1是tuple1_2(被Tx_A更新了),tuple2是tuple2_1。

反之,而若是咱們以Tx_B,Tx_A的順序串行執行時,結果爲:

Tx_B讀到的tuple1是tuple1_1,tuple2是tuple2_1。
Tx_A讀到的tuple1是tuple1_1,tuple2是tuple2_2(被Tx_B更新了)。

能夠看出,這三個結果都不同,不知足條件2,即併發的Tx_A和Tx_B不能被模擬爲Tx_A和Tx_B的任意一個序列執行,致使序列化失敗

其實我上面提到的讀寫閉環,更正式的說法是:序列化異常。上面說的那麼多,其實下面兩張圖便可解釋。

關於這個*-conflicts咱們遇到好幾個了。咱們先總結下:

wr-conflicts (Dirty Reads)
ww-conflicts (Lost Updates)
rw-conflicts (serialization anomaly)

下面說的SSI機制,就是用來解決rw-conflicts的。

好的,下面就開始說怎麼檢測這個序列化異常問題,也就是說,咱們要開始瞭解下SSI機制了。

在PostgreSQL中,使用如下方法來實現SSI:

  1. 利用SIREAD LOCK(謂詞鎖)記錄每個事務訪問的對象(tuple、page和relation);

  2. 在事務寫堆表或者索引元組時利用SIREAD LOCK監測是否存在衝突;

  3. 若是發現到衝突(即序列化異常),abort該事務。

從上面能夠看出,SIREAD LOCK是一個很重要的概念。解釋了這個SIREAD LOCK,咱們也就基本上理解了SSI。

所謂的SIREAD LOCK,在PostgreSQL內部被稱爲謂詞鎖。他的形式以下:

SIREAD LOCK := { tuple|page|relation, {txid [, ...]} }

也就是說,一個謂詞鎖分爲兩個部分:前一部分記錄被"鎖定"的對象(tuple、page和relation),後一部分記錄同時訪問了該對象的事務的virtual txid(有關它和txid的區別,這裏就不作多介紹了)。

SIREAD LOCK的實如今函數CheckForSerializableConflictOut中。該函數在隔離級別爲SERIALIZABLE的事務中發生做用,記錄該事務中全部DML語句所形成的影響。

例如,若是txid爲100的事務讀取了tuple_1,則建立一個SIREAD LOCK爲{tuple_1, {100}}。此時,若是另外一個txid爲101的事務也讀取了tuple_1,則該SIREAD LOCK升級爲{tuple_1, {100,101}}。須要注意的是若是在DML語句中訪問了索引,那麼索引中的元組也會被檢測,建立對應的SIREAD LOCK。

SIREAD LOCK的粒度分爲三級:tuple|page|relation。若是同一個page中的全部tuple都被建立了SIREAD LOCK,那麼直接建立page級別的SIREAD LOCK,同時釋放該page下的全部tuple級別的SIREAD LOCK。同理,若是一個relation的全部page都被建立了SIREAD LOCK,那麼直接建立relation級別的SIREAD LOCK,同時釋放該relation下的全部page級別的SIREAD LOCK。

當咱們執行SQL語句使用的是sequential scan時,會直接建立一個relation 級別的SIREAD LOCK,而使用的是index scan時,只會對heap tuple和index page建立SIREAD LOCK。

同時,我仍是要說明的是,對於index的處理時,SIREAD LOCK的最小粒度是page,也就是說你即便只訪問了index中的一個index tuple,該index tuple所在的整個page都被加上了SIREAD LOCK。這個特性經常會致使意想不到的序列化異常,咱們能夠在後面的例子中看到。

有了SIREAD LOCK的概念,咱們如今使用它來檢測rw-conflicts。

所謂rw-conflicts,簡單地說,就是有一個SIREAD LOCK,還有分別read和write這個SIREAD LOCK中的對象的兩個併發的Serializable事務。

這個時候,另一個函數閃亮登場:CheckForSerializableConflictIn()。每當隔離級別爲Serializable事務中執行INSERT/UPDATE/DELETE語句時,則調用該函數判斷是否存在rw-conflicts。

例如,當txid爲100的事務讀取了tuple_1,建立了SIREAD LOCK : {tuple_1, {100}}。此時,txid爲101的事務更新tuple_1。此時調用CheckForSerializableConflictIn()發現存在這樣一個狀態: {r=100, w=101, {Tuple_1}}。顯然,檢測出這是一個rw-conflicts。

下面是舉例時間。

首先,咱們有這樣一個表:

testdb=# CREATE TABLE tbl (id INT primary key, flag bool DEFAULT false);
testdb=# INSERT INTO tbl (id) SELECT generate_series(1,2000);
testdb=# ANALYZE tbl;

併發執行的Serializable事務像下面那樣執行:

假設全部的SQL語句都走的index scan。這樣,當SQL語句執行時,不只要讀取對應的heap tuple,還要讀取heap tuple 對應的index tuple。以下圖:

執行狀態以下:
T1: Tx_A執行SELECT語句,該語句讀取了heap tuple(Tuple_2000)和index page(Pkey2);

T2: Tx_B執行SELECT語句,該語句讀取了heap tuple(Tuple_1)和index page(Pkey1);

T3: Tx_A執行UPDATE語句,該語句更新了Tuple_1;

T4: Tx_B執行UPDATE語句,該語句更新了Tuple_2000;

T5: Tx_A commit;

T6: Tx_B commit; 因爲序列化異常,commit失敗,狀態爲abort。

這時咱們來看一下SIREAD LOCK的狀況。

T1: Tx_A執行SELECT語句,調用CheckForSerializableConflictOut()建立了SIREAD LOCK:L1={Pkey_2,{Tx_A}} 和 L2={Tuple_2000,{Tx_A}};

T2: Tx_B執行SELECT語句,調用CheckForSerializableConflictOut建立了SIREAD LOCK:L3={Pkey_1,{Tx_B}} 和 L4={Tuple_1,{Tx_B}};

T3: Tx_A執行UPDATE語句,調用CheckForSerializableConflictIn(),發現並建立了rw-conflict :C1={r=Tx_B, w=Tx_A,{Pkey_1,Tuple_1}}。這很顯然,由於Tx_B和TX_A分別read和write這兩個object。

T4: Tx_A執行UPDATE語句,調用CheckForSerializableConflictIn(),發現並建立了rw-conflict :C1={r=Tx_A, w=Tx_B,{Pkey_2,Tuple_2000}}。到這裏,咱們發現C1和C2構成了precedence graph中的一個環。所以,Tx_A和Tx_B這兩個事務都進入了non-serializable狀態。可是因爲Tx_A和Tx_B都未commit,所以CheckForSerializableConflictIn()並不會abort Tx_B(爲何不abort Tx_A?所以PostgreSQL的SSI機制中採用的是first-committer-win,即發生衝突後,先提交的事務保留,後提交的事務abort。)

T5: Tx_A commit;調用PreCommit_CheckForSerializationFailure()函數。該函數也會檢測是否存在序列化異常。顯然此時Tx_A和Tx_B處於序列化衝突之中,而因爲發現Tx_B仍然在執行中,因此,容許Tx_A commit。

T6: Tx_B commit; 因爲序列化異常,且和Tx_B存在序列化衝突的Tx_A已經被提交。所以commit失敗,狀態爲abort。

更多更復雜的例子,能夠參考這裏.

前面在討論SIREAD LOCK時,咱們談到對於index的處理時,SIREAD LOCK的最小粒度是page。這個特性會致使意想不到的序列化異常。更專業的說法是"False-Positive Serialization Anomalies"。簡而言之實際上並無發生序列化異常,可是咱們的SSI機制不完善,產生了誤報。

下面咱們來舉例說明。

對於上圖,若是SQL語句走的是sequential scan,情形以下:

若是是index scan呢?仍是有可能出現誤報:


這篇就是這樣。依然仍是有不少問題沒有講清楚。留待下次再說吧(拖延症晚期)。

相關文章
相關標籤/搜索