为在并发事务中执行的 INSERT 获取不同的行集

Get distinct sets of rows for an INSERT executed in concurrent transactions

我正在使用 Postgres 作为媒介实现一个简单的悲观锁定机制。目标是应用程序的多个实例可以同时获取不同用户组的锁。 应用程序实例不会尝试锁定特定用户。相反,他们将获取他们可以获得的任何用户锁。

例如,我们有应用程序的三个实例 运行,目前有 5 个用户未锁定。所有三个实例都尝试同时获取最多三个用户的锁。他们的请求以任意顺序提供。 理想情况下,服务的第一个实例将获得 3 个用户锁,第二个将获得 2 个,第三个将不获得任何锁。

到目前为止,我还没有能够编写完成此操作的查询。我会告诉你我迄今为止最好的尝试。

示例的表格如下:

CREATE TABLE my_locks (
  id                  bigserial       PRIMARY KEY,
  user_id             bigint          NOT NULL UNIQUE
);

CREATE TABLE my_users (
  id                  bigserial       PRIMARY KEY,
  user_name           varchar(50)     NOT NULL
);

这是获取锁的查询:

INSERT INTO my_locks(user_id)
  SELECT u.id
  FROM my_users AS u
  LEFT JOIN my_locks
    AS l
    ON u.id = l.user_id
  WHERE l.user_id IS NULL
  LIMIT 3
RETURNING *

我曾希望将可锁定用户的收集和将锁插入数据库合并到一个查询中,以确保多个并发请求将一个接一个地完整处理。

这样不行。如果应用于上面的示例,其中三个实例使用此查询同时获取 5 个用户的池上的锁,一个实例获取三个锁,而其他实例会收到一个错误,因为它试图插入具有非唯一用户 ID 的锁。

这并不理想,因为它会阻止锁定机制缩放。有许多解决方法可以解决这个问题,但我正在寻找的是数据库级解决方案。有没有一种方法可以调整查询或数据库配置,使多个应用程序实例可以(几乎)同时获取完全不同的集合中的最大可用锁数?

锁定子句 SKIP LOCKED 应该非常适合您。随 Postgres 9.5 添加。
The manual:

With SKIP LOCKED, any selected rows that cannot be immediately locked are skipped.

FOR NO KEY UPDATE 应该足以满足您的需求。 (仍然允许其他 non-exclusive 锁。)理想情况下,您使用足够坚固的最弱锁。

只使用锁

如果您可以在涉及用户的事务锁定保持打开状态的同时完成您的工作,那么这就是您所需要的:

BEGIN;

SELECT id FROM my_users
LIMIT  3
FOR    NO KEY UPDATE SKIP LOCKED;

-- do some work on selected users here  !!!

COMMIT;

沿途收集锁并一直保留到当前事务结束。虽然顺序可以是任意的,但我们甚至不需要 ORDER BY。没有等待,SKIP LOCKED 不可能出现死锁。每个事务都会扫描 table 并锁定前 3 行以供获取。 非常便宜和快速。

由于事务可能会保持打开状态一段时间,所以不要将任何其他内容放入同一事务中,以免阻止不必要的次数。

另外使用锁table

如果您无法在涉及用户的事务锁定保持打开状态时完成您的工作,请在该附加 table my_locks 中注册用户。

上班前:

INSERT INTO my_locks(user_id)
SELECT id FROM my_users u
WHERE  NOT EXISTS (
   SELECT FROM my_locks l
   WHERE  l.user_id = u.id
   )
LIMIT  3
FOR    NO KEY UPDATE SKIP LOCKED
RETRUNGING *;

不需要明确的事务包装器。

my_locks 中的用户除了当前被排它性锁定的用户外,也被排除在外。这在并发负载下有效。当每个事务打开时,锁处于活动状态。一旦这些在事务结束时被释放,它们就已经被写入锁 table - 并且同时对其他事务可见。

存在 理论上的竞争条件 并发语句尚未在锁 table 中看到新提交的行,并在刚刚释放锁后获取相同的用户.但是尝试写入锁 table 会失败。 UNIQUE 约束是绝对的,不允许重复条目,忽略可见性。

在从您的锁中删除之前,用户将不再符合条件 table。

进一步阅读:

旁白:

... multiple simultaneous requests would be processed in their entirety one after the other.

It doesn't work that way.

要了解它的实际工作原理,请阅读手册中有关 Postgres 的多版本并发控制 (MVCC) 的内容,starting here