PostgreSQL 鎖機制淺析


鎖機制在 PostgreSQL 里非常重要 (對於其他現代的 RDBMS 也是如此)。對於數據庫應用程序開發者(特別是那些涉及到高並發代碼的程序員),需要對鎖非常熟悉。對於某些問題,鎖需要被重點關注與檢查。大部分情況,這些問題跟死鎖或者數據不一致有關系,基本上都是由於對 Postgres 的鎖機制不太了解導致的。雖然鎖機制在 Postgres 內部很重要,但是文檔缺非常缺乏,有時甚至還是錯誤的,與文檔所指出的結果不一致。我會告訴你精通 Postgres 的鎖機制需要知道的一切,要知道對鎖了解的越多,解決與鎖相關的問題就會越快。

文檔里都說了些什么?

Postgres 有 3 種鎖機制:表級鎖,行級鎖和建議性鎖。表級和行級的鎖可以是顯式的也可以是隱式的。建議性鎖一般是顯式的。顯式的鎖由顯式的用戶請求(通過特殊的查詢)獲取,隱式的鎖是通過標准的 SQL 命令來獲取。

除了表級和行級的鎖,還有頁級共享/排除鎖,用於控制對共享緩存池里表頁的訪問。在一行數據被讀取或者更新后,這些鎖會立即被釋放。應用程序開發者通常不需要關注頁級的鎖。

鎖機制會不時的變動,所以我們這里只針對 Postgres 9.x 的版本。9.1 和 9.2 基本上是差不多的,9.3 和 9.4 跟它們有些區別,主要涉及行級鎖。

 
 

表級鎖

大多數的表級鎖是由內置的 SQL 命令獲得的,但他們也可以通過鎖命令來明確獲取。可使用的表級鎖包括:

  • 訪問共享(ACCESS SHARE) - SELECT 命令可在查詢中引用的表上獲得該鎖。一般規則是所有的查詢中只有讀表才獲取此鎖。

  • 行共享(ROW SHARE) - SELECT FOR UPDATE 和 SELECT FOR SHARE 命令可在目標表上獲得該鎖(以及查詢中所有引用的表的訪問共享鎖)。

  • 行獨占(ROW EXCLUSIVE) - UPDATE、INSERT 和 DELETE 命令在目標表上獲得該鎖(以及查詢中所有引用的表的訪問共享鎖)。 一般規則是所有修改表的查詢獲得該鎖。

  • 共享更新獨占(SHARE UPDATE EXCLUSIVE) - VACUUM(不含FULL),ANALYZE,CREATE INDEX CONCURRENTLY,和一些 ALTER TABLE 的命令獲得該鎖。

  • 共享(SHARE) - CREATE INDEX 命令在查詢中引用的表上獲得該鎖。

  • 共享行獨占(SHARE ROW EXCLUSIVE) - 不被任何命令隱式獲取。

  • 排他(EXCLUSIVE) - 這個鎖模式在事務獲得此鎖時只允許讀取操作並行。它不能由任何命令隱式獲取。

  • 訪問獨占(ACCESS EXCLUSIVE) - ALTER TABLE,DROP TABLE,TRUNCATE,REINDEX,CLUSTER 和 VACUUM FULL 命令在查詢中引用的表上獲得該鎖。此鎖模式是 LOCK 命令的默認模式。

重要的是要知道,所有這些鎖都是表級鎖,即使它們名稱里有行(ROW)字。

每個鎖模式的最重要的信息是與彼此沖突的模式列表。在同一時間同一個表中,2 個事務不能同時保持相沖突的鎖模式。事務永遠不會與自身發生沖突。 非沖突的鎖可以支持多事務並發。同樣重要的是要知道有的模式和自身沖突。一些鎖模式在獲得后會持續到事務結束。但如果鎖是在建立一個保存點后獲得,保存點回滾后鎖會被立刻釋放。 下面的表格展示了哪些模式是互相沖突的:

 

行級鎖

在 Postgres 9.1 和 9.2 有兩種行級鎖模式,但在 Postgres 9.3 和 9.4 有四種行級鎖模式。

Postgres 不會記住修改的行在內存中的任何信息,所以一次鎖定的行的數目沒有限制。然而,鎖定一行可能會導致磁盤寫入,例如,SELECT FOR UPDATE 修改選定的行並標記它們鎖定,所以會導致磁盤寫入。

Postgres 9.1 和 9.2 中的行級鎖

在這兩種版本中,只有 2 種行級鎖:排他或共享鎖。當行更新或刪除時,會自動獲得排他行級鎖。行級鎖不阻止數據查詢,它們只阻止同一行寫入。 排他行級鎖可由 SELECT FOR UPDATE 命令明確獲得,即使行沒有實際更改。

