postgres中的候补名单,僵局
waitlist in postgres, deadlock
我正在尝试在 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
位置来通知他们。
服务仅限于给定数量的用户。系统如何决定哪个用户获得服务有多种选择。最简单的是先到先得。这就是它有两个阶段的原因(但它不应该以任何方式改变用例)。
可能的要求是:
- 插入:用户 applies/enrolls 服务
- 正在插入(
user_id
、service_id
、CURRENT TIMESTAMP
、state: 0 (new)
)
- a 根据具有相同 service_id
的 COUNT(*) 将状态更新为 1(等待)或 2(已申请)
- 删除:用户取消his/her服务申请
- 正在删除给定的应用程序
- 如果有人处于候补状态,请移至已申请,并发送相关通知
我的第一次尝试是一个天真的实现。假设服务限制为 10。
1/2 添加:
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
相同。它比原始版本好多了,但它也以过度应用而告终。
然后我开始在 selects 中使用 FOR NO KEY UPDATE
,但它们总是很快就陷入僵局。我在死锁上搜索了很多,但找不到任何有用的东西。
所以我想出了一个版本,其中 OnAdd 和 OnRemove 有非常相似的查询,只是选择 user_id
不同,而我没有添加 FOR UPDATE
。我不得不更改 1/1 Insert,因此默认状态已列入候补名单。
添加时:
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
但是在多线程测试中也死锁了
编辑:
按照下面 的建议,我添加了一列而不是计数。不是单独的 table,而是 services
.
里面
我向 OnAdd 和 OnRemove 添加了一个以
开头的事务
SELECT *
FROM services
WHERE id = ?
FOR NO KEY UPDATE
有时在多线程测试中应用不足。所以我加入 Remove 与 OnRemove 在同一个事务中,终于成功了。
根据我对您尝试执行的操作以及数据库工作方式的理解 - 您将需要一个已锁定 OnAdd 的共享资源。
原因是两个同时尝试 'add' 的线程必须竞争共享资源,这样只有一个线程获胜而其他线程出错/失败。您无法使用行数来实现您的目标。
一个解决方案是锁 table:
CREATE TABLE IF NOT EXISTS applies(
service_id SMALLINT REFERENCES services(id) ON DELETE CASCADE,
applied_count SMALLINT
);
然后:
- 打开事务(如果在过程之外)
- 获取 exclusive/write 锁到锁中的服务行 table
- 如果 limit/constraint 满足(例如 applied_count < 10)则...
- 将“应用”标记为已应用
UPDATE applies SET status = 2
- 更新锁定table(例如
SET applied_count = applied_count + 1
)
- 提交交易
我正在尝试在 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
位置来通知他们。
服务仅限于给定数量的用户。系统如何决定哪个用户获得服务有多种选择。最简单的是先到先得。这就是它有两个阶段的原因(但它不应该以任何方式改变用例)。
可能的要求是:
- 插入:用户 applies/enrolls 服务
- 正在插入(
user_id
、service_id
、CURRENT TIMESTAMP
、state: 0 (new)
) - a 根据具有相同 service_id 的 COUNT(*) 将状态更新为 1(等待)或 2(已申请)
- 正在插入(
- 删除:用户取消his/her服务申请
- 正在删除给定的应用程序
- 如果有人处于候补状态,请移至已申请,并发送相关通知
我的第一次尝试是一个天真的实现。假设服务限制为 10。
1/2 添加:
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
相同。它比原始版本好多了,但它也以过度应用而告终。
然后我开始在 selects 中使用 FOR NO KEY UPDATE
,但它们总是很快就陷入僵局。我在死锁上搜索了很多,但找不到任何有用的东西。
所以我想出了一个版本,其中 OnAdd 和 OnRemove 有非常相似的查询,只是选择 user_id
不同,而我没有添加 FOR UPDATE
。我不得不更改 1/1 Insert,因此默认状态已列入候补名单。
添加时:
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
但是在多线程测试中也死锁了
编辑:
按照下面 services
.
我向 OnAdd 和 OnRemove 添加了一个以
开头的事务SELECT *
FROM services
WHERE id = ?
FOR NO KEY UPDATE
有时在多线程测试中应用不足。所以我加入 Remove 与 OnRemove 在同一个事务中,终于成功了。
根据我对您尝试执行的操作以及数据库工作方式的理解 - 您将需要一个已锁定 OnAdd 的共享资源。
原因是两个同时尝试 'add' 的线程必须竞争共享资源,这样只有一个线程获胜而其他线程出错/失败。您无法使用行数来实现您的目标。
一个解决方案是锁 table:
CREATE TABLE IF NOT EXISTS applies(
service_id SMALLINT REFERENCES services(id) ON DELETE CASCADE,
applied_count SMALLINT
);
然后:
- 打开事务(如果在过程之外)
- 获取 exclusive/write 锁到锁中的服务行 table
- 如果 limit/constraint 满足(例如 applied_count < 10)则...
- 将“应用”标记为已应用
UPDATE applies SET status = 2
- 更新锁定table(例如
SET applied_count = applied_count + 1
) - 提交交易