2017-08-10 76 views
3

在我們使用Postgres工作的許多事情中,我們使用它作爲某種遠程請求的緩存。我們的模式是:Postgres中兩個DELETE查詢如何死鎖?

CREATE TABLE IF NOT EXISTS cache (
    key VARCHAR(256) PRIMARY KEY, 
    value TEXT NOT NULL, 
    ttl TIMESTAMP DEFAULT NULL 
); 

CREATE INDEX IF NOT EXISTS idx_cache_ttl ON cache(ttl); 

此表沒有觸發器或外鍵。更新通常是:

INSERT INTO cache (key, value, ttl) 
VALUES ('Ethan is testing8393645', '"hi6286166"', sec2ttl(300)) 
ON CONFLICT (key) DO UPDATE 
SET value = '"hi6286166"', ttl = sec2ttl(300); 

(凡sec2ttl被定義爲:)

CREATE OR REPLACE FUNCTION sec2ttl(seconds FLOAT) 
RETURNS TIMESTAMP AS $$ 
BEGIN 
    IF seconds IS NULL THEN 
     RETURN NULL; 
    END IF; 
    RETURN now() + (seconds || ' SECOND')::INTERVAL; 
END; 
$$ LANGUAGE plpgsql; 

查詢緩存在一個事務中做過這樣的:

BEGIN; 
DELETE FROM cache WHERE ttl IS NOT NULL AND now() > ttl; 
SELECT value FROM cache WHERE key = 'Ethan is testing6460437'; 
COMMIT; 

有幾件事情不喜歡這個設計 - DELETE發生在緩存「讀取」,cache.ttl上的索引不是升序,這使得它有用ss,(編輯:ASC是默認值,謝謝wargre!)以及我們使用Postgres作爲緩存的事實。但所有這一切本來是可以接受的,除了我們已經開始讓生產死鎖,這往往是這樣的:

ERROR: deadlock detected 
DETAIL: Process 12750 waits for ShareLock on transaction 632693475; blocked by process 10080. 
Process 10080 waits for ShareLock on transaction 632693479; blocked by process 12750. 
HINT: See server log for query details. 
CONTEXT: while deleting tuple (426,1) in relation "cache" 
[SQL: 'DELETE FROM cache WHERE ttl IS NOT NULL AND now() > ttl;'] 

更徹底地調查日誌表明,交易雙方均執行此操作DELETE

至於我可以告訴大家:

  • 我的交易是在READ COMMITTED隔離模式。
  • ShareLock被一個事務抓取,表明它想要改變另一個事務已經發生變化(即鎖定)的行。
  • 根據EXPLAIN查詢的輸出結果,ShareLocks應該被物理順序的DELETE事務抓取。
  • 死鎖表明兩個查詢以不同的順序鎖定行。

如果這一切都是正確的,那麼某種同時發生的事務已經改變了行的物理順序。我發現UPDATE可以將一行移動到更早或更晚的物理位置,但在我的應用程序中,UPDATE總是從DELETE s中刪除行(因爲它們總是延長行的TTL)。如果這些行以前是按照物理順序排列的,並且您刪除了一個,那麼您仍然保持物理順序。同樣爲DELETE。我們沒有執行任何VACUUM或您可能期望對行重新排序的任何其他操作。

基於Avoiding PostgreSQL deadlocks when performing bulk update and delete operations,我試圖DELETE查詢更改爲:

DELETE FROM cache c 
USING (
    SELECT key 
    FROM cache 
    WHERE ttl IS NOT NULL AND now() > ttl 
    ORDER BY ttl ASC 
    FOR UPDATE 
) del 
WHERE del.key = c.key; 

不過,我仍然能夠獲得本地死鎖。那麼一般來說,兩個DELETE查詢怎麼會死鎖呢?是因爲他們以未定義的順序鎖定,如果是這樣,我如何執行特定的訂單?

+0

如何在自動提交和無事務的連接中管理緩存? (順便說一下,索引是正確的,它是由你的dlete使用的) – wargre

+0

我想即使我沒有交易,Postgres圍繞每一個查詢創建一個交易,對吧? (順便說一句,謝謝!我看到ASC是索引的默認值。) – Ethan

+1

「SELECT ... FOR UPDATE」的意義在於爲所有鎖定採集實施全局一致的順序。如果兩個「ttl」值一致,那麼您的行排序是未定義的,並且如果更新了「ttl」值,那麼在明確定義的情況下,排序可能會在併發事務之間有所不同。改爲使用'ORDER BY key'。 –

回答

0

而應該忽略過期的緩存條目,這樣你就不會依賴於頻繁的刪除操作緩存過期:

SELECT value 
FROM cache 
WHERE 
    key = 'Ethan is testing6460437' 
    and (ttl is null or ttl<now()); 

而且具有周期性選擇鍵來刪除跳過已鎖定鍵另一份工作,其中有要麼迫使一個明確刪除的行,或者更好的順序,跳過已經鎖定更新行:

with delete_keys as (
    select key from cache 
    where 
    ttl is not null 
    and now()>ttl 
    for update skip locked 
) 
delete from cache 
where key in (select key from delete_keys); 

如果無法安排此週期應該運行此清理像隨機的,每1000只運行一次您的選擇que ry,像這樣:

create or replace function delete_expired_cache() 
returns void 
language sql 
as $$ 
    with delete_keys as (
    select key from cache 
    where 
     ttl is not null 
     and now()>ttl 
    for update skip locked 
) 
    delete from cache 
    where key in (select key from delete_keys); 
$$; 

SELECT value 
FROM cache 
WHERE 
    key = 'Ethan is testing6460437' 
    and (ttl is null or ttl<now()); 
select delete_expired_cache() where random()<0.001; 

您應該避免寫入,因爲它們很貴。不要經常刪除緩存。你


也應該使用timestamp with time zone型(或timestamptz的簡稱),而不是簡單的timestamp - 特別是如果你不知道爲什麼 - 一個timestamp是不是大多數人認爲它是事 - 怪SQL標準。

+0

謝謝 - 但我已經知道設計中的問題。我的問題不是如何修復設計,但爲什麼這種設計會導致死鎖? (另外,感謝關​​於timestamptz的提示,但是我們在我們的數據庫上設置了TIMEZONE TO UTC;我們的應用程序沒有真正公開除UTC以外的任何時間戳。) – Ethan