Продолжаем разбирать главу кабанчика («Высоконагруженные приложения» Мартина Клепманна) про транзакции. Ссылки на конспекты предыдущих глав есть в закрепе.
Асимметрия записи и фантомы
Разобраться с этими аномалиями нам помогут два примера. В первом примере у нас график врачей в больнице. Бизнес-требование: на дежурстве должен находиться как минимум один врач. Транзакции и состояние БД в случае, когда два дежурных врача одновременно запросили отгул, показаны на первой картинке к посту.
Во втором примере — бронирование переговорных. Бизнес-требование: нельзя забронировать переговорную на одно и то же время дважды. Поэтому при запросе бронирования мы сначала проверяем, нет ли уже бронирований на пересекающийся промежуток времени. Если нет, то можно создавать свое бронирование. Транзакции и состояние БД в случае попытки одновременного бронирования показаны на второй картинке к посту.
Возникшая аномалия называется асимметрией записи (write skew).
Схема возникновения асимметрии записи:
👀 транзакция читает данные,
🤔 принимает на основе прочитанного решение о дальнейших действиях
✍️ и выполняет запись в БД.
🤨 Однако на момент выполнения записи исходные условия, на основе которых принималось решение, уже не соответствуют действительности.
Это не «грязная» запись и не потеря обновления, поскольку транзакции обновляют два различных объекта (разные графики дежурств, разные записи в таблице бронирования).
В прошлом посте мы рассмотрели способы предотвращения потери обновлений. В случае асимметрии записи возможностей меньше:
🔴 Атомарные однообъектные операции (SET value = value + 10) не помогут, поскольку в транзакции участвует несколько объектов.
🟡 В некоторых случаях могут помочь ограничения целостности базы (например, уникальность, внешние ключи или ограничения на конкретных значений). Однако в случае дежурных врачей понадобилось бы ограничение, налагаемое на несколько объектов. В большинстве баз нет встроенной поддержки подобных ограничений, но их можно реализовать с помощью триггеров или материализованных представлений.
🟢 Явная блокировка результатов SELECT FOR UPDATE может помочь в тех случаях, когда SELECT возвращает какие-то данные.
В случае с выборкой дежурных врачей это поможет, но в случае с переговорными SELECT на первом шаге не вернет ничего, поэтому не на что будет устанавливать блокировки.
🟡 Автоматическое обнаружение базой данных
Эффект, при котором операция записи в одной транзакции меняет результат запроса на поиск в другой, называется фантомом (phantom).
Кратко схему аномалии фантомного чтения можно описать так:
➡️Первая транзакция делает SELECT по какому-то условию
➡️Другая транзакция делает INSERT/UPDATE/DELETE, который влияет на выборку из 1-го шага
➡️Первая транзакция делает SELECT и видит отличающийся результат по сравнению с первым чтением
Изоляция снимков состояния (уровень Repeatable Read в PostgreSQL) предотвращает фантомные чтения.
Но в транзакциях, выполняющих чтение и запись данных, как в примерах с врачами и бронированием, изоляция снимков состояния не поможет. Для автоматического предотвращения асимметрии записи нужен уровень настоящей сериализуемости.
🟢 Материализация конфликтов — это создание в базе объектов, на которые можно было бы повесить блокировку SELECT FOR UPDATE для превращения фантома в конфликт.
Например, в случае с бронированием переговорных можно создать отдельную таблицу, в которой каждая строка будет соответствовать связке переговорная + отрезок времени 15 минут. Тогда транзакция перед созданием бронирования могла бы блокировать (SELECT FOR UPDATE) в этой таблице строки, соответствующие нужной переговорной и отрезкам времени. После установки блокировок можно сделать проверку и вставить новую бронь, как и раньше. Обратите внимание: сама дополнительная таблица не хранит информацию о бронировании — она нужна только для блокировок.
Материализация конфликтов непроста, да и перенос управления конкурентным доступом в модель данных приложения выглядит не очень красиво. Поэтому ее рассматривают, когда не подходит изоляция уровня сериализуемости.
#кабанчик #сисдиз
Post #104
1.47K


- 👍 9
- 🔥 7
- ❤ 2
- 😁 1