Вопрос #1615372

Разберите конкретный пример SQL-конкуренции: две транзакции пытаются изменить одни и те же строки в базе данных с уровнем изоляции Read Committed — опишите возможные проблемы (фантомы, грязные чтения, блокировки) и как корректно обеспечить согласованность

Ответы
1


Кратко и по делу — конкретный пример и как это исправить.
Пример: две транзакции читают и затем меняют одну строку (таблица balances, колонка amount).
Транзакции: \(T_1\) и \(T_2\).
Сценарий «потерянного обновления» (typical under Read Committed, если просто читают и затем вычисляют обновление):
1) \(T_1\): `SELECT amount FROM balances WHERE id = 1;` — прочитал \(1000\)
2) \(T_2\): `SELECT amount FROM balances WHERE id = 1;` — прочитал \(1000\)
3) \(T_1\): `UPDATE balances SET amount = amount - 100 WHERE id = 1;` — записывает \(900\), коммит
4) \(T_2\): `UPDATE balances SET amount = amount + 100 WHERE id = 1;` — записывает \(1100\), коммит
Результат: изменение \(T_1\) потеряно (консистентный итог неверен). Read Committed предотвращает грязные чтения, но не предотвращает потерю обновлений или нерепродуцируемые чтения.
Возможные проблемы при уровне Read Committed:
- Грязные чтения (dirty reads): нет — Read Committed не даёт читать незакоммиченные изменения других транзакций.
- Неповторяемые чтения (non-repeatable reads): да — повторный SELECT в одной транзакции может вернуть другое значение после коммита другой транзакции.
- Фантомы (phantom reads): да — при повторном запросе по диапазону/предикату могут появиться или исчезнуть строки, вставленные/удалённые другими транзакциями.
- Блокировки/ожидание: при попытке обновить одну и ту же строку второй обновляющий может блокироваться до конца первого коммита; возможны дедлоки при циклических зависимостях.
Как это происходит технически (общая картина):
- Read Committed обычно использует эксклюзивные блокировки при записи; чтения не держат их до конца транзакции. Поэтому чтение не блокирует будущие записи, а запись блокирует другие записи до коммита.
- В MVCC-реализациях (Postgres, Oracle) семантика чуть отличается: чтения не блокируют записи, чтения видят последовательно-коммитнутую версию; записи конфликтуют при записи тех же строк.
Как корректно обеспечить согласованность (варианты, с компромиссами):
1) Пессимистическая блокировка (serialize access)
- Использовать `SELECT ... FOR UPDATE` перед вычислением/обновлением:
- \(T_1\): `SELECT amount FROM balances WHERE id = 1 FOR UPDATE;` — захватит блокировку; другие будут ждать.
- Это предотвращает потерянные обновления, но увеличивает конкуренцию и время удержания блокировок.
- Подходит, когда важна строгая последовательность и низкая конкуренция.
2) Оптимистическая блокировка (версионность)
- Добавить колонку `version` или `row_version`.
- Чтение: `SELECT amount, version FROM balances WHERE id = 1;`
Обновление: `UPDATE balances SET amount = :new, version = version + 1 WHERE id = 1 AND version = :old_version;`
Проверять число затронутых строк; если `0` — конфликт, откат и retry.
- Обычно лучший вариант для высоконагруженных систем (меньше блокировок), но требует логики повторов.
3) Повышение уровня изоляции
- Перейти на `REPEATABLE READ` — исключит неповторимые чтения; но фантому может всё ещё быть (зависит от СУБД).
- Перейти на `SERIALIZABLE` — обеспечивает вид транзакций как последовательных; предотвращает фантомы и многие классы логических ошибок. Требует обработки ошибок сериализации/требует ретраев при конфликте.
- Цена: больше откатов/ретраев и потенциально худшая производительность.
4) Атомарные обновления на уровне SQL
- Выполнить обновление в виде одного выражения без предварительного SELECT:
`UPDATE balances SET amount = amount - 100 WHERE id = 1;`
Это снижает окно для потерянного обновления, но не решает бизнес-логические проверки, зависящие от прочитанного значения.
5) Обработка дедлоков и ретраи
- Всегда короткие транзакции, обработка ошибок блокировок/дедлоков (ловить exception и делать retry с экспоненциальной задержкой).
Рекомендация практическая:
- Если критична корректность бизнес-логики (переводы, расчёты), используйте либо:
a) Optimistic locking (version + UPDATE ... WHERE version = ?) + retry, либо
b) SERIALIZABLE уровень (и обработка сериализационных ошибок) либо
c) `SELECT ... FOR UPDATE` внутри короткой транзакции.
- Для отчётных (чтение) задач — Read Committed обычно OK; для модификаций с условной логикой — избегайте простых read-then-write без защиты.
Коротко: Read Committed предотвращает грязные чтения, но не предотвращает потерю обновлений, неповторимые чтения и фантомы. Для согласованности: либо блокируйте строки явно (`FOR UPDATE`), либо применяйте оптимистическую версионность, либо повышайте изоляцию до Serializable и реализуйте ретраи.

Минимум 10 символов
Прямой эфир