Postgresql Serializable Transaction无法按预期工作

时间:2018-04-25 10:50:50

标签: sql postgresql concurrency transactions serializable

我正在尝试实施任务分配系统。用户可以从池中请求任务。即使设置为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用法,并用BEGINCOMMIT包围查询但是没有效果。

感谢任何帮助。

修改

我应该补充说,有时会出现预期的序列化错误/回滚,我们的应用程序会重试查询。我在最后几小时的日志中看到了这种正确的行为10次,但仍然错误地将其分配了两次相同的任务,如上所述。

2 个答案:

答案 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就足够了。