我怎样才能避免这种竞争条件?

ldr*_*drg 3 python sql postgresql synchronization race-condition

我有一个分布式任务队列,其任务如下所示:

# creates a uniquely-named file
new_path = do_work()

old_path = database.query('select old path')
unlink(old_path)

database.query('insert new path')
Run Code Online (Sandbox Code Playgroud)

这里有一个竞争条件:如果任务队列软件在同一时间引发其中两个任务,它们将从old_path数据库中获得相同的数据,并且竞赛失败者的unlink调用失败(孤立失败者的新路径从未来取消链接).

有没有办法构建这个来绕过这场比赛?如果需要,我可以从当前的设计中抛弃任何东西.具体来说,我正在使用PostgreSQL,Python和Celery.我知道我可以使用表格范围的锁定/更改psycopg2的事务级别到SERIALIZABLE,但我不确定是否可以避免这种竞争条件.表级锁定也意味着我必须为每个附加任务引入一个新表(因为没有它们相互阻塞),这听起来不太吸引人.

Cra*_*ger 5

我强烈建议你研究已经解决了这个问题的工具,比如PGQ.排队比你想象的要困难.这不是你要重新发明的轮子.

并发很难

Mihai的回答看起来很好,但在并发操作中有所下降.

两个并发UPDATE可以选择相同的行(在他的示例中有used_flag = FALSE).其中一个将获得锁定并继续.另一个将等到第一次运行并提交.当提交发生时,第二次更新将获得锁定,重新检查其条件,找不到任何行匹配,并且什么都不做.因此,实际上很可能 - 除了一组并发更新之外的所有更新都返回空集.

READ COMMITTED模式中,您仍然可以获得不错的结果,大致相当于UPDATE连续循环的单个会话.在SERIALIZABLE模式中,它将无可救药地失败.试试吧; 这是设置:

CREATE TABLE paths (
    used_flag boolean not null default 'f',
    when_entered timestamptz not null default current_timestamp,
    data text not null
);

INSERT INTO paths (data) VALUES
('aa'),('bb'),('cc'),('dd');
Run Code Online (Sandbox Code Playgroud)

这是演示.尝试使用三个并发会话,一步一步地进行.在READ COMMITTED中执行一次,然后使用所有会话再次SERIALIZABLE使用BEGIN ISOLATION LEVEL SERIALIZABLE而不是plain BEGIN.比较结果.

SESSION 1             SESSION2         SESSION 3

BEGIN;
                                       BEGIN;

UPDATE      paths
    SET     used_flag = TRUE
    WHERE   used_flag = FALSE
    RETURNING data;

                      BEGIN;

                      INSERT INTO
                      paths(data)
                      VALUES
                      ('ee'),('ff');      

                      COMMIT;               
                                       UPDATE      paths
                                           SET     used_flag = TRUE
                                           WHERE   used_flag = FALSE
                                           RETURNING data;


                      BEGIN;

                      INSERT INTO
                      paths(data)
                      VALUES
                      ('gg'),('hh');      

                      COMMIT;        

COMMIT;
Run Code Online (Sandbox Code Playgroud)

READ COMMITTED第一个UPDATE成功并生成四行.第二个产生剩下的两个ee,ff并在第一次更新运行后插入并提交.gg并且hh不会被第二次更新返回,即使它在提交后实际执行,因为它已经选择了它的行并且在它们被插入时等待锁定.

SERIALIZABLE隔离第一更新成功并产生四行.第二个失败了ERROR: could not serialize access due to concurrent update.在这种情况下,SERIALIZABLE隔离不会帮助你,它只会改变失败的性质.

没有显式事务,当UPDATE并发运行时会发生同样的事情.如果您使用显式事务,那么在不摆弄时间的情况下演示会更容易.

选择一行怎么样?

如上所述系统工作正常,但如果你想只获得最老的行怎么办?因为UPDATE在阻塞锁之前选择它将要操作的行,你会发现在任何给定的事务集中只有一个UPDATE会返回一个结果.

你会想到这样的技巧:

UPDATE      paths
    SET     used_flag = TRUE
    WHERE entry_id = (
        SELECT entry_id
        FROM paths 
        WHERE used_flag = FALSE
        ORDER BY when_entered
        LIMIT 1
    )
    AND used_flag = FALSE
    RETURNING data;
Run Code Online (Sandbox Code Playgroud)

要么

UPDATE      paths
    SET     used_flag = TRUE
    WHERE entry_id = (
        SELECT min(entry_id)
        FROM paths 
        WHERE used_flag = FALSE
    )
    AND used_flag = FALSE
    RETURNING data;
Run Code Online (Sandbox Code Playgroud)

但这些不符合您的预期; 当并发运行时,两者都将选择相同的目标行.一个将继续,一个将阻止锁定直到第一个提交,然后继续并返回一个空的结果.没有第二个AND used_flag = FALSE我认为他们甚至可以返回重复!将entry_id SERIAL PRIMARY KEY列添加到paths上面的演示表后尝试使用它.让他们参加比赛,就LOCK TABLE paths在第三场比赛中; 请参阅以下链接中给出的示例.

在另一个答案中写了这些问题,在我的回答中,多个线程可以在约束集上引起重复更新.

说真的,去看看PGQ.它已经为你解决了这个问题.