非物件鎖#

要鎖住不被視為關聯的資源,PostgreSQL 使用 object 類型的重量鎖。幾乎任何存放在系統目錄中的東西都能被鎖住:表空間、訂閱、綱要、角色、政策、列舉型別等等。

被鎖資源由三個值定義:

欄位意義
database含被鎖物件的資料庫 oid(若該物件為整個叢集共用則為 0)
classidpg_class 中列出的 oid,對應定義該資源類型的系統目錄表格名稱
objidclassid 所指系統目錄表格中列出的 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   | t

PostgreSQL 鎖住了 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唯讀交易異常也能一併避免