1. 程式人生 > 其它 >PostgreSQL 鎖 之 關係級鎖

PostgreSQL 鎖 之 關係級鎖

 

1.關於鎖的基本資訊

PostgreSQL 有各種各樣的技術來鎖定某些東西(或者至少是這樣稱呼的)。因此,我將首先用最籠統的術語解釋為什麼需要鎖,可用的鎖型別以及它們之間的區別。然後我們將弄清楚 PostgreSQL 中使用了哪些種類的鎖,然後我們將開始詳細討論不同種類的鎖。

鎖用於對共享資源的併發訪問進行排序。

併發訪問是指多個程序的同時訪問。這些程序本身既可以並行執行(如果硬體允許),也可以以分時模式順序執行,沒有區別。

如果沒有併發,就不需要鎖(例如:共享緩衝區快取需要鎖,而本地快取則不需要)。

在訪問資源之前,程序必須獲取與該資源關聯的鎖。所以這是一個一定的規則問題:一切正常,而程序符合訪問共享資源的既定規則。如果 DBMS 控制鎖,它會自行維護順序;但是如果一個應用程式設定了鎖,責任就落在了它身上。

在低層次上,鎖是共享記憶體中的一個區域,帶有一些指示是否釋放或獲取鎖(可能還有一些附加資訊:程序號、獲取時間等)。

 

請注意,共享記憶體中的這個區域本身是一個允許併發訪問的資源。如果我們下降到較低的級別,我們將看到為了規範訪問,作業系統提供了專門的同步原語(例如訊號量或互斥體)。它們的目的是確保訪問共享資源的程式碼僅在一個程序中使用。在最低級別,這些原語是通過原子處理器指令(例如test-and-set或compare-and-swap)實現的。

 

當某個程序不再需要某個資源時,後者會釋放鎖,以便其他程序可以使用它。

當然,有時無法獲得鎖:資源可能已被其他人使用。然後該程序要麼處於等待佇列中(如果鎖技術允許這樣做),要麼在一段時間後重復嘗試獲取鎖。無論如何,該程序必須在等待資源空閒時處於空閒狀態。

 

有時,可以使用其他非阻塞策略。例如:在某些情況下,多版本併發控制允許多個程序同時處理不同版本的資料,而不會相互阻塞。

 

一般來說,要保護的資源是指我們可以明確識別並將鎖地址與其相關聯的任何東西。

例如:一個 DBMS 物件,如資料頁(由檔案中的檔名和位置標識)、表(系統目錄中的 OID)或錶行(其中的頁和偏移量)可以是資源。諸如雜湊表、緩衝區等(由先前分配的編號標識)之類的記憶體結構也可以是資源。有時使用沒有物理意義的抽象資源(僅由唯一編號標識)甚至更方便。

 

影響加鎖效率的因素很多,我們重點介紹兩個。

  • 當資源按層次結構組織時,粒度至關重要。

例如:一個表由包含錶行的頁組成。所有這些物件都可以是資源。如果程序只對幾行感興趣,但在表級別獲得了鎖,則其他程序將無法同時處理不同的行。因此,粒度越高,越有利於並行化。

但這會導致鎖的數量增加(資訊需要儲存在記憶體中)。在這種情況下,可以應用鎖的升級:當低階高粒度鎖的數量超過一定限制時,將它們替換為一個更高級別的鎖。

  • 可以通過多種方式獲取鎖。

模式的名稱可以是任何名稱;真正重要的是它們相互相容的矩陣。與任何模式(包括自身)不相容的模式通常稱為獨佔。如果模式相容,多個程序可以同時獲取鎖;像這樣的模式稱為shared。一般來說,可以區分的相互相容的模式越多,併發的機會就越多。

 

根據持續時間,鎖可以分為長鎖和短鎖。

  • 長鎖的獲取時間可能很長(通常直到事務結束),並且通常與表(關係)和行等資源相關。作為一項規則,PostgreSQL 會自動控制這些鎖,但是使用者對這個過程有一定的控制權。

大量模式是長鎖的典型特徵,以啟用盡可能多的同時資料操作。通常有一個廣泛的基礎設施(例如:支援等待佇列和檢測死鎖)和監控工具可用於此類鎖,因為所有這些便利功能的維護成本無論如何都比受保護資料的操作成本低得多。

  • 短鎖的獲取時間很短(從幾條處理器指令到幾分之一秒),通常與共享記憶體中的資料結構有關。PostgreSQL 以全自動方式控制這些鎖——你只需要知道它們的存在。

