【问题标题】:waitlist in postgres, deadlockpostgres中的候补名单,死锁
【发布时间】:2021-10-18 15:34:45
【问题描述】:

我正在尝试在 Postgres 中创建等候名单。最小代码:

CREATE TABLE IF NOT EXISTS applies(
    created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    user_id SMALLINT REFERENCES users(id) ON DELETE CASCADE,
    service_id SMALLINT REFERENCES services(id) ON DELETE CASCADE,
    status SMALLINT DEFAULT 0, --0: new 1: waitlisted, 2: applied
    PRIMARY KEY (user_id, service_id)
    -- ...
);

CREATE INDEX ON applies (service_id);

状态很重要,因为我希望能够在用户被列入候补名单后通知他们。但是,如果他们根本没有被列入候补名单,则不想根据他们处于第一个 n 位置来通知他们。 服务仅限于给定数量的用户。系统如何决定哪个用户获得服务有多种选择。最简单的是先到先得。这就是它有两个阶段的原因(但它不应该以任何方式改变用例)。

可能的要求是:

  1. 插入:用户申请/注册服务
    1. 正在插入(user_id、service_id、CURRENT TIMESTAMP、state: 0 (new))
    2. 根据具有相同 service_id 的 COUNT(*),将状态更新为 1(已列入候补名单)或 2(已申请)
  2. 删除:用户取消他/她的服务申请
    1. 删除给定的应用程序
    2. 如果有人处于候补状态,请转到已申请并发送通知

我的第一次尝试是一个幼稚的实现。假设服务限制为 10。

1/2 OnAdd:

UPDATE applies
SET status = (
    SELECT CASE WHEN COUNT(*) <= 10 THEN 2 ELSE 1 END
    FROM applies
    WHERE service_id = 7918
    AND created <= '2021-08-16 16:20:34.161274+00:00:00'
)
WHERE user_id = 5070
AND service_id = 7918
RETURNING status

2/2 OnRemove:

SELECT user_id
FROM applies
WHERE status = 1
AND service_id = 7915
ORDER BY created
LIMIT 1

然后(我知道他们可以加入)

UPDATE applies
SET status = 2
WHERE status = 1
AND user_id = 5063
AND service_id = 7915

它适用于顺序测试,但多线程测试显示应用状态超过 10 行的情况。

所以我把它们放在以SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 开头的事务中,然后是REPEATABLE READ,它们给了我很多ERROR #40001 could not serialize access due to concurrent update。然后与READ COMMITTED 相同。它比天真的版本要好得多,但也以过度应用而告终。

然后我开始在选择中使用FOR NO KEY UPDATE,但它们总是很快就陷入僵局。我在死锁上搜索了很多,但找不到任何有用的东西。

所以我想出了一个版本,其中 OnAdd 和 OnRemove 的查询非常相似,只是在选择 user_id 上有所不同,而我没有添加 FOR UPDATE。我不得不更改 1/1 插入,所以默认状态是候补名单。

添加:

UPDATE applies
SET status = 2
WHERE service_id = 7860
AND 10 > (
    SELECT COUNT(*)
    FROM (
        SELECT service_id, user_id
        FROM applies
        WHERE service_id = 7860
        AND status = 2
        FOR NO KEY UPDATE
    ) as newstate)
AND user_id = 5012 RETURNING status

删除:

UPDATE applies
SET status = 2
WHERE service_id = 7863
AND 10 > (
    SELECT COUNT(*)
    FROM (
        SELECT service_id, user_id
        FROM applies
        WHERE service_id = 7863
        AND status = 2
        FOR NO KEY UPDATE
    ) as newstate)
AND user_id = (
    SELECT user_id
    FROM applies
    WHERE service_id = 7863
    And status = 1
    ORDER BY created
    LIMIT 1
)
RETURNING user_id

但是在多线程测试中它也陷入了死锁。

编辑:

正如下面melcher 建议的那样,我添加了一个列而不是计数。不是单独的表,而是在services 内。

我向 OnAdd 和 OnRemove 添加了一个以

开头的事务
SELECT *
FROM services
WHERE id = ?
FOR NO KEY UPDATE

有时在多线程测试中应用不足。所以我加入了 Remove 以与 OnRemove 进行同一事务,并且最终成功了。

【问题讨论】:

    标签: sql postgresql locking deadlock


    【解决方案1】:

    根据我对您正在尝试做什么以及数据库如何工作的理解 - 您将需要一个在 OnAdd 时被锁定的共享资源。

    原因是同时尝试“添加”的两个线程必须竞争共享资源,以便只有一个线程获胜,而另一个错误/失败。您无法使用行数来实现目标。

    一种解决方案是锁表:

    CREATE TABLE IF NOT EXISTS applies(
        service_id SMALLINT REFERENCES services(id) ON DELETE CASCADE,
        applied_count SMALLINT
    );
    

    然后:

    1. 打开事务(如果在过程之外)
    2. 获取锁表中服务行的排他/写锁
    3. 如果满足限制/约束(例如,applied_count
    4. 将“应用”标记为已应用UPDATE applies SET status = 2
    5. 更新锁表(例如SET applied_count = applied_count + 1)
    6. 提交事务

    【讨论】:

    • 我没有使用咨询锁,但它们看起来很棒!看起来他们也更有表现力。它们只能用数字来识别吗?用于不同的用例并且无法命名它们似乎有点令人沮丧。
    • 抱歉,我对 Postgres 没有太多经验,而且我自己也从未使用过咨询锁。我在 MS SQL Server 中只使用了sp_getapplock,它们的名称长度为 255 个字符。直到今天当我搜索“postgres sp_getapplock”时,我才了解到 Postgres 中有“咨询锁”之类的东西。我之所以推荐它们是因为它们是专门用于处理并发的引擎中内置的,并且在某些情况下可能比定制的锁表更好。
    • 我认为它只是添加了一个新层,这将有同样的问题。 applied_count 需要读写,就像应用行一样。 OnAdd 会增加它,OnRemove 会选择,然后保持或减少。如果您可以多指定一点,使用精确的 OnAdd 和 OnRemove 查询,我可以试一试。
    • 您遇到的问题是并发问题。查询的并发执行都将通过计数检查,然后都更新状态,导致您超出所需的限制。因此,在运行检查/更新状态时,您需要获取共享资源的独占锁。不确定我今天是否有时间提供示例查询/程序,可能在本周末。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-04
    • 2020-03-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多