锁定PostgreSQL表中的所有行

时间:2019-02-26 10:22:46

标签: postgresql locking

我正在尝试在PG表中创建某种任务队列,类似于https://www.pgcon.org/2016/schedule/attachments/414_queues-pgcon-2016.pdf,但要复杂一些。

1)有些任务与某个entity_id相关联,并且当它们的entity_id不同时,它们可以并行执行。因此,对于他们来说,有一张桌子:

create table entity_tasks (
  entity_id bigint,
  task text,
  inserted_at timestamp default now()
);

2)有些任务必须排他地执行,即与所有其他任务顺序执行。对于此类任务,也有一个表:

create table block_everything_tasks (
  task TEXT,
  inserted_at TIMESTAMP DEFAULT NOW()
);

block_everything_tasks执行任务应阻止entity_tasksblock_everything_tasks中所有任务的执行。

在进行了原型制作后,我还添加了一张桌子

create table entities_for_tasks (
  entity_id bigint primary key
);

每个实体获取和执行任务的过程如下:

begin;
    select entity_id into entity_to_lock
    from entities_for_tasks
    for update skip locked
    limit 1;

    select * from entity_tasks
    where entity_id = entity_to_lock
    order by inserted_at
    limit 1;

    -- execute them and delete from the `entity_tasks`
commit;

到目前为止,一切都很好,但是当我尝试从block_everything_tasks实现获取任务时,它变得很尴尬。我在这里看到了一些解决方案,但不喜欢其中任何一个。

1)我可以像这样显式锁定整个entity_to_lock

begin;
    lock table entity_to_lock;

    select * from block_everything_tasks
    order by inserted_at
    limit 1;

    -- execute them and delete from the `entity_tasks`
commit;

但这会阻止向任务entity_to_lock添加行,并可能阻止向队列之一添加任务。

2)或者我可以尝试做类似的事情

begin;
    with lock as (
      select * from entity_to_lock for update
    )
    select * from block_everything_tasks
    order by inserted_at
    for update skip locked
    limit 1;

    -- execute them and delete from the `entity_tasks`
commit;

这看起来像是一种不错的解决方案,我不会阻止提交者,并且entity_to_lock也不算太大,但是我不会使用entity_to_lock中的行并且它们没有被锁定,所以就行不通了。

所以我的问题是

  • 在选项(1)中是否有方法可以锁定entity_to_lock表 以便仍然可以插入并且select * from entity_to_lock where ... for update将被锁定?
  • 或者是否有方法可以锁定选项(2)中的所有行而不实际消耗这些行?
  • 还是应该在这里提出其他建议?

1 个答案:

答案 0 :(得分:1)

INSERTUPDATE都获得了ROW EXCLUSIVE锁,因此您将找不到任何表级锁,但不排除另一个。

您可以使用SELECT FOR UPDATE锁定所有现有行以防止更改,但不会同时影响INSERT版记录,因此它们仍会被拾取并已处理,无论当前正在运行什么任务。

entities_for_tasks表与entity_tasks保持同步也可能会出现问题,具体取决于您如何填充它以及所使用的isolation level。这种模式在SERIALIZABLE以下的任何位置都容易出现竞争状况。


退一步,您确实要解决两个不同的问题:创建和分配任务,以及协调任务的执行。第一个问题可以通过基本排队机制很好地解决,但是尝试通过重载来解决第二个问题似乎是所有这些冲突的根源。

因此,请别管队列,考虑一下协调任务执行所需的实际条件:

  1. 显示“任务正在运行”的锁
  2. 一组锁,上面写着“一个任务正在针对实体x运行”

...其中block_everything_tasks中的任务需要对(1)的排他锁,而entity_tasks中的任务可以相互共享对(1)的锁,但需要对(2)。

实现此目的最明确的方法是通过advisory locks,它使您可以“锁定”具有某些特定于应用程序含义的任意整数。

假设没有实体具有ID 0,那么我们将其用于顶级“任务正在运行”锁。然后,在成功从队列中提取任务之后,每个排他任务将运行:

SELECT pg_advisory_xact_lock(0);

...并且每个实体任务将运行:

SELECT pg_advisory_xact_lock_shared(0);
SELECT pg_advisory_xact_lock(<entity_id of selected task>);

咨询锁定的主要问题是数据库的每个用户都需要就这些整数的含义达成一致,否则,他们可能最终出于同一目的而争用同一锁定,而这无关紧要。锁定函数的两个参数(int,int)重载使您可以将锁定的范围限制为特定的用例,但是当ID为bigint时,这并没有太大帮助。

如果不能确定您是使用咨询锁的数据库中唯一的数据库,则可以使用基于表的方法来模拟。设置表格:

CREATE_TABLE currently_processing (
  entity_id bigint PRIMARY KEY
);

...然后执行排他任务:

LOCK currently_processing;

...以及针对实体任务:

INSERT INTO currently_processing VALUES (<entity_id of selected task>);
<run the task>
DELETE FROM currently_processing WHERE entity_id = <entity_id of selected task>;

INSERT会尝试在表上获取共享锁(被排他任务阻止),而PRIMARY KEY上的唯一索引将导致表的并发INSERT要阻止相同的ID,直到冲突的事务提交或回滚为止。