我正在尝试实施任务分配系统。用户可以从池中请求任务。即使设置为SERIALIZABLE,事务有时也会向多个用户提供相同的任务,即使不应该这样做。
CREATE TABLE tasks(
_id CHAR(24) PRIMARY KEY,
totalInstances BIGINT NOT NULL
);
CREATE TABLE assigned(
_id CHAR(24) PRIMARY KEY,
_task CHAR(24) NOT NULL
);
任务表中填充了很多行,假设每个行都有totalInstances = 1
,这意味着每个任务最多应分配一次。
assigned
中添加行的查询:WITH task_instances AS (
SELECT t._id, t.totalInstances - COUNT(assigned._id) openInstances
FROM tasks t
LEFT JOIN assigned ON t._id = assigned._task
GROUP BY t._id, t.totalInstances
),
selected_task AS (
SELECT _id
FROM task_instances
WHERE openInstances > 0
LIMIT 1
)
INSERT INTO assigned(_id, _task)
SELECT $1, _id
FROM selected_task;
$1
是传递给每个查询的随机ID。
我们有大约100名活跃用户请求任务。这可以按预期工作,除了1000个请求中的一次。
然后,在并行请求时为相同的 assigned
id创建两个_task
行。我期望一个可序列化的执行回滚第二个,因为openInstances应该被第一个减少到0。
我们使用Postgres 10.3,查询通过带有withTransactionIsolation(Serializable)
的Slick 3.2.3从Scala代码运行。没有其他查询从assigned
表中删除或插入。
Postgres日志显示请求在不同的会话中运行,并且SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE;
在每个任务分配查询之前执行。
我尝试用不同的样式重写查询,包括VIEW
子查询的WITH
s用法,并用BEGIN
和COMMIT
包围查询但是没有效果。
感谢任何帮助。
我应该补充说,有时会出现预期的序列化错误/回滚做,我们的应用程序会重试查询。我在最后几小时的日志中看到了这种正确的行为10次,但仍然错误地将其分配了两次相同的任务,如上所述。
答案 0 :(得分:1)
我尝试了这样的例子:
第1节:
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
WITH task_instances AS (
SELECT t._id, t.totalInstances - COUNT(assigned._id) openInstances
FROM tasks t
LEFT JOIN assigned ON t._id = assigned._task
GROUP BY t._id, t.totalInstances
),
selected_task AS (
SELECT _id
FROM task_instances
WHERE openInstances > 0
LIMIT 1
)
INSERT INTO assigned(_id, _task)
SELECT 1, _id
FROM selected_task;
第二节:
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
WITH task_instances AS (
SELECT t._id, t.totalInstances - COUNT(assigned._id) openInstances
FROM tasks t
LEFT JOIN assigned ON t._id = assigned._task
GROUP BY t._id, t.totalInstances
),
selected_task AS (
SELECT _id
FROM task_instances
WHERE openInstances > 0
LIMIT 1
)
INSERT INTO assigned(_id, _task)
SELECT 2, _id
FROM selected_task;
COMMIT;
第1节:
COMMIT;
这就是我得到的:
ERROR: could not serialize access due to read/write dependencies among transactions
DETAIL: Reason code: Canceled on identification as a pivot, during commit attempt.
HINT: The transaction might succeed if retried.
所以它按预期工作。
我唯一的解释是你的设置有问题,而你毕竟没有使用SERIALIZABLE
。
您是否在应用程序中看到序列化错误?如果没有,那将证实我的怀疑。
答案 1 :(得分:0)
Serializable隔离级别并不意味着事务是字面上的串行。它只保证读取已提交的,可重复的读取和没有幻像读取。而你所描述的行为并不构成违规行为。
为避免重复记录,您只需执行
即可select ... from task_instances for update
由于这个"更新"子句所选行将被锁定以用于事务生存期。因此,只有一个事务可以更新,第二个事务必须等到第一个事务提交。因此,第二笔交易将读取第一笔交易更新的价值 - 这是您在此需要的保证。
同样重要的是,如果您使用"选择更新"在这种情况下,你甚至不需要Serializable隔离级别,read committed就足够了。