共享行級鎖可由 SELECT FOR SHARE 命令獲得。一個共享鎖並不阻止其他事務獲取同樣的共享鎖。然而,當任何其他事務持有共享鎖時,事務的更新、刪除或排他鎖都不被允許。

 

Postgres 9.3 和 9.4 中的行級鎖

在 Postgres 9.3 和 9.4 中有四種類型的行級鎖:

  • 更新(FOR UPDATE) - 這種模式導致 SELECT 讀取的行的更新被鎖定。這可以防止它們被其他事務鎖定,修改或刪除。即嘗試 UPDATE、DELETE、SELECT FOR UPDATE、SELECT FOR NO KEY UPDATE、SELECT FOR SHARE 或 SELECT FOR KEY SHARE 的其他事務將被阻塞。刪除一行,更新一些列也可以獲得到此種鎖模式(目前的列集是指那些具有唯一索引,並且可被用作外鍵 - 但將來這可能會改變)。

  • 無鍵更新(FOR NO KEY UPDATE) - 這種模式與 FOR UPDATE 相似,但是更弱 - 它不會阻塞SELECT FOR KEY SHARE 鎖模式。它通過不獲取更新鎖的 UPDATE 命令獲得。

  • 共享(FOR SHARE) - 這種模式與無鍵更新鎖類似,除了它可以獲取共享鎖(非排他)。一個共享鎖阻止其他事務在這些行上進行 UPDATE,DELETE,SELECT FOR UPDATE 或 SELECT FOR NO KEY UPDATE 操作,但並不阻止它們進行 SELECT FOR SHARE 或 SELECT FOR KEY SHARE。

  • 鍵共享(FOR KEY SHARE)- 行為類似於共享,但該鎖是較弱的:阻止了 SELECT FOR UPDATE,但不阻止 SELECT FOR NO KEY UPDATE。一個鍵共享鎖阻止其他事務進行 DELETE 或任何更改該鍵值的 UPDATE,但不妨礙任何其他 UPDATE、SELECT FOR NO KEY UPDATE、SELECT FOR SHARE 或者SELECT FOR KEY SHARE。

行級鎖沖突:

 

頁級鎖

除了表級別和行級別的鎖以外,頁面級別的共享/排他鎖被用來控制對共享緩沖池中表頁面的讀/寫。 這些鎖在行被抓取或者更新后馬上被釋放。應用開發者通常不需要關心頁級鎖,我們在這里提到它們只是為了完整。

勸告鎖

Postgres提供創建具有應用定義的鎖的方法,這些被稱為勸告鎖(advisory locks),因為系統並不支持其使用,其取決於應用對鎖的正確使用。

Postgres中有兩種途徑可以獲得一個勸告鎖:會話層級或事務層級。一旦在會話層級獲得勸告鎖,會一直保持到被顯式釋放或會話結束。不同於標准的鎖請求,會話層級的勸告鎖請求並不遵守事務語義:事務被回滾后鎖也會隨着回滾保持着,同樣地即使調用鎖的事務之后失敗了,解鎖請求仍然是有效的。一個鎖可以被擁有它的進程多次獲取;對於每個完成的鎖請求,在鎖被真正釋放前一定要有一個對應的解鎖請求。

另一方面,事務層級的鎖請求表現得更像普通的鎖請求:它們在事務結束時會自動釋放,並且沒有顯式的解鎖操作。對於短暫地使用勸告鎖,這種特性通常比會話層級更方便。可以想見,會話層級與事務層級請求同一個勸告鎖標識符會互相阻塞。如果一個會話已經有了一個勸告鎖,它再請求時總會成功的,即使其他會話在等待此鎖;不論保持現有的鎖和新的請求是會話層級還是事務層級,都是這樣。文檔中可以找到操作勸告鎖的完整函數列表。

這里有幾個獲取事務層級勸告鎖的例子(pg_locks是系統視圖,文章之后會說明。它存有事務保持的表級鎖和勸告鎖的信息):

啟動第一個psql會話,開始一個事務並獲取一個勸告鎖:

-- Transaction 1
BEGIN;
SELECT pg_advisory_xact_lock(1);
-- Some work here

    現在啟動第二個psql會話並在同一個勸告鎖上執行一個新的事務:

-- Transaction 2
BEGIN;
SELECT pg_advisory_xact_lock(1);
-- This transaction is now blocked

在第三個psql會話里我們可以看下這個鎖現在的情況:

復制代碼
SELECT * FROM pg_locks;-- Only relevant parts of output
   locktype    | database | relation | page | tuple | virtualxid | transactionid | classid | objid | objsubid | virtualtransaction |  pid  |        mode         | granted |fastpath---------------+----------+----------+------+-------+------------+---------------+---------+-------+----------+--------------------+-------+---------------------+---------+----------
    advisory   |    16393 |          |      |       |            |               |       0 |     1 |        1 | 4/36               |  1360 | ExclusiveLock       | f       | f
    advisory   |    16393 |          |      |       |            |               |       0 |     1 |        1 | 3/186              | 14340 | ExclusiveLock       | t       | f
-- Transaction 1
COMMIT;
-- This transaction now released lock, so Transaction 2 can continue
復制代碼

 

我們同樣可以調用獲取鎖的非阻塞方法,這些方法會嘗試去獲取鎖,並返回true(如果成功了)或者false(如果無法獲取鎖)。

復制代碼
-- Transaction 1
BEGIN;
SELECT pg_advisory_xact_lock(1);
-- Some work here
-- Transaction 2
BEGIN;
SELECT pg_try_advisory_xact_lock(1) INTO vLockAcquired;
IF vLockAcquired THEN
-- Some work
ELSE
-- Lock not acquired
END IF;
-- Transaction 1
COMMIT;
復制代碼

 

監控鎖

所有活動事務持有的監控鎖的基本配置即為系統視圖 pg_locks。這個視圖為每個可加鎖的對象、已請求的鎖模式和相關事務包含一行記錄。非常重要的一點是,pg_locks 持有內存中被跟蹤的鎖的信息,所以它不顯示行級鎖!(譯注:據查以前的文檔,有關行級鎖的信息是存在磁盤上,而非內存)這個視圖顯示表級鎖和勸告鎖。如果一個事務在等待一個行級鎖,它通常在視圖中顯示為在等待該行級鎖的當前所有者的固定事務 ID。這使得調試行級鎖更為困難。事實上,在任何地方你都看不到行級鎖,直到有人阻塞了持有此鎖的事務(然后你在 pg_locks 表里可以看到一個被上鎖的元組)。pg_locks 是可讀性欠佳的視圖(不是很人性化),所以我們來讓顯示鎖定信息的視圖更好接受些:

復制代碼
-- View with readable locks info and filtered out locks on system tables
CREATE VIEW active_locks AS
SELECT clock_timestamp(), pg_class.relname, pg_locks.locktype, pg_locks.database,
       pg_locks.relation, pg_locks.page, pg_locks.tuple, pg_locks.virtualtransaction,
       pg_locks.pid, pg_locks.mode, pg_locks.granted
FROM pg_locks JOIN pg_class ON pg_locks.relation = pg_class.oid
WHERE relname !~ '^pg_' and relname <> 'active_locks';
-- Now when we want to see locks just type
SELECT * FROM active_locks;
復制代碼

 

復制代碼
--查看會話session
select pg_backend_pid();

--查看會話持有的鎖
select * from pg_locks where pid=3797;

--1,查看數據庫

select  pg_database.datname, pg_database_size(pg_database.datname) AS size from pg_database; //查詢所有數據庫,及其所占空間大小

--2. 查詢存在鎖的數據表

select a.locktype,a.database,a.pid,a.mode,a.relation,b.relname -- ,sa.*
from pg_locks a
join pg_class b on a.relation = b.oid 
inner join  pg_stat_activity sa on a.pid=sa.procpid

--3.查詢某個表內,狀態為lock的鎖及關聯的查詢語句

select a.locktype,a.database,a.pid,a.mode,a.relation,b.relname -- ,sa.*
from pg_locks a
join pg_class b on a.relation = b.oid 
inner join  pg_stat_activity sa on a.pid=sa.procpid
where a.database=382790774  and sa.waiting_reason='lock'
order by sa.query_start
--4.查看數據庫表大小

select pg_database_size('playboy'); 
復制代碼

 

--查看會話被誰阻塞
select pg_blocking_pids(3386);

 

死鎖

顯式鎖定的使用可能會增加死鎖的可能性,死鎖是指兩個(或多個)事務相互持有對方想要的鎖。
例如,如果事務 1 在表 A 上獲得一個排他鎖,同時試圖獲取一個在表 B 上的排他鎖, 而事務 2 已經持有表 B 的排他鎖,同時卻正在請求表 A 上的一個排他鎖,那么兩個事務就都不能進行下去。PostgreSQL能夠自動檢測到死鎖情況 並且會通過中斷其中一個事務從而允許其它事務完成來解決這個問題(具體哪個事務會被中 斷是很難預測的,而且也不應該依靠這樣的預測)。
要注意死鎖也可能會作為行級鎖的結果而發生(並且因此,它們即使在沒有使用顯式鎖定的情況下也會發生)。考慮如下情況,兩個並發事務在修改一個表。第一個事務執行:

