Как работают параллельные транзакции с одной строкой при уровне изоляции SERIALIZABLE
Что проверяют
Знаете ли вы, что SERIALIZABLE в разных СУБД устроен по-разному, и готов ли ваш код к тому, что транзакция на этом уровне может просто упасть. Следующий вопрос обычно: что вы делаете, получив ошибку сериализации.
Как отвечать
Стандарт обещает одно: результат такой же, как при каком-то последовательном выполнении. Способ реализации выбирает база.
В PostgreSQL это Serializable Snapshot Isolation:
- обе обновляют одну строку: вторая ждёт блокировку строки. Откатилась первая, вторая продолжает. Закоммитилась первая, вторая получает
could not serialize access due to concurrent updateс кодом40001; - одна читает, другая пишет: никто никого не ждёт. Чтение идёт по снимку, база ставит неблокирующие предикатные блокировки и отслеживает зависимости;
- опасный цикл зависимостей: классический write skew, когда двое дежурных одновременно снимаются со смены. Такой порядок невозможен последовательно, и одна транзакция падает с
40001, иногда уже наCOMMIT.
В MySQL InnoDB подход блокировочный: при выключенном autocommit обычный SELECT работает как SELECT ... FOR SHARE. Две транзакции читают строку, обе хотят её обновить, и получается дедлок: InnoDB откатывает одну с ошибкой 1213.
Вывод для кода на Python: транзакцию нужно уметь повторять целиком. Уровень ставят в SQLAlchemy через create_engine(url, isolation_level="SERIALIZABLE"), в Django через OPTIONS базы. Обёртка ловит SQLSTATE 40001 (в psycopg это errors.SerializationFailure), откатывает и запускает функцию заново, с лимитом попыток и паузой.
Где спотыкаются
Описывают SERIALIZABLE как «всё блокируется до конца транзакции». Для PostgreSQL это неверно: читатели писателей не блокируют, вместо ожидания будет ошибка.
Повторяют только упавший запрос, а не всю транзакцию. Решения, принятые на прочитанных раньше данных, остаются старыми.
Делают внутри транзакции внешние вызовы: отправляют письмо или идут в чужой API. При повторе действие выполнится дважды.