非物件鎖#
要鎖住不被視為關聯的資源,PostgreSQL 使用 object 類型的重量鎖。幾乎任何存放在系統目錄中的東西都能被鎖住:表空間、訂閱、綱要、角色、政策、列舉型別等等。
被鎖資源由三個值定義:
| 欄位 | 意義 |
|---|---|
database | 含被鎖物件的資料庫 oid(若該物件為整個叢集共用則為 0) |
classid | pg_class 中列出的 oid,對應定義該資源類型的系統目錄表格名稱 |
objid | 由 classid 所指系統目錄表格中列出的 oid |
實驗:在交易中建立表格後查詢非關聯鎖:
=> BEGIN;
=> CREATE TABLE example(n integer);
=> SELECT database, classid, objid, mode, granted
FROM pg_locks
WHERE locktype = 'object' AND pid = pg_backend_pid() \gx
−[ RECORD 1 ]−−−−−−−−−−−−−−
database | 16391 -- internals
classid | 2615 -- pg_namespace
objid | 2200 -- public
mode | AccessShareLock
granted | tPostgreSQL 鎖住了
public綱要,確保交易還在進行時沒人能刪掉它。同理,刪除物件需要對該物件本身以及它所依賴的所有資源取得排他鎖。
關聯延伸鎖#
隨著關聯中的元組增加,PostgreSQL 會盡可能把新元組插入既有頁面的可用空間。但顯然到了某個時點它必須新增頁面,也就是延伸關聯。就實體佈局而言,新頁面被加到對應檔案的末尾(這又可能導致建立新檔案)。
為了讓新頁面一次只由一個行程新增,這項操作由 extend 類型的特殊重量鎖保護。索引清理也使用這種鎖,以禁止在索引掃描期間新增頁面。
關聯延伸鎖的行為與前面看到的有所不同:
- 延伸一建立完成就立刻釋放,不等交易結束
- 不會造成死結,因此不被納入等待圖
然而若延伸關聯的程序耗時超過
deadlock_timeout,死結檢查仍會執行。這不是典型情況,但當大量行程並行執行多筆插入時就可能發生。此時檢查可能被呼叫多次,實質上癱瘓系統的正常運作。為降低這個風險,heap 檔案一次延伸數個頁面(與等待該鎖的行程數成正比,但每次操作不超過 512 個頁面)。例外是 B-tree 索引檔案,它們一次只延伸一頁。
頁面鎖#
page 類型的頁面層級重量鎖只被 GIN 索引使用,而且只在一種情況下。
GIN 索引能加速在複合值中搜尋元素,例如文字文件中的單字。可以粗略地說它是「存放個別單字而非整份文件」的 B-tree。新增文件時,索引必須被徹底更新以納入該文件中出現的每個單字。
為提升效能,GIN 索引允許延後插入(deferred insertion),由
fastupdate儲存參數控制(預設 on):新單字先被快速加進一份無序的待處理清單(pending list),過一陣子再把累積的項目全部搬進主索引結構。由於不同文件很可能含有重複單字,這種做法相當划算。
為避免多個行程並行搬移單字,索引的中繼頁面會被以排他模式鎖住,直到所有單字從待處理清單搬入主索引為止。這個鎖不會干擾索引的一般使用。
與關聯延伸鎖一樣,頁面鎖在任務完成時立即釋放,不等交易結束,因此永遠不會造成死結。
諮詢鎖#
與其他重量鎖(如關聯鎖)不同,諮詢鎖(advisory lock)永遠不會自動取得:它們由應用程式開發者控制。當應用程式需要為某個特定目的訂製鎖定邏輯時,這類鎖很方便。
假設我們需要鎖住一個不對應任何資料庫物件的資源。此時該資源需要被指派一個數字 ID;若資源有唯一名稱,最簡單的做法是為該名稱產生雜湊碼:
=> SELECT hashtext('resource1');
hashtext
−−−−−−−−−−−
991601810
(1 row)PostgreSQL 提供一整類管理諮詢鎖的函式,名稱以 pg_advisory 開頭,並可包含以下暗示用途的字詞:
| 字詞 | 意義 |
|---|---|
lock | 取得鎖 |
try | 若能不等待就取得鎖 |
unlock | 釋放鎖 |
share | 使用共享鎖定模式(預設為排他模式) |
xact | 取得並持有鎖直到交易結束(預設持有到 session 結束) |
=> SELECT pg_advisory_lock(hashtext('resource1'));
=> SELECT locktype, objid, mode, granted
FROM pg_locks WHERE locktype = 'advisory' AND pid = pg_backend_pid();
locktype | objid | mode | granted
−−−−−−−−−−+−−−−−−−−−−−+−−−−−−−−−−−−−−−+−−−−−−−−−
advisory | 991601810 | ExclusiveLock | t
(1 row)取得的鎖在交易完成後依然持有(若未使用 xact 變體)。資源上的操作結束後,必須明確釋放:
=> SELECT pg_advisory_unlock(hashtext('resource1'));謂詞鎖#
謂詞鎖(predicate lock)這個術語早在最初嘗試以鎖實作完整隔離時就出現了。當時面臨的問題是:即使鎖住所有待讀取與待更新的列,仍無法保證完整隔離——若新的、滿足過濾條件的列被插入表格,它們就會成為幻影。
因此有人建議鎖住**條件(謂詞)**而非列。若查詢帶 a > 10 謂詞,鎖住這個謂詞就不允許新增滿足此條件的列,幻讀便可避免。
麻煩在於:若出現帶不同謂詞的查詢(例如
a < 20),你必須判斷這兩個謂詞是否重疊。理論上這個問題在演算法上不可解;實務上只能對非常簡單的謂詞類別求解。
PostgreSQL 的 Serializable 隔離層級以不同方式實作:它使用可序列化快照隔離(Serializable Snapshot Isolation,SSI)協定。
「謂詞鎖」這個術語留了下來,但意義已徹底改變——事實上這些「鎖」什麼都沒鎖住:它們用來追蹤不同交易之間的資料依賴。
為什麼需要它#
已證明:Repeatable Read 層級的快照隔離除了寫入偏斜與唯讀交易異常之外不允許任何異常。這兩種異常會在資料依賴圖中形成特定樣式,而這些樣式能以相對低的成本被發現。
問題是我們必須區分兩類依賴:
- WR 依賴:第一筆交易讀取某列,該列稍後被第二筆交易更新
- RW 依賴:第一筆交易修改某列,該列稍後被第二筆交易讀取
這種追蹤在 Serializable 層級自動開啟——這正是「所有交易(至少所有相互關聯的交易)都必須使用該層級」如此重要的原因。若有任何交易在不同層級運行,它就不會設定(或檢查)謂詞鎖,Serializable 層級會降級為 Repeatable Read。
再次強調:儘管名為鎖,謂詞鎖不鎖任何東西。真正發生的是:交易即將提交時會被檢查是否有「危險」依賴,若 PostgreSQL 懷疑有異常,該交易就會被中止。
粒度取決於掃描方式#
循序掃描 → 謂詞鎖加在整張表格上(即使部分列不滿足過濾條件):
=> BEGIN ISOLATION LEVEL SERIALIZABLE;
=> EXPLAIN (analyze, ...) SELECT * FROM pred WHERE n > 100;
Seq Scan on pred (actual rows=9900 loops=1)
=> SELECT relation::regclass, locktype, page, tuple
FROM pg_locks WHERE mode = 'SIReadLock' AND pid = 34753;
relation | locktype | page | tuple
−−−−−−−−−−+−−−−−−−−−−+−−−−−−+−−−−−−−
pred | relation | |
(1 row)謂詞鎖雖有自己的基礎設施,
pg_locks視圖仍把它們與重量鎖一起顯示。所有謂詞鎖一律以SIRead(Serializable Isolation Read)模式取得。謂詞鎖可能比交易存活得更久,因為它們用來追蹤交易之間的依賴。但無論如何,它們是自動管理的。
索引掃描 → 情況大幅改善。對 B-tree 索引,只需在讀取的 heap 元組與掃描過的索引葉頁面上設謂詞鎖,這樣就「鎖住」了整個被讀取的範圍,而非僅是確切的值:
=> EXPLAIN (analyze, ...) SELECT * FROM pred WHERE n BETWEEN 1000 AND 1001;
Index Scan using pred_n_idx on pred (actual rows=2 loops=1)
relation | locktype | page | tuple
−−−−−−−−−−−−+−−−−−−−−−−+−−−−−−+−−−−−−−
pred | tuple | 4 | 96
pred | tuple | 4 | 97
pred_n_idx | page | 28 |
(3 rows)對應已掃描元組的葉頁面數量可能改變——例如新列插入表格時索引頁面可能分裂。PostgreSQL 會把這點納入考量,也鎖住新出現的頁面。
鎖升級#
每個被讀取的元組都被分別上鎖,而這類元組可能相當多。謂詞鎖使用自己在伺服器啟動時配置的池,總數受限於 max_pred_locks_per_transaction(預設 64)乘以 max_connections(預設 100)。
儘管參數名稱如此,謂詞鎖並非按個別交易計數。
這裡遇到與列層級鎖相同的問題,但解法不同:套用鎖升級。
- 與同一頁面相關的元組鎖數量一旦超過
max_pred_locks_per_page(預設 2),就被單一頁面層級鎖取代 - 頁面層級鎖的升級遵循同樣原則:某關聯的這類鎖數量超過
max_pred_locks_per_relation(預設 −2)就被單一關聯層級鎖取代
若
max_pred_locks_per_relation為負值,門檻計算方式為max_pred_locks_per_transaction除以該參數的絕對值——因此預設門檻是 32。
支援的索引類型與最佳實務#
謂詞鎖支援:
- B-tree
- 雜湊索引、GiST、GIN(PostgreSQL 11 起)
若執行索引掃描、但該索引不支援謂詞鎖,整個索引都會被鎖住。可以預期:無正當理由而被中止的交易數量也會隨之增加。
為了讓 Serializable 層級更有效率地運作,明確以
READ ONLY子句宣告唯讀交易是合理的做法。若鎖管理器看出某筆唯讀交易不會與其他交易衝突,它就能釋放已設定的謂詞鎖並不再取得新的。而若該交易同時被宣告為
DEFERRABLE,唯讀交易異常也能一併避免。