Сериализуемая транзакция Postgresql не работает должным образом - PullRequest
0 голосов
/ 25 апреля 2018

Я пытаюсь внедрить систему назначения задач . Пользователи могут запрашивать задачи из пула. Даже если для параметра 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 - случайный идентификатор, передаваемый каждому запросу.

Симптомы

У нас около 100 активных пользователей, регулярно запрашивающих задания. Это работает, как и ожидалось, за исключением, может быть, один раз в 1000 запросов. Затем две assigned строки создаются для одного и того же _task id при параллельных запросах. Я ожидал бы, что сериализуемое выполнение откатит второе, так как openInstances должен был быть уменьшен до 0 первым.

Настройка

Мы используем Postgres 10.3, и запрос запускается из кода Scala через Slick 3.2.3 с withTransactionIsolation(Serializable). Никакие другие запросы не удаляются и не вставляются в таблицу assigned.

Журналы Postgres показывают, что запросы выполняются в разных сеансах и что SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE; выполняется перед каждым запросом назначения задачи.

Я попытался переписать запрос в разных стилях, включая VIEW s для подзапросов WITH и окружив запрос BEGIN и COMMIT, но безрезультатно.

Любая помощь приветствуется.

Редактировать

Я должен добавить, что иногда возникают ожидаемые ошибки / откаты сериализации do , после чего наше приложение повторяет запрос. Я вижу это правильное поведение 10 раз в журналах последних часов, но 2 раза оно по-прежнему ошибочно назначало одну и ту же задачу дважды, как описано выше.

Ответы [ 2 ]

0 голосов
/ 26 апреля 2018

Я попробовал ваш пример следующим образом:

Сессия 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;

Сессия 2:

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.

Вы когда-нибудь видели ошибки сериализации в вашем приложении?Если нет, это подтвердит мое подозрение.

0 голосов
/ 25 апреля 2018

Сериализуемый уровень изоляции не означает, что транзакции являются буквально последовательными.Это только гарантирует чтение совершенных, повторяющихся чтений и отсутствие фантомных чтений.И поведение, которое вы описали, не выглядит как нарушение.

Чтобы избежать дублирования записей, вы можете просто сделать

select ... from task_instances for update

Из-за этого условия «для обновления» выбранная строка будет заблокирована на время существования транзакции.Таким образом, только одна транзакция сможет обновить, а вторая должна будет ждать, пока первая не будет зафиксирована.В результате вторая транзакция будет считывать значение, обновленное первой, и это правильная гарантия, которая вам нужна.

Что также важно, если вы используете «выбрать для обновления» в таком сценарии, вы ненужен даже уровень изоляции Serializable, для чтения будет достаточно.

...