pgsql中的行鎖
前言
日常的工作中,對於同一個資源的操作,有時候我們難免要加上鎖,以防止在操作中被別的進程給刪除或者更改掉了,那么更多還是行級鎖,那么我們就來探究下。
用戶可見的鎖
從系統視圖pg_locks中可見
用戶可見的鎖,用戶自己能夠主動調用的,可以在pg_locks中看到是否grant的鎖。包括regular lock和咨詢鎖。
regular Lock
regular lock分為表級別和行級別兩種。
行級別
通過一些數據庫操作自動獲得一些行鎖,行鎖並不阻塞數據查詢,只阻塞writes和locker,比如如下操作。
FOR UPDATE
FOR NO KEY UPFATE
FOR SHARE
FOR KEY SHARE
FOR UPDATE
FOR UPDATE鎖可以使得SELECT語句獲取行級鎖,用於更新數據。鎖定該行可以防止該行在本次的操作過程中,被其他的事務獲取鎖或者進行更改刪除操作。就是說其他事務的操作會被阻塞直到當前事務結束;同樣的,SELECT FOR UPDATE命令會等待直
到前一個事務結束。即嘗試 UPDATE、DELETE、SELECT FOR UPDATE、SELECT FOR NO KEY UPDATE、SELECT FOR SHARE 或 SELECT FOR KEY SHARE 的其他事務將被阻塞。反過來,SELECT FOR UPDATE將等待已經在相同行上運行以上這些命令的
並發事務,並且接着鎖定並且返回被更新的行(或者沒有行,因為行可能已被刪除)。不過,在一個REPEATABLE READ或SERIALIZABLE事務中,如果一個要被鎖定的行在事務開始后被更改,將會拋出一個錯誤。
任何在一行上的DELETE命令也會獲得FOR UPDATE鎖模式,在某些列上修改值的UPDATE也會獲得該鎖模式。當前UPDATE情況中被考慮的列集合是那些具有能用於外鍵的唯一索引的列(所以部分索引和表達式索引不被考慮),但是這種要求未來有可能會改變。
FOR NO KEY UPDATE
和FOR UPDATE命令類似,但是對於獲取鎖的要求更加寬松一些,在同一行中不會阻塞SELECT FOR KEY SHARE命令。同樣在UPDATE命令的時候如果沒有獲取到FOR UPDATE鎖的情況下會獲取到該鎖。
FOR SHARE
行為與FOR NO KEY UPDATE類似,不過它在每個檢索到的行上獲得一個共享鎖而不是排他鎖。一個共享鎖會阻塞其他事務在這些行上執行UPDATE、DELETE、SELECT FOR UPDATE或者SELECT FOR NO KEY UPDATE,但是它不會阻止它們執行SELECT FOR SHARE或者SELECT FOR KEY SHARE。
FOR KEY SHARE
行為與FOR SHARE類似,不過鎖較弱:SELECT FOR UPDATE會被阻塞,但是SELECT FOR NO KEY UPDATE不會被阻塞。一個鍵共享鎖會阻塞其他事務執行修改鍵值的DELETE或者UPDATE,但不會阻塞其他UPDATE,也不會阻止SELECT FOR NO KEY UPDATE、SELECT FOR SHARE或者SELECT FOR KEY SHARE。
測試下加鎖之后的數據可見性
create table test_lock
(
id serial not null,
name text not null
);
alter table test_lock
owner to postgres;
create unique index test_lock_id_uindex
on test_lock (id);
INSERT INTO public.test_lock (id, name) VALUES (1, '小明');
INSERT INTO public.test_lock (id, name) VALUES (2, '小白');
加鎖測試(FOR UPDATE)
查詢1
/*查詢事務1*/
begin;
select *
from test_lock
where id = 1
for update
查詢2
/*查詢事務2*/
begin;
select *
from test_lock
where id = 1
for update
當事務1在查詢中鎖住資源的時候,事務2就一直查不到數據,等待事務1提交
查詢1事務提交
commit
事務2的查詢馬上結束等待,查詢出當前的數據
別忘了事務2的commit提交
加鎖測試(FOR UPDATE,UPDATE)
UPDATE和DELETE,操作也是帶鎖的,測試下和FOR UPDATE的阻塞情況
查詢1
/*查詢事務1*/
begin;
update test_lock set name='mignming' where id=1
查詢2
/*查詢事務2*/
begin;
select *
from test_lock
where id = 1
for update
發現SELECT FOR UPDATE被阻塞了
提交查詢1,update的事務
commit
然后查詢2結束阻塞,獲取到了事務1更新的數據
別忘了事務2的commit提交
命令說明
begin;--開啟事務
begin transaction;--開啟事務
commit;--提交
rollback;--回滾
set lock_timeout=5000;--設置超時時間
需要注意的點
連表查詢加鎖時,不支持單邊連接形式,例如:
select u.*,r.* from db_user u left join db_role r on u.roleid=r.id for update;
支持以下形式,並鎖住了兩個表中關聯的數據:
select u.*,r.* from db_user u, db_role r where u.roleid=r.id for update;
舉個栗子
有一個分類表category,有一個文檔表document。一個分類對應多個文檔,刪除分類的時候有一個限制,分類下面必須沒有文檔才能刪除。這時候刪除可能出現這樣的場景,當刪除分類的時候,后面新建了一個文檔,兩事務並行,根據pgsql默認的事務隔離級別,讀已提交。新建文檔,通過分類id獲取到的分類信息,是進行分類刪除之前的信息,也就是查詢到這個分類存在。當兩個事物一起執行了,有可能出現新建的文檔的分類id不存在的情況。那就出現了臟數據了。
如何解決呢?
首先想到的肯定是加鎖了。
如果一個資源加鎖了,后面的操作必須要等到前面資源操作結束才能獲取到資源信息。
這是pgsq文檔對讀已提交隔離中行鎖的查詢描述,需要注意的是UPDATE,DELETE也是會鎖住資源的
UPDATE、DELETE、SELECT FOR UPDATE和SELECT FOR SHARE命令在搜索目標行時的行為和SELECT一樣: 它們將只找到在命令開始時已經被提交的行。 不過,在被找到時,這樣的目標行可能已經被其它並發事務更新(或刪除或鎖住)。在這種情況下, 即將進行的更新將等待第一個更新事務提交或者回滾(如果它還在進行中)。 如果第一個更新事務回滾,那么它的作用將被忽略並且第二個事務可以繼續更新最初發現的行。 如果第一個更新事務提交,若該行被第一個更新者刪除,則第二個更新事務將忽略該行,否則第二個更新者將試圖在該行的已被更新的版本上應用它的操作。該命令的搜索條件(WHERE子句)將被重新計算來看該行被更新的版本是否仍然符合搜索條件。如果符合,則第二個更新者使用該行的已更新版本繼續其操作。在SELECT FOR UPDATE和SELECT FOR SHARE的情況下,這意味着把該行的已更新版本鎖住並返回給客戶端。
所以,解決方法就是在,添加文檔的時候對分類id加鎖,這樣刪除分類的鎖就和下面查詢的鎖互斥了,兩者必須有個先后執行的順序,會避免臟數據的產生。
WITH lock_document_categories_cte AS (
SELECT id
FROM document_categories
WHERE id = ${categoryId}
AND enterprise_id = ${enterpriseId}
FOR UPDATE
),lock_document_directories_cte AS (
SELECT id
FROM document_directories
WHERE id = ${directoryId}
AND enterprise_id = ${enterpriseId}
FOR UPDATE
)
INSERT INTO documents (
enterprise_id, directory_id, category_id, code, name, author_id
) VALUES (
${enterpriseId}, (SELECT * FROM lock_document_directories_cte), (SELECT * FROM lock_document_categories_cte), ${code}, ${name}, ${authorId}
)
RETURNING id
通過lock_document_categories_cte中的FOR UPDATE,鎖住資源,這樣就和delete中的鎖互斥了。
總結
UPDATE、DELETE、SELECT FOR UPDATE和SELECT FOR SHARE。都會對資源加鎖。當加鎖的資源在被執行的時候。后面的操作,要等前面資源操作執行完成才能進行操作, 即將進行的更新將等待第一個更新事務提交或者回滾(如果它還在進行中)。 如果第一個更新事務回滾,那么它的作用將被忽略並且第二個事務可以繼續更新最初發現的行。 如果第一個更新事務提交,若該行被第一個更新者刪除,則第二個更新事務將忽略該行,否則第二個更新者將試圖在該行的已被更新的版本上應用它的操作。該命令的搜索條件(WHERE子句)將被重新計算來看該行被更新的版本是否仍然符合搜索條件。如果符合,則第二個更新者使用該行的已更新版本繼續其操作。在SELECT FOR UPDATE和SELECT FOR SHARE的情況下,這意味着把該行的已更新版本鎖住並返回給客戶端。
參考
【Postgresql鎖機制(表鎖和行鎖)】https://blog.csdn.net/turbo_zone/article/details/84036511
【postgresql行級鎖for update測試】https://blog.csdn.net/shuoyu816/article/details/80086810
【PostgreSQL 鎖解密 】https://www.oschina.net/translate/postgresql-locking-revealed
【顯式鎖定】http://postgres.cn/docs/11/explicit-locking.html