最少的模式(獨佔和共享)和簡單的基礎設施是短鎖的典型特徵。有時甚至可能沒有監控工具。

 

PostgreSQL 使用不同型別的鎖。

物件級鎖屬於長的、“重量級”的鎖。關係和其他物件是這裡的資源。如果您在本文中遇到“鎖”或“鎖定”這個詞而沒有明確說明,那麼它的意思就是這種“普通”鎖定。

在長鎖中,行級鎖是比較突出的。它們的實現方式與其他長鎖不同,因為它們的數量可能很大(想象一下在一個事務中更新一百萬行)。我們將在下一篇文章中討論這些鎖。

 

 

物件級鎖

我們從物件級鎖開始。這裡的物件主要是指關係,即表、索引、序列和物化檢視,但也包括其他一些實體。這些鎖通常用於保護物件免受同時更改或在更改物件時使用,但也用於其他需要。

措辭很模糊,不是嗎?正是如此,因為該組中的鎖用於各種目的。唯一將它們結合在一起的是它們的組織方式。

 

 

組織

物件鎖儲存在伺服器的共享記憶體中。它們的數量受兩個引數值的乘積限制:max_locks_per_transaction × max_connections。

鎖池是所有事務的一個,也就是說,一個事務可以獲得比max_locks_per_transaction更多的鎖:唯一重要的是系統中的鎖總數不超過指定的數量。池是在啟動時建立的,因此要更改上述兩個引數中的任何一個,都需要重新啟動伺服器。

pg_locks您可以在檢視中看到所有鎖。

如果資源已經以不相容的模式鎖定,則嘗試獲取鎖的事務將排隊等待直到鎖被釋放。等待事務不消耗處理器資源:後端程序涉及“入睡”,並在資源空閒時由作業系統喚醒。

當要繼續工作時,一個事務需要另一個事務正在使用的資源,而第二個事務需要第一個事務正在使用的資源,就會發生死鎖。通常,可能會出現兩個以上事務的死鎖。在這種情況下,等待將無限持續,因此,PostgreSQL 會自動檢測到這種情況並中止其中一個事務以使其他事務繼續。(我們將在下一篇文章中詳細討論死鎖。)

 

 

物件型別

以下是我們將在本文和下一篇文章中處理的鎖型別(或者,如果您喜歡,物件型別)的列表。名稱是根據檢視的locktype列提供的pg_locks。

  • 關係

 鎖定關係。

  • transactionid和virtualxid

鎖定事務 ID(實際或虛擬)。每個事務本身在自己的 ID 上都持有一個排他鎖,因此,當我們需要等待另一個事務完成時,使用這種鎖很方便。

  • 元組

鎖定一個元組。在某些情況下用於優先考慮等待同一行上的鎖定的多個事務。

  • 擴充套件

在將頁面新增到某種關係的檔案時使用。

  • 目的

鎖定不同於關係的物件(資料庫、模式、訂閱等)。

頁面上的鎖定 — 不經常使用且僅由某些型別的索引使用。

  • 諮詢

諮詢鎖——使用者手動獲取它們。

 

2.關係級鎖

模式

 

除非關係級鎖是鎖中最重要的,否則它的模式肯定最多。為它定義了多達 8 種不同的模式。需要這麼多才能同時執行與一張表相關的最大可能數量的命令。

將這些模式記入您的記憶或試圖瞭解它們的名稱是沒有意義的。真正重要的是手邊有顯示哪些鎖相互衝突的矩陣。為方便起見,此處提供了它以及需要相應級別鎖定的命令示例:

 

  • 前 4 種模式允許併發更改表資料,而後 4 種不允許。

  • 第一種模式(Access Share)是最弱的,它與除最後一種(Access Exclusive)之外的任何其他模式都相容。最後一種模式是獨佔的,它與任何其他模式都不相容。

  • ALTER TABLE 命令有多種風格,不同的風格需要不同級別的鎖。因此,此命令出現在矩陣的不同行中,並用星號標記。

 

 

演示

讓我們考慮一個例子。如果我們執行 CREATE INDEX 命令會發生什麼?

