Вспомнила вопрос с недавнего собеса и решила разобрать в канале.
Две транзакции меняют разные счётчики, но одна из них всё равно может завершиться ошибкой. Причина в том, как база отслеживает захват объектов.
Пример: таблица
counters(id PRIMARY KEY, value), без триггеров и связей с другими таблицами. Обе транзакции работают в SERIALIZABLE. Сначала выполняются оба SELECT, затем оба UPDATE и попытки COMMIT:-- Транзакция T1
SELECT value FROM counters WHERE id = 1;
UPDATE counters SET value = value + 1 WHERE id = 1;
-- Транзакция T2
SELECT value FROM counters WHERE id = 2;
UPDATE counters SET value = value + 1 WHERE id = 2;
SERIALIZABLE обещает результат успешных транзакций как будто транзакции выполняются последовательно.Для проверки база запоминает, кто какую область читал. Эти отметки -
SIReadLock - не блокируют запись. Они помогают заметить, что чужое изменение затронуло прочитанные данные.Пример: T1 прочитала старое значение, а T2 его изменила. Значит, в последовательном выполнении T1 должна быть раньше T2. Иначе она увидела бы новое значение. Так появляется зависимость T1 → T2.
При
Seq Scan - последовательном чтении таблицы - отметка чтения ставится на всю таблицу, хотя запрос меняет одну строку.Поэтому база учитывает две зависимости:
- T1 меняет строку
1 → задевает отметку T2 → T2 должна быть раньше T1.- T2 меняет строку
2 → задевает отметку T1 → T1 должна быть раньше T2.Требования противоречат друг другу. PostgreSQL отклоняет одну транзакцию с ошибкой
serialization_failure, код 40001. Это ложный конфликт: счётчики можно изменить в любом порядке, но широкая отметка связала две независимые операции.
Чтение через индекс может сузить отметку до строки/страницы и тем самым уменьшить число конфликтов. Поэтому при разборе таких откатов полезно проверить план выполнения запроса. Но полностью исключить конфликты нельзя: приложение должно уметь при ошибке
40001 повторить всю транзакцию, включая чтения.классная статья про блокировки в постгрес
#production_case