這樣就在指定帳號的行上獲得了一個行級鎖。然后,第二個事務執行:

UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 22222;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 11111;

第一個UPDATE語句成功地在指定行上獲得了一個行級鎖,因此它成功更新了該行。 但是第
二個UPDATE語句發現它試圖更新的行已經被鎖住了,因此它等待持有該鎖的事務結束。事
務二現在就在等待事務一結束,然后再繼續執行。現在,事務一執行:

UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 22222;

事務一試圖在指定行上獲得一個行級鎖,但是它得不到:事務二已經持有了這樣的鎖。所以
它要等待事務二完成。因此,事務一被事務二阻塞,而事務二也被事務一阻塞:一個死鎖。
PostgreSQL將檢測這樣的情況並中斷其中一個事務。

防止死鎖的最好方法通常是保證所有使用一個數據庫的應用都以一致的順序在多個對象上獲得鎖。在上面的例子里,如果兩個事務以同樣的順序更新那些行,那么就不會發生死鎖。 我們也應該保證一個事務中在一個對象上獲得的第一個鎖是該對象需要的最嚴格的鎖模式。如果我們無法提前驗證這些,那么可以通過重試因死鎖而中斷的事務來即時處理死鎖。
只要沒有檢測到死鎖情況,尋求一個表級或行級鎖的事務將無限等待沖突鎖被釋放。這意味着一個應用長時間保持事務開啟不是什么好事(例如等待用戶輸入)。

咨詢鎖

  • PostgreSQL提供了一種方法創建由應用定義其含義的鎖。這種鎖被稱為咨詢鎖,因為系統並不強迫其使用 — 而是由應用來保證其正確的使用。咨詢鎖可用於 MVCC 模型不適用的鎖定策略。例如,咨詢鎖的一種常用用法是模擬所謂“平面文件”數據管理系統典型的悲觀鎖策略。雖然一個存儲在表中的標志可以被用於相同目的,但咨詢鎖更快、可以避免表膨脹並且會由服務器在會話結束時自動清理。
  • 有兩種方法在PostgreSQL中獲取一個咨詢鎖:在會話級別或在事務級別。一旦在會話級別獲得了咨詢鎖,它將被保持直到被顯式釋放或會話結束。不同於標准鎖請求,會話級咨詢鎖請求不尊重事務語義:在一個后來被回滾的事務中得到的鎖在回滾后仍然被保持,並且同樣即使調用它的事務后來失敗一個解鎖也是有效的。一個鎖在它所屬的進程中可以被獲取多次;對於每一個完成的鎖請求必須有一個相應的解鎖請求,直至鎖被真正釋放。在另一方面,事務級鎖請求的行為更像普通鎖請求:在事務結束時會自動釋放它們,並且沒有顯式的解鎖操作。這種行為通常比會話級別的行為更方便,因為它使用一個咨詢鎖的時間更短。對於同一咨詢鎖標識符的會話級別和事務級別的鎖請求按照期望將彼此阻塞。如果一個會話已經持有了一個給定的咨詢鎖,由它發出的附加請求將總是成功,即使有其他會話在等待該鎖;不管現有的鎖和新請求是處在會話級別還是事務級別,這種說法都是真的。
  • 和所有PostgreSQL中的鎖一樣,當前被任何會話所持有的咨詢鎖的完整列表可以在pg_locks系統視圖中找到
  • 咨詢鎖和普通鎖都被存儲在一個共享內存池中,它的尺寸由max_locks_per_transaction和max_connections配置變量定義。 必須當心不要耗盡這些內存,否則服務器將不能再授予任何鎖。這對服務器可以授予的咨詢鎖數量設置了一個上限,根據服務器的配置不同,這個限制通常是數萬到數十萬。
  • 在使用咨詢鎖方法的特定情況下,特別是查詢中涉及顯式排序和LIMIT子句時,由於 SQL 表達式被計算的順序,必須小心控制鎖的獲取。例如:
SELECT pg_advisory_lock(id) FROM foo WHERE id = 12345; -- ok
SELECT pg_advisory_lock(id) FROM foo WHERE id > 12345 LIMIT 100; -- danger!
SELECT pg_advisory_lock(q.id) FROM
(
 SELECT id FROM foo WHERE id > 12345 LIMIT 100
) q; -- ok

 

在上述查詢中,第二種形式是危險的,因為不能保證在鎖定函數被執行之前應用LIMIT。這
可能導致獲得某些應用不期望的鎖,並因此在會話結束之前無法釋放。 從應用的角度來看,
這樣的鎖將被掛起,雖然它們仍然在pg_locks中可見。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM