Зачем строгая двухфазная блокировка удерживает блокировки на изменяемые строки до завершения транзакции?
Строгая двухфазная блокировка удерживает эксклюзивные блокировки изменённых данных до фиксации или отката транзакции, чтобы другие транзакции не могли опереться на ещё не подтверждённый результат. Это предотвращает чтение и дальнейшее распространение незакоммиченных изменений, поэтому при откате одной транзакции не требуется откатывать зависимые от неё транзакции.
Конкурентное выполнение транзакций повышает пропускную способность, но может создавать расписания, результат которых зависит от случайного чередования операций. Одной из задач механизмов блокировок стало получение поведения, эквивалентного безопасному последовательному выполнению, без полного запрета параллельной работы.
Двухфазная блокировка разделяет работу с блокировками на фазу их получения и фазу их освобождения. Строгий вариант усиливает это правило для изменяемых данных: блокировки записи сохраняются до окончания транзакции, что упрощает восстановление после ошибок.
Предположим, транзакция изменила баланс, но ещё не выполнила фиксацию. Если другая транзакция прочитает это значение и на его основе обновит свои данные, между транзакциями возникнет зависимость от незавершённого результата.
Если первая транзакция затем откатится, вторая уже успеет завершиться, используя данные, которых в базе фактически никогда не должно было остаться. Такая ситуация называется каскадным откатом и усложняет восстановление, а иногда делает уже подтверждённые результаты некорректными.
При изменении строки транзакция получает эксклюзивную блокировку. Пока транзакция не завершена, конфликтующая операция другой транзакции не может изменить эту строку, а при блокирующем режиме чтения — и прочитать её как подтверждённое текущее значение.
После COMMIT изменение становится окончательным, и блокировка освобождается. После ROLLBACK изменение удаляется, а ожидающие операции продолжают работу уже с исходным состоянием.
Ключевое свойство — строгость расписания: ни одна транзакция не использует запись, созданную другой незавершённой транзакцией. Поэтому откат не распространяется каскадом на транзакции, которые уже успели прочитать промежуточный результат.
Строгая двухфазная блокировка не устраняет все конфликты. Она может уменьшать параллелизм, вызывать ожидания и взаимные блокировки. Кроме того, конкретная СУБД может сочетать блокировки с MVCC, поэтому обычное согласованное чтение иногда не блокируется, тогда как обновление или явное блокирующее чтение всё равно ожидает освобождения записи.
Минимальный пример показывает, что вторая запись конфликтует с незавершённой первой:
Если первая сессия зафиксирует изменения, вторая продолжит работу с новым состоянием. Если первая откатится, вторая применит изменение к восстановленному значению, а не к промежуточному результату.
В платёжном сервисе одна транзакция резервирует деньги на счёте, а другая одновременно пытается списать их. Рассматривались три варианта: разрешить свободное обновление, использовать оптимистическую проверку версии или блокировать строку на время изменения.
Свободное обновление опасно потерей результата или использованием незакоммиченного состояния. Оптимистическая проверка хорошо работает при редких конфликтах, но требует корректной обработки ошибки конфликта и повторения всей бизнес-транзакции. Блокировка уменьшает параллелизм, зато даёт простой порядок доступа к критическому балансу.
Выбрали блокирование строки и короткие транзакции с единым порядком захвата ресурсов. В результате незавершённое резервирование не стало основанием для другого списания, а вероятность взаимных блокировок снизилась. Цена решения — ожидание при конкурирующих операциях и необходимость контролировать время жизни транзакций.
Для строгой двухфазной блокировки обычно достаточно удерживать до фиксации или отката блокировки, защищающие записи, изменённые транзакцией. Разрешение совместной блокировки чтения раньше завершения возможно в менее строгих вариантах, но тогда другие транзакции могут увидеть результат чтения до окончательной фиксации и получить менее строгие гарантии.
Важно не смешивать это правило с поведением конкретной СУБД: при MVCC обычный читатель может получить старую подтверждённую версию и не ждать эксклюзивную блокировку. Это не означает, что запись уже доступна для конкурентного обновления.
Нет. Если первая транзакция удерживает строку A и ждёт строку B, а вторая удерживает B и ждёт A, возникает цикл ожидания — deadlock. Строгая блокировка сохраняет корректность незакоммиченных данных, но не гарантирует отсутствие циклов.
СУБД обнаруживает такой цикл или использует тайм-аут, выбирает одну транзакцию жертвой и откатывает её. Практическая защита — захватывать несколько ресурсов в едином порядке, уменьшать длительность транзакций и повторять всю отклонённую транзакцию на уровне приложения.
Классическая двухфазная блокировка при корректном захвате конфликтующих ресурсов обеспечивает конфликтную сериализуемость, но детали зависят от того, какие объекты реально блокируются. Например, блокировка отдельных существующих строк сама по себе может не защитить от вставки новой строки, попадающей в проверяемый диапазон.
Для защиты диапазонов нужны подходящие предикатные, диапазонные или индексные блокировки либо другой механизм, предоставляющий эквивалентную гарантию. Поэтому необходимо оценивать не только название уровня изоляции, но и правила блокирования конкретной СУБД.