【发布时间】: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 位置来通知他们。
服务仅限于给定数量的用户。系统如何决定哪个用户获得服务有多种选择。最简单的是先到先得。这就是它有两个阶段的原因(但它不应该以任何方式改变用例)。
可能的要求是:
- 插入:用户申请/注册服务
- 正在插入(
user_id、service_id、CURRENT TIMESTAMP、state: 0 (new)) - 根据具有相同 service_id 的 COUNT(*),将状态更新为 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