我們從文件中瞭解到,此命令獲取共享鎖。從矩陣中,我們瞭解到該命令與自身相容(即可以同時建立多個索引)以及與讀取命令相容。因此,SELECT 命令將繼續工作,而 UPDATE、DELETE 和 INSERT 將被阻止。

反之亦然:更改表資料的未完成事務將阻止 CREATE INDEX 命令的執行。正是由於這個原因,該命令的 CREATE INDEX CONCURRENTLY 風格才可用。它的執行需要更長的時間(甚至可能因錯誤而失敗),但它允許併發資料更新。

您可以在實踐中確保這一點。對於實驗,我們將使用“銀行”賬戶表,這是我們從第一個系列開始就熟悉的,其中將儲存帳號和金額。

 

  • => CREATE TABLE pgccc_accounts( 

  •   acc_no integer PRIMARY KEY, 

  •   amount numeric 

  • ); 

  • => INSERT INTO pgccc.accounts 

  •   VALUES (1,1000.00), (2,2000.00), (3,3000.00); 

  • }

 

在第二個會話中,我們將開始一個事務。我們將需要後端程序的程序 ID。

 

  • |  => SELECT pg_backend_pid(); 

  •  

  • |   pg_backend_pid 

  • |  ---------------- 

  • |             4746 

  • |  (1 row)

 

剛剛開始的事務持有什麼鎖?調查pg_locks:

 

  • => SELECT locktype, relation::REGCLASS,  

  • virtualxid AS virtxid,  

  • transactionid AS xid, mode, granted 

  • FROM pg_locks  

  • WHERE pid = 4746; 

  •  

  •   locktype  | relation | virtxid | xid |     mode      | granted 

  • ------------+----------+---------+-----+---------------+--------- 

  •  virtualxid |          | 5/15    |     | ExclusiveLock | t 

  • (1 row)

 

正如我之前所說,一個事務總是在它自己的 ID 上持有一個獨佔鎖,在這種情況下它是虛擬的。此程序沒有其他鎖。

現在讓我們更新一個表格行。情況將如何變化?

 

正在更改的表和 UPDATE 命令使用的索引(為主鍵建立)上的鎖出現了。獲得的兩個鎖都是行獨佔的。此外,還添加了對實際事務 ID 的排他鎖(該 ID 在事務開始更改資料時出現)。

現在我們將嘗試在另一個會話中在表上建立索引。

 

 

 

該命令“hangs”等待資源空閒。它特別嘗試獲取什麼鎖?讓我們弄清楚:

 

現在很清楚,事務試圖獲取表上的共享鎖,但不能(granted = f)。

為了找到加鎖程序的程序ID(pid),一般是幾個pid,使用9.6版本出現的功能比較方便(之前必須仔細檢視所有內容才能得出結論) ) pg_locks:

然後,為了瞭解情況,我們可以獲取有關找到的 pid 所屬會話的資訊:

 

事務完成後,釋放鎖並建立索引。

 

 

3.加入佇列

為了更好地理解不相容鎖的發生意味著什麼,讓我們看看如果我們在系統執行期間執行 VACUUM FULL 命令會發生什麼。

讓 SELECT 成為在上表中執行的第一個命令。它獲得了最弱級別的訪問共享鎖。為了控制釋放鎖的時間,我們在事務內部執行這個命令——直到事務完成,鎖才會被釋放。實際上,幾個命令可以讀取(和更新)一個表,並且執行一些查詢可能需要很長時間。

  • => BEGIN; 

  • => SELECT * FROM pgccc_accounts; 

  •  

  •  acc_no | amount   

  • --------+--------- 

  •       2 | 2000.00 

  •       3 | 3000.00 

  •       1 | 1100.00 

  • (3 rows)

 

  • => SELECT locktype, mode, granted,  

  • pid, pg_blocking_pids(pid)  

  • AS wait_for 

  • FROM pg_locks  

  • WHERE relation = 'pgccc_accounts'::regclass;

 

 

 

然後管理員執行 VACUUM FULL 命令,該命令需要一個具有 Access Exclusive 級別的鎖,並且與所有內容都不一致,即使是 Access Share。(LOCK TABLE 命令需要相同的鎖。)並且事務已排隊。

但是應用程式繼續發出查詢,因此係統中也會出現 SELECT 命令。假設它可以在 VACUUM FULL 等待時“通過”,但不是——它確實在等待 VACUUM FULL。

 

當使用 SELECT 命令的第一個事務完成並釋放鎖時,VACUUM FULL 命令(我們通過 LOCK TABLE 命令模擬)啟動。

  • => COMMIT; 

  •  

  • COMMIT 

  •  

  • LOCK TABLE

 

 

並且只有在 VACUUM FULL 完成並釋放鎖之後,所有排隊的命令(本例中為 SELECT)才能獲得適當的鎖(訪問共享)並執行。

  •  => COMMIT; 

  •  

  •  COMMIT 

  •  

  •   acc_no | amount   

  •  --------+--------- 

  •   2 | 2000.00 

  •   3 | 3000.00 

  •   1 | 1100.00 

  •   (3 rows)

 

因此,不正確執行的命令可能會在比執行命令本身更長的時間間隔內使系統工作癱瘓。


 

4.監控工具
毫無疑問,正確的工作需要鎖,但它們會導致不必要的等待。可以跟蹤這些等待以找出其根本原因並儘可能消除它(例如:通過更改應用程式的演算法)。

我們已經熟悉了一種方法:當發生長鎖時,我們可以查詢pg_locks檢視,檢視鎖定和鎖定事務(使用pg_blocking_pids函式)並使用pg_stat_activity.

另一種方法是開啟log_lock_waits引數。在這種情況下,如果事務等待的時間超過deadlock_timeout,資訊將進入伺服器訊息日誌(雖然該引數用於死鎖,但此處指的是正常等待)。

我們試試吧。

  • => ALTER SYSTEM  

  • SET log_lock_waits = on; 

  • => SELECT pg_reload_conf();

 

deadlock_timeout引數的預設值為一秒:

  • => SHOW deadlock_timeout; 

  •  

  •  deadlock_timeout 

  • ------------------ 

  •  1s 

  • (1 row)

 

讓我們重現一次鎖。

  • => BEGIN; 

  • => UPDATE pgccc_accounts  

  • SET amount = amount - 100.00  

  • WHERE acc_no = 1; 

  •  

  • UPDATE 

  •  

  • |  => BEGIN; 

  • |  => UPDATE pgccc_accounts  

  • SET amount = amount + 100.00  

  • WHERE acc_no = 1;

 

 

第二個 UPDATE 命令正在等待鎖定。讓我們稍等片刻,完成第一筆交易。

  • => SELECT pg_sleep(1); 

  • => COMMIT; 

  •  

  • COMMIT

 

現在可以完成第二筆交易。

  • |  UPDATE 

  •  

  • |  => COMMIT; 

  •  

  • |  COMMIT

 

並記錄了所有重要資訊:

  • postgres$ tail -n 7 /var/log/postgresql/postgresql-11-main.log 

  •  

  • 2022-03-31 15:26:30.827 MSK [5898] student@test LOG:  process 

  • 5898 still waiting for ShareLock on transaction 529427 after 

  • 1000.186 ms 

  •  

  • 2022-03-31 15:26:30.827 MSK [5898] student@test DETAIL:  

  • Process holding the lock: 5862. Wait queue: 5898. 

  •  

  • 2022-03-31 15:26:30.827 MSK [5898] student@test CONTEXT:  

  • while updating tuple (0,4) in relation "pgccc_accounts" 

  •  

  • 2022-03-31 15:26:30.827 MSK [5898] student@test STATEMENT:  

  • UPDATE accounts SET amount = amount + 100.00 

  • WHERE acc_no = 1; 

  •  

  • 2022-03-31 15:26:30.836 MSK [5898] student@test LOG:  

  • process 5898 acquired ShareLock on transaction 529427 

  • after 1009.536 ms 

  •  

  • 2022-03-31 15:26:30.836 MSK [5898] student@test CONTEXT:  

  • while updating tuple (0,4) in relation "pgccc_accounts" 

  •  

  • 2022-03-31 15:26:30.836 MSK [5898] student@test STATEMENT:  UPDATE accounts SET amount = amount + 100.00 WHERE acc_no = 1;

  •  

 

原地址連結:

https://postgrespro.com/blog/pgsql/5967999

 

 

PG考試諮詢