深刻解析 PostgreSQL 系列整理自 The Internals of PostgreSQL 等系列文章,從碎片化地閱讀到體系化地學習,感受對數據庫有了更深刻地瞭解;舉一反三,相互印證,也是有利於掌握 MySQL 等其餘的關係型數據庫或者 NoSQL 數據庫。
併發控制旨在針對數據庫中對事務並行的場景,保證 ACID 中的一致性(Consistency)與隔離(Isolation)。數據庫技術中主流的三種併發控制技術分別是: Multi-version Concurrency Control (MVCC), Strict Two-Phase Locking (S2PL), 以及 Optimistic Concurrency Control (OCC),每種技術也都有不少的變種。在 MVCC 中,每次寫操做都會在舊的版本之上建立新的版本,而且會保留舊的版本。當某個事務須要讀取數據時,數據庫系統會從全部的版本中選取出符合該事務隔離級別要求的版本。MVCC 的最大優點在於讀並不會阻塞寫,寫也不會阻塞讀;而像 S2PL 這樣的系統,寫事務會事先獲取到排他鎖,從而會阻塞讀事務。html
PostgreSQL 以及 Oracle 等 RDBMS 實際使用了所謂的 Snapshot Isolation(SI)這個 MVCC 技術的變種。Oracle 引入了額外的 Rollback Segments,當寫入新的數據時,老版本的數據會被寫入到 Rollback Segment 中,隨後再被覆寫到實際的數據塊。PostgreSQL 則是使用了相對簡單的實現方式,新的數據對象會被直接插入到關聯的 Table Page 中;而在讀取表數據的時候,PostgreSQL 會經過可見性檢測規則(Visibility Check Rules)來選擇合適的版本。git
SI 可以避免 ANSI SQL-92 標準中定義的三個反常現象:髒讀(Dirty Reads),不可重複讀(Non-Repeatable Reads)以及幻讀(Phantom Reads);在 9.1 版本後引入的 Serializable Snapshot Isolation(SSI)則可以提供真正的順序讀寫的能力。github
Isolation Level | Dirty Reads | Non-repeatable Read | Phantom Read | Serialization Anomaly |
---|---|---|---|---|
READ COMMITTED | Not possible | Possible | Possible | Possible |
REPEATABLE READ | Not possible | Not possible | Not possible in PG; See Section 5.7.2. (Possible in ANSI SQL) | Possible |
SERIALIZABLE | Not possible | Not possible | Not possible | Not possible |
當某個事務開啓時,PostgreSQL 內置的 Transaction Manager 會爲它分配惟一的 Transaction ID(txid);txid 是 32 位無類型整型值,能夠經過 txid_current()
函數來獲取當前的 txid:sql
testdb=# BEGIN; BEGIN testdb=# SELECT txid_current(); txid_current -------------- 100 (1 row)
PostgreSQL 還保留了三個關鍵 txid 值做特殊標記:0 表示無效的 txid,1 表示啓動時的 txid,僅在 Database Cluster 啓動時使用;2 表明了被凍結的(Frozen)txid,用於在序列化事務時候使用。PostgreSQL 選擇數值類型做爲 txid,也是爲了方便進行比較;對於 txid 值爲 100 的事務而言,全部小於 100 的事務是發生在過去的,可見的;而全部大於 100 的事務,是發生在將來,即不可見的。數據庫
鑑於實際系統中的 txid 數目的須要可能會超過最大值,PostgreSQL 實際是將這些 txid 做爲環來看待。數組
Table Pages 中的 Heap Tuples 每每包含三個部分:HeapTupleHeaderData 結構,NULL bitmap 以及用戶數據。服務器
其中 HeapTupleHeaderData 與事物處理強相關的屬性有:數據結構
BEGIN; INSERT; INSERT; INSERT; COMMIT;
這個事務,若是是首個 INSERT 命令建立的 Tuple,那麼其 t_cid 值爲 0,第二個就是 1如上所述,Table Pages 中的 Tuples 呈以下佈局:架構
在執行插入操做時,PostgreSQL 會直接將某個新的 Tuple 插入到目標表的某個頁中:併發
假如某個 txid 爲 99 的事務插入了新的 Tuple,那麼該 Tuple 的頭域會被設置爲以下值:
(0, 1)
,即指向了本身testdb=# CREATE EXTENSION pageinspect; CREATE EXTENSION testdb=# CREATE TABLE tbl (data text); CREATE TABLE testdb=# INSERT INTO tbl VALUES('A'); INSERT 0 1 testdb=# SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid FROM heap_page_items(get_raw_page('tbl', 0)); tuple | t_xmin | t_xmax | t_cid | t_ctid -------+--------+--------+-------+-------- 1 | 99 | 0 | 0 | (0,1)
在刪除操做中,目標 Tuple 會被先邏輯刪除,即將 t_xmax 的值設置爲當前刪除該 Tuple 的事務的 txid 值。
當該事務被提交以後,PostgreSQL 會將該 Tuple 標記爲 Dead Tuple,並隨後在 VACUUM 處理過程當中被完全清除。
在更新操做時,PostgreSQL 會首先邏輯刪除最新的 Tuple,而後插入新的 Tuple:
上圖所示的行被 txid 爲 99 的事務插入,被 txid 爲 100 的事務連續更新兩次;在該事務提交以後,Tuple_2 與 Tuple_3 就會被標記爲 Dead Tuples。
當插入某個 Heap Tuple 或者 Index Tuple 時,PostgreSQL 使用相關表的 FSM 來決定應該選擇哪一個 Page 來進行具體的插入操做。每一個 FSM 都存放着表或者索引文件相關的剩餘空間容量的信息,可使用以下方式查看:
testdb=# CREATE EXTENSION pg_freespacemap; CREATE EXTENSION testdb=# SELECT *, round(100 * avail/8192 ,2) as "freespace ratio" FROM pg_freespace('accounts'); blkno | avail | freespace ratio -------+-------+----------------- 0 | 7904 | 96.00 1 | 7520 | 91.00 2 | 7136 | 87.00 3 | 7136 | 87.00 4 | 7136 | 87.00 5 | 7136 | 87.00 ....
PostgreSQL 使用 Commit Log,亦稱 clog 來存放事務的狀態;clog 存放於 Shared Memory 中,在整個事務處理的生命週期中都起到了重要的做用。PostgreSQL 定義了四種不一樣的事務狀態:IN_PROGRESS, COMMITTED, ABORTED, 以及 SUB_COMMITTED。
Clog 有 Shared Memory 中多個 8KB 大小的頁構成,其邏輯上表現爲類數組結構,數組下標便是關聯的事務的 txid,而值就是當前事務的狀態:
若是當前的 txid 超過了當前 clog 頁可承載的最大範圍,那麼 PostgreSQL 會自動建立新頁。而在 PostgreSQL 中止或者 Checkpoint 進程運行的時候,clog 的數據會被持久化存儲到 pg_xact 子目錄下,以 0000,0001 依次順序命名,單個文件的最大尺寸爲 256KB。而當 PostgreSQL 重啓的時候,存放在 pg_xact 目錄下的文件會被從新加載到內存中。而隨着 PostgreSQL 的持續運行,clog 中勢必會累計不少的過期或者無用的數據,Vacuum 處理過程當中一樣會清除這些無用的數據。
事務快照便是存放了當前所有事務是否爲激活狀態信息的數據結構,PostgreSQL 內部將快照表示爲簡單的文本結構,xmin:xmax:xip_list’
;譬如 "100",其意味着全部 txid 小於或者等於 99 的事務是非激活狀態,而大於等於 100 的事務是處在了激活狀態。
testdb=# SELECT txid_current_snapshot(); txid_current_snapshot ----------------------- 100:104:100,102 (1 row)
以 100:104:100,102
爲例,其示意圖以下所示:
事務快照主要由事務管理器(Transaction Manager)提供,在 READ COMMITTED 這個隔離級別,不管是否有 SQL 命令執行,該事務都會被分配到某個快照;而對於 REPEATABLE READ 或者 SERIALIZABLE 隔離級別的事務而言,僅當首個 SQL 語句被執行的時候,纔會被分配到某個事務快照用於進行可見性檢測。事務快照的意義在於,當某個快照進行可見性判斷時,不管目標事務是否已經被提交或者放棄,只要他在快照中被標記爲 Active,那麼其就會被當作 IN_PROGRESS 狀態的事務來處理。
事務管理器始終保存有關當前運行的事務的信息。假設三個事務一個接一個地開始,而且 Transaction_A 和 Transaction_B 的隔離級別是 READ COMMITTED,Transaction_C 的隔離級別是 REPEATABLE READ。
T1:
T2:
T3:
T4:
T5:
可見性檢測的規則用於根據 Tuple 的 t_xmin 與 t_xmax,clog 以及自身分配到的事務快照來決定某個 Tuple 相對於某個事務是否可見。
當某個 Tuple 的 t_xmin 值對應的事務的狀態爲 ABORTED 時候,該 Tuple 永遠是不可見的:
/* t_xmin status = ABORTED */ // Rule 1: If Status(t_xmin) = ABORTED ⇒ Invisible Rule 1: IF t_xmin status is 'ABORTED' THEN RETURN 'Invisible' END IF
對於非插入該 Tuple 的事務以外的其餘事務關聯的 Tuple 而言,該 Tuple 永遠是不可見的;僅對於與該 Tuple 同屬一事務的 Tuple 可見(此時該 Tuple 未被刪除或者更新的)。
/* t_xmin status = IN_PROGRESS */ IF t_xmin status is 'IN_PROGRESS' THEN IF t_xmin = current_txid THEN // Rule 2: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax = INVAILD ⇒ Visible Rule 2: IF t_xmax = INVALID THEN RETURN 'Visible' // Rule 3: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin = current_txid ∧ t_xmax ≠ INVAILD ⇒ Invisible Rule 3: ELSE /* this tuple has been deleted or updated by the current transaction itself. */ RETURN 'Invisible' END IF // Rule 4: If Status(t_xmin) = IN_PROGRESS ∧ t_xmin ≠ current_txid ⇒ Invisible Rule 4: ELSE /* t_xmin ≠ current_txid */ RETURN 'Invisible' END IF END IF
此時該 Tuple 在大部分狀況下都是可見的,除了該 Tuple 被更新或者刪除。
/* t_xmin status = COMMITTED */ IF t_xmin status is 'COMMITTED' THEN // If Status(t_xmin) = COMMITTED ∧ Snapshot(t_xmin) = active ⇒ Invisible Rule 5: IF t_xmin is active in the obtained transaction snapshot THEN RETURN 'Invisible' // If Status(t_xmin) = COMMITTED ∧ (t_xmax = INVALID ∨ Status(t_xmax) = ABORTED) ⇒ Visible Rule 6: ELSE IF t_xmax = INVALID OR status of t_xmax is 'ABORTED' THEN RETURN 'Visible' ELSE IF t_xmax status is 'IN_PROGRESS' THEN // If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax = current_txid ⇒ Invisible Rule 7: IF t_xmax = current_txid THEN RETURN 'Invisible' // If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = IN_PROGRESS ∧ t_xmax ≠ current_txid ⇒ Visible Rule 8: ELSE /* t_xmax ≠ current_txid */ RETURN 'Visible' END IF ELSE IF t_xmax status is 'COMMITTED' THEN // If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) = active ⇒ Visible Rule 9: IF t_xmax is active in the obtained transaction snapshot THEN RETURN 'Visible' // If Status(t_xmin) = COMMITTED ∧ Status(t_xmax) = COMMITTED ∧ Snapshot(t_xmax) ≠ active ⇒ Invisible Rule 10: ELSE RETURN 'Invisible' END IF END IF END IF
以簡單的雙事務更新與查詢爲例:
上圖中 txid 200 的事務的隔離級別是 READ COMMITED,txid 201 的隔離級別爲 READ COMMITED 或者 REPEATABLE READ。
根據 Rule 6,此時僅有 Tuple_1
是處於可見狀態:
# Rule6(Tuple_1) ⇒ Status(t_xmin:199) = COMMITTED ∧ t_xmax = INVALID ⇒ Visible testdb=# -- txid 200 testdb=# SELECT * FROM tbl; name -------- Jekyll (1 row) testdb=# -- txid 201 testdb=# SELECT * FROM tbl; name -------- Jekyll (1 row)
對於 txid 200 的事務而言,根據 Rule 7 與 Rule 2 可知,Tuple_1
可見而 Tuple_2
不可見:
# Rule7(Tuple_1): Status(t_xmin:199) = COMMITTED ∧ Status(t_xmax:200) = IN_PROGRESS ∧ t_xmax:200 = current_txid:200 ⇒ Invisible # Rule2(Tuple_2): Status(t_xmin:200) = IN_PROGRESS ∧ t_xmin:200 = current_txid:200 ∧ t_xmax = INVAILD ⇒ Visible testdb=# -- txid 200 testdb=# SELECT * FROM tbl; name ------ Hyde (1 row)
而對於 txid 201 的事務而言,Tuple_1
是可見的,Tuple_2
是不可見的:
# Rule8(Tuple_1): Status(t_xmin:199) = COMMITTED ∧ Status(t_xmax:200) = IN_PROGRESS ∧ t_xmax:200 ≠ current_txid:201 ⇒ Visible # Rule4(Tuple_2): Status(t_xmin:200) = IN_PROGRESS ∧ t_xmin:200 ≠ current_txid:201 ⇒ Invisible testdb=# -- txid 201 testdb=# SELECT * FROM tbl; name -------- Jekyll (1 row)
若是此時 txid 201 的事務處於 READ COMMITED 的隔離級別,那麼 txid 200 會被當作 COMMITTED 來處理,由於此時獲取到的事務快照是 201:201:
,所以 Tuple_1
是不可見的,而 Tuple_2
是可見的:
# Rule10(Tuple_1): Status(t_xmin:199) = COMMITTED ∧ Status(t_xmax:200) = COMMITTED ∧ Snapshot(t_xmax:200) ≠ active ⇒ Invisible # Rule6(Tuple_2): Status(t_xmin:200) = COMMITTED ∧ t_xmax = INVALID ⇒ Visible testdb=# -- txid 201 (READ COMMITTED) testdb=# SELECT * FROM tbl; name ------ Hyde (1 row)
若是此時 txid 201 的事務處於 REPEATABLE READ 的隔離級別,此時獲取到的事務快照仍是 200:200:
,那麼 txid 200 的事務必須被當作 IN_PROGRESS 狀態來處理;所以此時 Tuple_1
是可見的,而 Tuple_2
是不可見的:
# Rule9(Tuple_1): Status(t_xmin:199) = COMMITTED ∧ Status(t_xmax:200) = COMMITTED ∧ Snapshot(t_xmax:200) = active ⇒ Visible # Rule5(Tuple_2): Status(t_xmin:200) = COMMITTED ∧ Snapshot(t_xmin:200) = active ⇒ Invisible testdb=# -- txid 201 (REPEATABLE READ) testdb=# SELECT * FROM tbl; name -------- Jekyll (1 row)
所謂的 更新丟失(Lost Update),也就是寫衝突(ww-conflict),其出如今兩個事務同時更新相同的行;在 PostgreSQL 中,REPEATABLE READ 與 SERIALIZABLE 這兩個級別都須要規避這種異常現象。
(1) FOR each row that will be updated by this UPDATE command (2) WHILE true /* The First Block */ (3) IF the target row is being updated THEN (4) WAIT for the termination of the transaction that updated the target row (5) IF (the status of the terminated transaction is COMMITTED) AND (the isolation level of this transaction is REPEATABLE READ or SERIALIZABLE) THEN (6) ABORT this transaction /* First-Updater-Win */ ELSE (7) GOTO step (2) END IF /* The Second Block */ (8) ELSE IF the target row has been updated by another concurrent transaction THEN (9) IF (the isolation level of this transaction is READ COMMITTED THEN (10) UPDATE the target row ELSE (11) ABORT this transaction /* First-Updater-Win */ END IF /* The Third Block */ ELSE /* The target row is not yet modified or has been updated by a terminated transaction. */ (12) UPDATE the target row END IF END WHILE END FOR
在上述流程中,UPDATE 命令會遍歷每一個待更新行,當發現該行正在被其餘事務更新時進入等待狀態直到該行被解除鎖定。若是該行已經被更新,而且隔離級別爲 REPEATABLE 或者 SERIALIZABLE,則放棄更新。
Being updated 意味着該行由另外一個併發事務更新,而且其事務還沒有終止。由於 PostgreSQL 的 SI 使用 first-updater-win 方案, 在這種狀況下,當前事務必須等待更新目標行的事務的終止。假設事務 Tx_A 和 Tx_B 同時運行,而且 Tx_B 嘗試更新行;可是 Tx_A 已更新它而且仍在進行中,Tx_B 等待 Tx_A 的終止。在更新目標行提交的事務以後,繼續當前事務的更新操做。 若是當前事務處於 READ COMMITTED 級別,則將更新目標行; 不然 REPEATABLE READ 或 SERIALIZABLE,當前事務當即停止以防止丟失更新。
PostgreSQL 的併發控制機制還依賴於如下的維護流程:
首先討論下 txid 環繞式處理的問題,假設 txid 100 的事務插入了某個 Tuple_1
,則該 Tuple 對應的 t_xmin 值爲 100;然後服務器又運行了許久,Tuple_1
期間並未被改變。直到 txid 爲 2^31 + 101
時,對於該事務而言,其執行 SELECT 命令時,是沒法看到 Tuple_1
的 ,由於 txid 爲 100 的事務相對於其是發生在將來的,由其建立的 Tuple 天然也就是不可見的。
爲了解決這個問題,PostgreSQL 引入了所謂的 frozen txid(被凍結的 txid),而且設置了 FREEZE 進程來具體處理該問題。前文說起到 txid 2 是保留值,專門表徵那些被凍結的 Tuple,這些 Tuple 永遠是非激活的、可見的。FREEZE 進程一樣由 Vacuum 進程統一調用,它會掃描全部的表文件,將那些與當前 txid 差值超過 vacuum_freeze_min_age 定義的 Tuple 的 t_xmin 域設置爲 2。在 9.4 版本以後,則是將 t_infomask 域中的 XMIN_FROZEN 位設置來表徵該 Tuple 爲凍結狀態。