在Postgresql中选择未锁定的行

时间:2008-12-23 17:34:34

标签: postgresql locking

有没有办法在Postgresql中选择未锁定的行?我有一个多线程应用程序可以:

Select... order by id desc limit 1 for update

在桌子上。

如果多个线程运行此查询,它们都会尝试拉回同一行。

一个获取行锁,另一个获取然后在第一个更新行后失败。我真正喜欢的是第二个线程获取与WHERE子句匹配且尚未锁定的第一行。

为了澄清,我希望每个线程在执行select之后立即更新第一个可用行。

因此,如果有ID: 1,2,3,4行,则第一个线程会进入,选择ID=4行并立即更新它。

如果在该事务期间第二个线程出现,我希望它与ID=3一起获取并立即更新该行。

对于共享,不会使用nowait完成此操作,因为WHERE子句将与锁定的行(ID=4 in my example)匹配。基本上我想要的是WHERE条款中的“AND NOT LOCKED”。

Users

-----------------------------------------
ID        | Name       |      flags
-----------------------------------------
1         |  bob       |        0
2         |  fred      |        1
3         |  tom       |        0
4         |  ed        |        0

如果查询是“Select ID from users where flags = 0 order by ID desc limit 1”,并且当返回一行时,接下来的事情是“Update Users set flags = 1 where ID = 0”,那么我希望第一个线程用ID 4抓取行然后下一个用ID 3抓住一行。

如果我将“For Update”附加到select,则第一个线程获取该行,第二个线程阻塞然后不返回任何内容,因为一旦第一个事务提交,WHERE子句就不再满足了。

如果我不使用“For Update”,那么我需要在后续更新中添加WHERE子句(WHERE flags = 0),因此只有一个线程可以更新该行。

第二个线程将选择与第一个相同的行,但第二个线程的更新将失败。

无论哪种方式,第二个线程都无法获得一行并进行更新,因为我无法让数据库将第4行提供给第一个线程,第3行提供给事务重叠的第二个线程。

14 个答案:

答案 0 :(得分:26)

此功能SELECT ... SKIP LOCKED正在Postgres 9.5中实施。 http://www.depesz.com/2014/10/10/waiting-for-9-5-implement-skip-locked-for-row-level-locks/

答案 1 :(得分:8)

否否NOOO: - )

我知道作者的意思。我有类似的情况,我想出了一个很好的解决方案。首先,我将从描述我的情况开始。我有一个表i,我存储了必须在特定时间发送的消息。 PG不支持函数的时序执行,因此我们必须使用守护进程(或cron)。我使用自定义编写的脚本打开几个并行进程。每个进程选择一组必须以+1秒/ -1秒的精度发送的消息。表本身是使用新消息动态更新的。

因此每个进程都需要下载一组行。这组行不能被其他进程下载,因为它会造成很多混乱(有些人会在只收到一个时收到几条消息)。这就是我们需要锁定行的原因。 使用锁定下载一组消息的查询:

FOR messages in select * from public.messages where sendTime >= CURRENT_TIMESTAMP - '1 SECOND'::INTERVAL AND sendTime <= CURRENT_TIMESTAMP + '1 SECOND'::INTERVAL AND sent is FALSE FOR UPDATE LOOP
-- DO SMTH
END LOOP;

此查询的过程每0.5秒启动一次。因此,这将导致下一个查询等待第一个锁解锁行。这种方法造成了巨大的延误即使我们使用NOWAIT,查询也会导致我们不想要的异常,因为表中可能存在必须发送的新消息。如果仅使用FOR SHARE,查询将正确执行,但仍然需要花费大量时间来创建大量延迟。

为了使它成功,我们做了一点魔术:

  1. 更改查询:

    FOR messages in select * from public.messages where sendTime >= CURRENT_TIMESTAMP - '1 SECOND'::INTERVAL AND sendTime <= CURRENT_TIMESTAMP + '1 SECOND'::INTERVAL AND sent is FALSE AND is_locked(msg_id) IS FALSE FOR SHARE LOOP
    -- DO SMTH
    END LOOP;
    
  2. 神秘的函数'is_locked(msg_id)'看起来像这样:

    CREATE OR REPLACE FUNCTION is_locked(integer) RETURNS BOOLEAN AS $$
    DECLARE
        id integer;
        checkout_id integer;
        is_it boolean;
    BEGIN
        checkout_id := $1;
        is_it := FALSE;
    
        BEGIN
            -- we use FOR UPDATE to attempt a lock and NOWAIT to get the error immediately 
            id := msg_id FROM public.messages WHERE msg_id = checkout_id FOR UPDATE NOWAIT;
            EXCEPTION
                WHEN lock_not_available THEN
                    is_it := TRUE;
        END;
    
        RETURN is_it;
    
    END;
    $$ LANGUAGE 'plpgsql' VOLATILE COST 100;
    
  3. 当然,我们可以自定义此功能,以便在数据库中的任何表上工作。在我看来,最好为一个表创建一个检查功能。向此函数添加更多内容可能会使其变慢。无论如何我需要更长时间来检查这个子句,所以不需要让它更慢。对我来说,这是一个完整的解决方案,它可以很好地工作。

    现在,当我的50个进程并行运行时,每个进程都有一组唯一的新消息要发送。一旦发送,我只需更新send = TRUE的行,再也不会再回到它。

    我希望这个解决方案也适合你(作者)。如果您有任何疑问,请告诉我: - )

    哦,请告诉我这是否也适合你。

答案 2 :(得分:6)

我使用这样的东西:

select  *
into l_sms
from sms
where prefix_id = l_prefix_id
    and invoice_id is null
    and pg_try_advisory_lock(sms_id)
order by suffix
limit 1;

并且不要忘记拨打pg_advisory_unlock

答案 3 :(得分:4)

如果您正在尝试实现队列,请查看PGQ,它已经解决了这个问题和其他问题。 http://wiki.postgresql.org/wiki/PGQ_Tutorial

答案 4 :(得分:2)

您似乎正在尝试执行某项操作,例如获取尚未由其他进程处理的队列中的最高优先级项目。

一个可能的解决方案是添加一个where子句,将其限制为未处理的请求:

select * from queue where flag=0 order by id desc for update;
update queue set flag=1 where id=:id;
--if you really want the lock:
select * from queue where id=:id for update;
...

希望第二个事务会在对标志的更新发生时阻塞,然后它将能够继续,但是该标志会将它限制在下一行。

使用可序列化的隔离级别也可能会得到您想要的结果,而不会产生所有这些精神错乱。

根据应用程序的性质,可能有比在数据库中更好的方法来实现它,例如FIFO或LIFO管道。此外,可以颠倒您需要它们的顺序,并使用序列来确保它们按顺序处理。

答案 5 :(得分:1)

这可以通过SELECT ... NOWAIT来完成;一个例子是here

答案 6 :(得分:0)

看起来你正在寻找一个选择分享。

http://www.postgresql.org/docs/8.3/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE

FOR SHARE的行为类似,只是它在每个检索到的行上获取共享锁而不是独占锁。共享锁阻止其他事务对这些行执行UPDATE,DELETE或SELECT FOR UPDATE,但它不会阻止它们执行SELECT FOR SHARE。

如果在FOR UPDATE或FOR SHARE中命名了特定的表,则只锁定来自这些表的行; SELECT中使用的任何其他表都像往常一样阅读。没有表列表的FOR UPDATE或FOR SHARE子句会影响命令中使用的所有表。如果FOR UPDATE或FOR SHARE应用于视图或子查询,则会影响视图或子查询中使用的所有表。

