2017-08-10 46 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