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;
現在我們有了做實驗的游樂場。。。
簡單示例
我們創建一些用于練習的表:
CREATE TABLE parent ( id serial NOT NULL PRIMARY KEY, name text NOT NULL); CREATE TABLE child ( id serial NOT NULL PRIMARY KEY, parent_id int4 NOT NULL, name text NOT NULL, CONSTRAINT child_parent_fk FOREIGN KEY (parent_id) REFERENCES parent(id) );
并嘗試一些簡單的事務,看看鎖是什么樣的:
BEGIN;
SELECT * FROM active_locks; -- There are no active locks yet
clock_timestamp | relname | locktype | database | relation | page | tuple | virtualtransaction | pid | mode | granted
-----------------+---------+----------+----------+----------+------+-------+--------------------+-----+------+---------
(0 rows)
INSERT INTO parent (name) VALUES ('Parent 1');
SELECT * FROM active_locks;
clock_timestamp | relname | locktype | database | relation | page | tuple | virtualtransaction | pid | mode | granted
----------------------------+---------------+----------+----------+----------+------+-------+--------------------+------+------------------+---------
2015-04-12 13:43:02.896+02 | parent_id_seq | relation | 16393 | 16435 | | | 3/150 | 9000 | AccessShareLock | t
2015-04-12 13:43:02.896+02 | parent | relation | 16393 | 16437 | | | 3/150 | 9000 | RowExclusiveLock | t
(2 rows)
COMMIT;
SELECT * FROM active_locks;
clock_timestamp | relname | locktype | database | relation | page | tuple | virtualtransaction | pid | mode | granted
-----------------+---------+----------+----------+----------+------+-------+--------------------+-----+------+---------
(0 rows) 我們可以看到在 parent 表里插入一行后,我們獲得了 parent 表上的行獨占鎖。parent_id_seq 是 parent 表的主鍵序列。由于這種關系被選中(如表),我們獲得了訪問共享鎖。
咱們試著往 child 表里插點東西;
BEGIN; INSERT INTO child (parent_id, name) VALUES (1, 'Child 1 Parent 1'); SELECT * FROM active_locks; clock_timestamp | relname | locktype | database | relation | page | tuple | virtualtransaction | pid | mode | granted ---------------------------+--------------+----------+----------+----------+------+-------+--------------------+------+------------------+--------- 2015-04-12 13:50:48.17+02 | parent_pkey | relation | 16393 | 16444 | | | 3/152 | 9000 | AccessShareLock | t 2015-04-12 13:50:48.17+02 | parent | relation | 16393 | 16437 | | | 3/152 | 9000 | RowShareLock | t 2015-04-12 13:50:48.17+02 | child_id_seq | relation | 16393 | 16446 | | | 3/152 | 9000 | AccessShareLock | t 2015-04-12 13:50:48.17+02 | child | relation | 16393 | 16448 | | | 3/152 | 9000 | RowExclusiveLock | t (4 rows) COMMIT;
現在的情況就有趣多了。我們可以看到 parent 表上增加的行共享鎖。我們看不到的是,這個插入同樣獲得了 parent 表上引用行的行級共享鎖。并行執行兩個事務我們就可以看到它了:
-- Transaction 1 BEGIN; INSERT INTO child (parent_id, name) VALUES (1, 'Child 2 Parent 1');
-- Transaction 2 BEGIN; DELETE FROM parent WHERE id = 1;
現在開始第三個會話,看看我們的鎖怎么樣了:
SELECT * FROM active_locks; clock_timestamp | relname | locktype | database | relation | page | tuple | virtualtransaction | pid | mode | granted ----------------------------+--------------+----------+----------+----------+------+-------+--------------------+------+---------------------+--------- 2015-04-12 14:18:35.005+02 | parent_pkey | relation | 16393 | 16444 | | | 4/32 | 4428 | RowExclusiveLock | t 2015-04-12 14:18:35.005+02 | parent | relation | 16393 | 16437 | | | 4/32 | 4428 | RowExclusiveLock | t 2015-04-12 14:18:35.005+02 | parent_pkey | relation | 16393 | 16444 | | | 3/153 | 9000 | AccessShareLock | t 2015-04-12 14:18:35.005+02 | parent | relation | 16393 | 16437 | | | 3/153 | 9000 | RowShareLock | t 2015-04-12 14:18:35.005+02 | child_id_seq | relation | 16393 | 16446 | | | 3/153 | 9000 | AccessShareLock | t 2015-04-12 14:18:35.005+02 | child | relation | 16393 | 16448 | | | 3/153 | 9000 | RowExclusiveLock | t 2015-04-12 14:18:35.005+02 | parent | tuple | 16393 | 16437 | 0 | 1 | 4/32 | 4428 | AccessExclusiveLock | t (7 rows)
DELETE 查詢被阻塞了,等待事務 1 完成。我們可以看到它在元組 1 上獲得了一個鎖。但是如果我們看到所有的鎖都是準許的(granted=t),為什么 DELETE 查詢被阻塞了?這兩個事務在任何關系上的鎖都沒有同步過。事實上,如果一個事務在某些行上持有一個鎖,第二個事務請求這個鎖,第二個事務會嘗試獲取持有此鎖的事務上的共享鎖。當第一個事務完成時,第二個事務將繼續。這是可能的,因為每個事務都持有它自身的排他鎖。我們可以看看 pg_locks 視圖,這是輸出(只有部分是重要的):
locktype | database | relation | page | tuple | virtualxid | transactionid | classid | objid | objsubid | virtualtransaction | pid | mode | granted |fastpath---------------+----------+----------+------+-------+------------+---------------+---------+-------+----------+--------------------+-------+---------------------+---------+---------- transactionid | | | | | | 707 | | | | 3/153 | 9000 | ExclusiveLock | t | f transactionid | | | | | | 707 | | | | 4/32 | 4428 | ShareLock | f | f transactionid | | | | | | 708 | | | | 4/32 | 4428 | ExclusiveLock | t | f
我們可以看到事務 707(pid 9000)和事務 708(pid 4428)擁有它們事務標識上的排他鎖,事 務708 獲得了事務 707 上的共享鎖。
現在,最有趣的樣本。我們可以玩玩更新子表但不實際改變任何父表與相關的東西(在這個案例中是parent_id列)。
BEGIN; UPDATE child SET name = 'My new name' WHERE id = 1; SELECT * FROM active_locks; clock_timestamp | relname | locktype | database | relation | page | tuple | virtualtransaction | pid | mode | granted----------------------------+------------+----------+----------+----------+------+-------+--------------------+------+------------------+--------- 2015-04-14 09:05:42.713+02 | child_pkey | relation | 16393 | 16455 | | | 3/183 | 3660 | RowExclusiveLock | t 2015-04-14 09:05:42.713+02 | child | relation | 16393 | 16448 | | | 3/183 | 3660 | RowExclusiveLock | t (2 rows) UPDATE child SET name = 'My new name' WHERE id = 1; SELECT * FROM active_locks; clock_timestamp | relname | locktype | database | relation | page | tuple | virtualtransaction | pid | mode | granted----------------------------+-------------+----------+----------+----------+------+-------+--------------------+------+------------------+--------- 2015-04-14 09:05:45.765+02 | parent_pkey | relation | 16393 | 16444 | | | 3/183 | 3660 | AccessShareLock | t 2015-04-14 09:05:45.765+02 | parent | relation | 16393 | 16437 | | | 3/183 | 3660 | RowShareLock | t 2015-04-14 09:05:45.765+02 | child_pkey | relation | 16393 | 16455 | | | 3/183 | 3660 | RowExclusiveLock | t 2015-04-14 09:05:45.765+02 | child | relation | 16393 | 16448 | | | 3/183 | 3660 | RowExclusiveLock | t (4 rows) COMMIT;
這是非常有趣的,最重要的是要記住。我們可以看到,我們正在執行的 UPDATE 查詢不會觸及任何與父表相關的東西。第一次執行后,我們可以看到,只有 child 表包含表級鎖。行級鎖也是如此。只有 child 表的行有更新鎖。這是 Postgres 中的優化。如果鎖管理器可以從第一個查詢中發現外鍵沒有改變(沒有被更新查詢提及或被設置為相同的值),它不會鎖定父表。但在第二個查詢它會像文檔描述的那樣處理(它將鎖定 parent 表為行共享鎖定模式和涉及的行為分享模式)。這是非常危險的,因為它會導致最危險的和最難找到的死鎖。我們可以在事務開頭使用顯式鎖定以避免它。Postgres 9.1 和 9.2 的特性不同于 9.3 和 9.4。不同之處與行級鎖相關。9.3 和 9.4 將會在 parent 表上獲得較弱的鍵共享鎖模式。這種鎖定模式不與無鍵更新模式沖突,所以能被兩個事務并行獲取。這種要好很多,所以 9.3 和 9.4 死鎖的概率也會更低。
防止死鎖的最好方式,是當我們意識到它們可能在兩個事務之間發生時,去按一定順序獲取行級鎖(例如主鍵排序)和首先獲取最嚴格的鎖。對前文所述的 Postgres 鎖定優化留個心眼,顯式鎖定有時是避免死鎖的唯一途徑。
一旦在 Postgres 發生死鎖,可以通過中止一個參與死鎖的事務來消除。準確預測哪個事務會被中止很難,也不應依賴于此。
摘要
對 Postgres 中鎖的工作機制銘記于心非常重要。在高并發環境中死鎖可能無法避免,但重要的是要知道如何發現、監控并解決它們。即使所有事情都照書而行也不一定能解決所有潛在的鎖問題,但會減少它們并使其易于解決。表級鎖可以通過 pg_locks 系統視圖查看,但行級鎖不行,所以這讓調試鎖更為困難,所以我希望這在 Postgres 的未來版本中變得可能。
本文地址:http://www.oschina.net/translate/postgresql-locking-revealed
原文地址:http://blog.nordeus.com/dev-ops/postgresql-locking-revealed.htm