如果需要为不同的表指定不同的锁定行为,则可以编写多个FOR UPDATE和FOR SHARE子句。如果FOR UPDATE和FOR SHARE子句都提到(或隐式地)影响了相同的表,那么它将被处理为FOR UPDATE。类似地,如果在影响它的任何子句中指定了表,则将表处理为NOWAIT。

FOR UPDATE和FOR SHARE不能用于无法用单个表行明确标识返回行的上下文中;例如,它们不能与聚合一起使用。

答案 7 :(得分:0)

你想要完成什么?你能更好地解释为什么解锁行更新或完整交易都不能满足你的需要吗?

更好的是,你可以防止争用并且只是让每个线程使用不同的偏移吗?如果经常更新表的相关部分,这将无法正常工作;你仍然会发生碰撞,但只有在重载时才会发生碰撞。

Select... order by id desc offset THREAD_NUMBER limit 1 for update

答案 8 :(得分:0)

由于我还没有找到更好的答案,我决定在我的应用程序中使用锁定来同步对执行此查询的代码的访问。

答案 9 :(得分:0)

以下怎么样? 可能可以比其他示例更原子地对待,但应该仍然进行测试,以确保我的假设没有错误。

UPDATE users SET flags = 1 WHERE id = ( SELECT id FROM users WHERE flags = 0 ORDER BY id DESC LIMIT 1 ) RETURNING ...;

你可能仍然会遇到postgres内部使用的锁定方案,以便在同时更新UPDATE时提供一致的SELECT结果。

答案 10 :(得分:0)

我在申请中遇到了同样的问题,并提出了与Grant Johnson的方法非常相似的解决方案。 FIFO或LIFO管道不是一个选项,因为我们有一个应用程序服务器集群访问一个DB。我们所做的是

SELECT ... WHERE FLAG=0 ... FOR UPDATE
,然后立即紧跟
UPDATE ... SET FLAG=1 WHERE ID=:id
,以便尽可能降低锁定时间。根据表的列数和大小,只有在第一个选择中获取ID并且一旦标记了行以获取剩余数据,它可能会有所帮助。存储过程可以进一步减少往返次数。

答案 11 :(得分:0)

^^有效。考虑具有“锁定”的“立即”状态。

假设您的表格是这样的:

id |名字|姓氏|状态

例如,可能的状态是:1 =待定,2 =已锁定,3 =已处理,4 =已失败,5 =已拒绝

每个新记录都会插入状态待定(1)

您的程序执行:“更新mytable set status = 2,其中id =(从mytable中选择id,其中名称如'%John%'和status = 1 limit 1)返回id,name,surname”

然后你的程序做了它的事情,如果它得出的结论是这个线程根本不应该处理那一行,它确实: “更新mytable set status = 1,其中id =?”

其他更新其他状态。

答案 12 :(得分:0)

我的解决方案是将UPDATE语句与RETURNING子句一起使用。

Users

-----------------------------------
ID        | Name       |      flags
-----------------------------------
1         |  bob       |        0  
2         |  fred      |        1  
3         |  tom       |        0   
4         |  ed        |        0   

而不是SELECT .. FOR UPDATE使用

BEGIN; 

UPDATE "Users"
SET ...
WHERE ...;
RETURNING ( column list );

COMMIT;

因为UPDATE语句在表上获得ROW EXCLUSIVE锁,所以它会更新序列化更新。仍然允许读取,但它们只在UPDATE事务开始之前看到数据。

参考:Concurrency Control Pg文档章节。

答案 13 :(得分:0)

用于多线程和集群?
这个怎么样?

START TRANSACTION;

// All thread retrive same task list
// If result count is very big, using cursor 
//    or callback interface provied by ORM frameworks.
var ids = SELECT id FROM tableName WHERE k1=v1;

// Each thread get an unlocked recored to process.
for ( id in ids ) {
   var rec = SELECT ... FROM tableName WHERE id =#id# FOR UPDATE NOWAIT;
   if ( rec != null ) {
    ... // do something
   }
}

COMMIT;