簡體   English   中英

PostgreSQL select 更新鎖,新行

[英]PostgreSQL select for update lock, new rows

我有以下並發用例:可以隨時調用端點並且應該發生操作。 操作在偽代碼中是這樣的(當前隔離級別是READ COMMITTED ):

SELECT * FROM TABLE_A WHERE IS_LATEST=true FOR UPDATE
// DO SOME APP LOGIC TO TEST VALIDITY
// ALL GOES WELL => INSERT OR UPDATE NEW ROW WITH IS_LATEST=TRUE => COMMIT
// OTHERWISE => ROLLBACK (all good not interesting)

現在,如果這些操作中的兩個在更新方面同時開始,則使用SELECT FOR UPDATE的方法很好。 因為兩個事務看到相同的行數,一個將更新行,第二個事務將等待輪到它才能進行SELECT FOR UPDATE並且 state 有效。

我遇到的問題是當我在第一筆交易中插入時。 發生的情況是,例如,當第一個事務鎖定SELECT FOR UPDATE時,有兩行,然后事務繼續,在事務中間,第二個事務進來想要SELECT FOR UPDATE (最新)並等待第一個transaction to finish.. 第一個事務完成並且數據庫中實際上有一個新的第三個項目,但是第二個事務在等待行鎖被釋放時只拾取了兩行。 (這是因為在調用SELECT FOR UPDATE時,快照不同,只有兩行匹配IS_LATEST=true )。

有沒有辦法讓SELECT鎖在等待后獲取最新的快照?

使用您的方法,第二個事務中的查詢將在鎖定消失后返回空結果,因為它在相關行上看到is_latest = FALSE ,並且新行尚不可見。 所以在這種情況下你必須重試交易。

我建議您改用REPEATABLE READ隔離級別和樂觀鎖定:

BEGIN ISOLATION LEVEL REPEATABLE READ;

SELECT * FROM table_a WHERE is_latest;  -- no locks!

/* perform your application ruminations */

UPDATE table_a SET is_latest = FALSE WHERE id = <id you found above>;

INSERT INTO table_a (is_latest, ...) VALUES (TRUE, ...);

COMMIT;

然后可能會發生三件事:

  1. 您的查詢找到一行,事務成功。

  2. 您的查詢沒有找到任何行,那么您可以插入第一行。

  3. 查詢找到一行,但更新該行會導致序列化錯誤。
    在那種情況下,您知道並發事務受到干擾,並且您重復完整的事務作為響應。

問題是每個命令只能看到在查詢開始之前已經提交的行。 有各種可能的解決方案......

更嚴格的隔離級別

可以使用更嚴格的隔離級別來解決這個問題,但這樣做的成本相對較高。

Laurenz 已經為此提供了解決方案

只需啟動一個新命令

保持(便宜的)默認隔離級別READ COMMITTED ,然后開始一個新命令

只有幾行要鎖定

雖然只鎖定了滿滿一手行,但最簡單的解決方案是重復相同的SELECT... FOR UPDATE 第二次迭代看到新提交的行並額外鎖定它們。

有一個理論上的競爭條件,附加事務可能會在等待事務之前鎖定新行。 這將導致僵局。 極不可能,但絕對可以肯定的是,以一致的順序鎖定行:

BEGIN;  -- default READ COMMITTED

SELECT FROM table_a WHERE is_latest ORDER BY id FOR UPDATE;  -- consistent order
SELECT * FROM table_a WHERE is_latest ORDER BY id FOR UPDATE;  -- just repeat !!

--  DO SOME APP LOGIC TO TEST VALIDITY

-- pseudo-code
IF all_good
   UPDATE table_a SET is_latest = true WHERE ...;
   INSERT table_a (IS_LATEST, ...) VALUES (true, ...);
   COMMIT;
ELSE
   ROLLBACK;
END; 

(id) WHERE is_latest上的部分索引將是理想的。

要鎖定更多行

對於超過一手的行,我會改為創建一個專用的單行標記表。 防彈實現可能如下所示,以管理員或超級用戶身份運行:

CREATE TABLE public.single_task_x (just_me bool CHECK (just_me) PRIMARY KEY DEFAULT true);
INSERT INTO public.single_task_x VALUES (true);
REVOKE ALL ON public.single_task_x FROM public;
GRANT SELECT, UPDATE ON public.single_task_x TO public;  -- or just to those who need it

看:

然后:

BEGIN;  -- default READ COMMITTED

SELECT FROM public.single_task_x FOR UPDATE;
SELECT * FROM table_a WHERE is_latest;  -- FOR UPDATE? ①

--  DO SOME APP LOGIC TO TEST VALIDITY

-- pseudo-code
IF all_good
   ROLLBACK;
ELSE
   UPDATE table_a SET is_latest = true WHERE ...;
   INSERT table_a (IS_LATEST, ...) VALUES (true, ...);
   COMMIT;
END; 

單鎖更便宜。
① 您可能想要也可能不想額外鎖定,以防御其他寫入,可能使用較弱的鎖定....

無論哪種方式,所有鎖都會在事務結束時自動釋放。

咨詢鎖

或者使用咨詢鎖 pg_advisory_xact_lock()在交易期間持續存在:

BEGIN;  -- default READ COMMITTED

SELECT pg_advisory_xact_lock(123);
SELECT * FROM table_a WHERE is_latest;

-- do stuff

COMMIT;  -- or ROLLBACK; 

確保為您的特定任務使用唯一的令牌。 在我的例子中是123 如果您有許多不同的任務,請考慮使用查找表。

要在不同的時間點(不是事務結束時)釋放鎖,請考慮使用pg_advisory_lock()的會話級鎖。 然后您可以(並且必須)使用pg_advisory_unlock()手動解鎖 - 或者關閉 session。

這兩個都等待鎖定的資源。 替代函數返回false而不是等待......

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM