Прошлый раз мы разобрали уровень Read Commited, который защищает от «грязных» чтений и «грязных» записей. Однако на уровне Read Commited возможно возникновение других ошибок конкурентного доступа.
📌 Неповторяемое чтение (non-repeatable read)
На первой картинке к посту изображена аномалия неповторяемого чтения (non-repeatable read). Или более редкий термин — асимметрия чтения (read skew).
Кратко схему аномалии non-repeatable read я бы описала так:
➡️ Первая транзакция делает SELECT
➡️ Другая делает UPDATE и коммит
➡️ Первая транзакция делает SELECT и видит несоответствие с первым чтением
То есть первая транзакция получила неповторяемое чтение.
В ситуации с картинки это временная проблема, которая решится после обновления страницы. Однако в некоторых ситуациях подобные несоответствия недопустимы.
Чаще всего для решения проблемы с неповторяемым чтением используется изоляция снимков состояния.
Ее идея состоит в том, что каждая из транзакций читает данные из согласованного снимка состояния БД, то есть видит данные, которые были зафиксированы в базе на момент ее начала этой транзакции. Даже если данные затем были изменены другой транзакцией, каждая транзакция видит только старые данные — снимок состояния, «замороженного» в определенный момент времени.
Изоляция снимков состояния используется в PostgreSQL, MySQL с InnoDB, Oracle, SQL Server.
Реализация изоляции снимков состояния
Для реализации изоляции снимков состояния базы применяют обобщение механизма, используемого для предотвращения «грязных» чтений: БД хранит несколько различных зафиксированных версий объекта, поскольку разным транзакциям может понадобиться состояние базы на различные моменты времени. Этот метод получил название многоверсионного управления конкурентным доступом (multiversion concurrency control — MVCC).
В самом начале выполнения транзакция получает уникальный, монотонно возрастающий идентификатор (
txid). В каждой строке таблицы есть поле created_by, содержащее идентификатор транзакции, вставившей эту строку в таблицу. Также в каждой строке таблицы есть поле deleted_by, изначально пустое. Если транзакция удаляет строку, то строка помечается для удаления путем установки deleted_by = txid запросившейудаление транзакции. В дальнейшем, когда уже никакая транзакция точно не обратится к удаленным данным, процесс сборки мусора БД удалит все помеченные для удаления строки и освободит занимаемое ими место.
Обновление превращается внутри базы данных в удаление и создание.
📌 Потерянное обновление (lost update)
Такая аномалия возникает, когда две транзакции читают одну и ту же строку таблицы, затем одна транзакция обновляет эту строку, после чего вторая транзакция тоже обновляет ту же строку, не учитывая изменений, сделанных первой транзакцией.
Пример на второй картинке к посту.
Для предотвращения потерянных обновлений можно использовать:
✅ атомарные записи
UPDATE accounts SET balance = balance + 100
WHERE id = 123;
✅ явные блокировки
Если функциональности встроенных атомарных операций базы данных недостаточно, приложение может явно заблокировать предназначенные для обновления объекты. После блокировки приложение безопасно выполняет действия, а другим транзакциям при попытке конкурентного чтения того же объекта приходится подождать.
BEGIN TRANSACTION;
SELECT * FROM items WHERE category = 'robot' FOR UPDATE;
-- Делаем какие-то проверки, после чего обновляем возвращенные SELECT-ом данные
UPDATE items SET price = '100.00' WHERE id = 123;
COMMIT;
Предложение FOR UPDATE указывает базе блокировать все возвращенные запросом строки.
✅ автоматическое обнаружение потери обновлений
PostgreSQL на уровне изоляции Repeatable Read (и другие БД, кроме MySQL) обеспечивают автоматическое обнаружение потери обновлений и прерывание проблемных транзакций.
На этом уровне приложению не требуется использовать атомарные операции или блокировки, которые можно забыть. Обнаружение потерянных обновлений выполняется автоматически, поэтому меньше подвержено ошибкам.
#кабанчик #сисдиз

