這篇博文討論了 PostgreSQL 中多版本併發控制的基礎知識。然後介紹快照以及它們如何控制元組的可見性。還討論了與表掃描 API 的整合。
與許多關聯式資料庫管理系統一樣,PostgreSQL 使用多版本併發控制(MVCC)來支援並行執行的事務,並協調對圖元的並行訪問。
- 快照用於確定圖元的哪個版本在哪個事務中可見。
- 每個修改資料的事務都有一個事務 ID(txid)。
- 圖元與兩個屬性(xmin、xmax)一起儲存,這兩個屬性決定了圖元在哪個快照(以及哪個事務)中可見。
本博文將討論快照的一些實現細節。
元組可見性
本文使用下表來說明快照在 PostgreSQL 中的工作原理。
CREATE TABLE temperature ( |
讓我們在此表中插入第一條記錄:這是透過建立一個新事務、獲取當前事務 ID(如果可用)、插入新元組、再次獲取事務 ID 並提交事務來完成的。
BEGIN; |
從這個示例中可以看出,PostgreSQL 只在資料被修改後立即為事務分配一個事務 ID,這一點很重要。這樣做是為了防止不必要的工作,並防止事務 ID 耗盡。即使事務 ID 是一個 32 位整數,該值也會在某個時刻耗盡。PostgreSQL 可以處理這種溢位(即凍結元組,以正確處理事務 ID 包絡)。
系統屬性 xmin 和 xmax 決定了能看到某個元組的第一個事務和最後一個事務。此外,ctid 屬性顯示元組在相應頁面上的編號。
當 SELECT 語句中明確提到這些屬性時,就會顯示它們的值:
SELECT xmin, xmax, ctid, * FROM temperature; |
輸出結果表示:
- 所有事務 ID >= 5062286 的事務都能看到這個元組。
- 刪除元組時,xmax 值將填入能看到此元組的最大事務 ID。
- ctid為 0,1 表示該圖元是第 0 頁的第一個圖元。
現在我們刪除該圖元:
EGIN; |
但是,當執行 SELECT 語句時,什麼也不會返回,而是返回一個帶有 xmin 和 xmax 值的元組。
SELECT xmin, xmax, ctid, * FROM temperature; |
產生這種行為的原因是內部掃描器。如果一個元組在當前事務快照中不可見。要從元組中獲取這些值,我們需要使用更底層的工具,而不是簡單的 SELECT。
PostgreSQL 的 pageinspect 擴充套件允許我們獲取儲存在頁面上的所有元組,並解碼內部標誌和屬性。需要載入該擴充套件,然後就可以檢查關係的頁面了。
-- Load the extension |
輸出結果顯示,第 0 頁的第一個元組(上述輸出中的ctid 為 (0,1))的 t_max 值為 5062291,與刪除該元組的事務 ID 相同。因此,每個事務 ID 大於 5062291 的事務都看不到這個元組。
快照
PostgreSQL 掃描表時,必須指定快照。請參見 table_beginscan 函式,該函式將快照資料作為第二個引數:
static inline TableScanDesc table_beginscan(Relation rel, |
內部資料結構
通常,事務快照 transaction snapshot被用作該函式的引數。結構 SnapshotData 包含快照的所有資訊。在本博文中,我們將重點討論以下屬性:
typedef struct SnapshotData |
欄位 xmin 定義了系統中最老的活動事務。所有 txid 小於此值的事務都已提交。xmax 包含快照已知的最新事務 ID。當前快照不可見 txid > xmax 的所有圖元。
為什麼需要 xip 和 xcnt 欄位?
對於 xmin 和 xmax 之間的事務 ID,需要確定建立快照時事務是已提交還是正在進行中。
DBMS 處理多個使用者的查詢。他們可以隨時啟動事務。這些事務的開始時間和提交時間沒有順序。這意味著在建立快照時,可能有事務 ID 大於 xmin 的事務已經提交。然而,在 [xmin, xmax] 範圍內的其他一些事務仍未提交。由於需要正確處理已提交和未提交事務的資料,因此定義了一個長度為 xcnt 的事務 ID 陣列 xip。它包含所有大於 xmin 且小於 xmax 的事務,這些事務在快照拍攝時正在進行中。
示例
為了說明這種行為,讓我們用三個事務做一個實際例子。
事務1
BEGIN; |
第一個事務在表temperature 中插入新資料,但保持未提交狀態。該事務的事務 ID 是 5062310。
事務2
BEGIN; |
此外,第二個事務向同一個表插入資料,但也保持未提交狀態。該事務的 ID 是 5062311。
事務3
SELECT * FROM pg_current_snapshot(); |
第三個事務使用函式 pg_current_snapshot 獲取當前快照。函式的輸出意味著,ID 小於 5062310 的事務的所有更改都是可見的。等於或大於事務 ID 5062310 的更改不可見,此時不存在未提交的事務。
那麼,仍未提交的事務 5062310 和 5062311 又是怎麼回事呢?由於在這個演示系統中到目前為止還沒有提交更多事務,所以 PostgreSQL 沒有更改當前事務 ID。不過,這是可以改的:
SELECT * FROM pg_current_xact_id_if_assigned(); |
與函式 pg_current_xact_id_if_assigned 不同,函式 pg_current_xact_id 強制為當前事務指定一個事務 ID。在我們的例子中,它是 5062312。使用該事務 ID 也會導致快照的更新。
第一個值保持不變。不過,ID 小於 5062310 的事務修改的所有圖元在當前快照中都是可見的。但是,上限 (xmax) 發生了變化。現在,所有等於或大於 5062313 的更改在當前快照中都不可見。因為我們的事務 ID 是 5062312,所以這些更改不可見是合理的。那麼新的 5062310,5062311 部分呢?這是快照的 xip 部分,表示在拍攝快照時,5062310 和 5062311 這兩個事務尚未提交。因此,這些更改在當前快照中也不可見。一旦這些事務中的一個提交,並且我們拍攝了新快照,事務 ID 就會從 zip 中移除,因此這些更改在當前快照中就會可見。
匯出快照
PostgreSQL 另一個有趣的功能是匯出快照並將其載入到其他會話中。呼叫 pg_export_snapshot 函式可以匯出快照。該函式會返回快照的 ID,並在資料目錄的 pg_snapshots 資料夾中建立一個相應的檔案。
BEGIN; |
該檔案包含的資訊與上面討論過的 pg_current_snapshot 所返回的資訊相同。此外,它還包含有關所用隔離級別或所用資料庫 ID 的更多資訊。
$ cat ~/postgresql-sandbox/data/REL_15_1_DEBUG/pg_snapshots/0000000C-000005F6-1 |
可以透過呼叫 SET TRANSACTION SNAPSHOT 0000000C-000005F6-1 將匯出的快照載入到另一個事務中,從而使用與建立快照的事務相同的快照執行。
快照和事務隔離級別
- 根據隔離級別的不同,快照會在事務啟動時(可重複讀取)或為事務中的每條語句(已提交讀取)建立。
- 當為事務中的每條語句建立新快照時,其他事務中已提交的資料就會在當前事務中可見。
- 如果只為整個事務建立一個快照,xmax 值就會保持不變,ID 更高的事務中的新資料就不會可見,讀取也是可重複的。