Уровень изоляции и явная блокировка: непредвиденная ошибка сериализации - PullRequest
0 голосов
/ 14 января 2019

Я пишу веб-приложение, и я экспериментировал с переносом операторов SQL из каждого веб-запроса с транзакцией с ISOLATION LEVEL REPEATABLE READ, чтобы определить, где мое веб-приложение может выполнять неповторяемые операции чтения. Мой план состоял не в том, чтобы повторить попытку в случае неповторяемого чтения, а просто сообщить пользователю об ошибке на стороне сервера (500) и записать информацию (так как я ожидаю, что это будет очень редко).

В то же время в моем коде есть места, где я использую явную блокировку (SELECT ... FOR UPDATE), чтобы убедиться, что я правильно сериализовал доступ и не вызывал неповторимых чтений.

Однако объединение двух идей дает мне неожиданные результаты.

Ниже приведен минимальный пример :


+--------------------------------------------------+--------------------------------------------------+
| Session 1                                        | Session 2                                        |
+--------------------------------------------------+--------------------------------------------------+
| BEGIN;                                           |                                                  |
| SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; |                                                  |
| SELECT * FROM users WHERE id = 1 FOR UPDATE;     |                                                  |
| (returns as expected)                            |                                                  |
+--------------------------------------------------+--------------------------------------------------+
|                                                  | BEGIN;                                           |
|                                                  | SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; |
|                                                  | SELECT * FROM users WHERE id = 1 FOR UPDATE;     |
|                                                  | (blocks as expected)                             |
+--------------------------------------------------+--------------------------------------------------+
| UPDATE users SET name = 'foobar' WHERE id = 1;   |                                                  |
| COMMIT;                                          |                                                  |
| (works as expected)                              |                                                  |
+--------------------------------------------------+--------------------------------------------------+
|                                                  | ERROR:  could not serialize access due           |
|                                                  | to concurrent update                             |
+--------------------------------------------------+--------------------------------------------------+

Мое ожидание заключается в том, что, поскольку сеанс 2 не выполнял никаких операций чтения до этого оператора SELECT, и поскольку этот оператор возвращается только после того, как сеанс 1 завершил свое обновление, тогда сеанс 2 должен увидеть обновленное версия таблицы, и это сделало бы ее повторяемой.

Я полагаю, что, скорее всего, Postgres принимает версию при запуске BEGIN, а не при получении блокировки для первого SELECT.

Мои вопросы :

  • Правильно ли мое понимание?
  • Есть ли способ заставить Postgres вести себя так, как я ожидаю?
  • Это будет считаться ошибкой, или работает, как задумано ?

1 Ответ

0 голосов
/ 14 января 2019

С "13.2.2. Повторяемый уровень изоляции чтения" :

Команды

UPDATE, DELETE, SELECT FOR UPDATE и SELECT FOR SHARE ведут себя так же, как и SELECT, в отношении поиска целевых строк: они найдут только целевые строки, которые были зафиксированы на момент начала транзакции , Однако такая целевая строка, возможно, уже была обновлена ​​(или удалена, или заблокирована) другой параллельной транзакцией к тому времени, когда она найдена. В этом случае повторяемая транзакция чтения будет ожидать первой обновляющей транзакции, чтобы зафиксировать или откатить (если она все еще выполняется). Если первый модуль обновления откатывается назад, то его эффекты сводятся на нет, и повторяемая транзакция чтения может продолжить обновление первоначально найденной строки. Но если первый модуль обновления фиксирует (и фактически обновил или удалил строку, а не просто заблокировал ее), то повторяемая транзакция чтения будет откатываться с сообщением

    ERROR:  could not serialize access due to concurrent update

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

Так что да, ваше понимание кажется правильным, если под BEGIN вы подразумеваете начало транзакции. И нет, это не ошибка, но она работает как задумано и задокументировано.

Насколько я понимаю, READ COMMITTED, по умолчанию, должно делать то, что вы хотите. Обратите внимание, что после фиксации первой транзакции в клиенте 1, SELECT FOR UPDATE блокируется до тех пор, пока клиент 2 не выполнит фиксацию или откат, так как теперь SELECT FOR UPDATE успешно выполнено. Таким образом, первая транзакция в клиенте 2 будет считывать одни и те же значения (если сама их не изменяет) до конца транзакции.

Client 1                                        | Client 2
------------------------------------------------+------------------------------------------------
BEGIN TRANSACTION;                              |
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; |
SELECT * FROM users WHERE id = 1 FOR UPDATE;    |
                                                | BEGIN TRANSACTION;
                                                | SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
                                                | SELECT * FROM users WHERE id = 1 FOR UPDATE;
                                                | -- blocks
UPDATE users SET name = 'foobar' WHERE id = 1;  |
COMMIT;                                         |
                                                | -- name = 'foobar' is read
BEGIN TRANSACTION;                              |
SET TRANSACTION ISOLATION LEVEL READ COMMITTED; |
SELECT * FROM users WHERE id = 1 FOR UPDATE;    |
-- blocks                                       |
                                                | SELECT * FROM users WHERE id = 1 FOR UPDATE;
                                                | -- name = 'foobar' is read
                                                | COMMIT;
UPDATE users SET name = 'foobaz' WHERE id = 1;  |
-- name = 'foobaz' is written                   |
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...