Разберите следующий SQL-кейс: транзакция T1 читает сумму балансов, затем T2 переводит деньги между счетами; при уровне изоляции READ COMMITTED появляется потерянное обновление/фантом — приведите последовательность операций, иллюстрирующую аномалию, опишите, какие уровни изоляции или механизмы (блокировки, MVCC, optimistic concurrency control) её предотвращают, и оцените компромисс между согласованностью и производительностью
Коротко, по делу. Сценарий (исходно): два счёта A и B, балансы A=100100100, B=100100100 (итого 200200200). Иллюстрация аномалии (последовательность операций): 1. T1: BEGIN; 2. T1: SELECT SUM(balance) FROM accounts; — читает сумму 200200200. 3. T2: BEGIN; 4. T2: UPDATE accounts SET balance = balance - 50 WHERE id='A'; — теперь A=505050 (внутри T2). 5. T2: UPDATE accounts SET balance = balance + 50 WHERE id='B'; — B=150150150. 6. T2: COMMIT; — изменения видимы остальным после коммита. 7. T1 (не перечитывая балансы, полагаясь на ранее прочитанный суммарный результат): выполняет нормирующее обновление, например: UPDATE accounts SET balance = SUM/2 = 200/2=100200/2 = 100200/2=100 для A и B — в итоге A и B снова становятся по 100100100. Результат: перевод 505050 выполненный T2 «потерян» — T1 переписал строки, основываясь на устаревших данных. Пояснение аномалии: - Это пример “lost update” (потерянного обновления), возникающего потому что T1 использует устаревшую информацию и перезаписывает изменения T2. - Также здесь присутствует эффект неповторимого чтения/фантома: T1 не видит коммит T2 при принятии решения и тем самым нарушается согласованность логики приложения. Какие уровни изоляции/механизмы это предотвращают - READ COMMITTED: предотвращает грязные чтения, но не защищает от lost update/неповторяемых чтений/фантомов. В данном примере READ COMMITTED ничего не даст, если явных блокировок нет. - REPEATABLE READ (MVCC snapshot): T1 будет видеть снимок на момент начала транзакции, то есть не увидит изменения T2 — это предотвращает неповторяемые чтения, но не защищает от всех конфликтов записи; поведение зависит от СУБД (MySQL/InnoDB, PostgreSQL имеют отличия). - SERIALIZABLE: обеспечивает сериализуемость — аномалий типа lost update/fanтом/write skew не будет; возможны abort’ы транзакций при конфликте (требует повторной попытки). - Пессимистическое блокирование (SELECT ... FOR UPDATE): блокирует строки, которые собираются обновлять; если T1 взял FOR UPDATE на A и B, T2 будет ждать или получит ошибку — предотвращает потерю обновлений, но снижает параллелизм. - Optimistic concurrency (версионное сравнение, WHERE version = old_version): при обновлении проверяется версия; если кто-то уже изменил строку, обновление отклоняется — приложение повторяет транзакцию. Эффективно при низкой конкуренции. - MVCC + проверка write-write: многие MVCC реализации запрещают коммит, если есть конфликт записей (последний коммит выигрывает или один из коммитов откатывается) — защищает от простого lost update при конфликте по тем же строкам. Компромисс согласованность ↔ производительность - Чем выше изоляция (SERIALIZABLE), тем выше согласованность и меньше аномалий, но: больше блокировок/конфликтов → ниже параллелизм, выше задержки и частота откатов/повторов. - READ COMMITTED: хороший компромисс для нагрузок с частыми чтениями — высокая пропускная способность, но приложение должно быть готово к неповторимым чтениям и lost updates (решаются на уровне приложения или через SELECT FOR UPDATE / оптимистическую проверку). - Пессимистическое блокирование: работает стабильно при высокой конфликтности, но снижает пропускную способность и вызывает ожидания/deadlock. - Оптимистический контроль версий: минимально влияет на чтения, лучше при низкой конкуренции; требует обработки отказов и повторов транзакций. - MVCC: очень хорош для чтений (не блокирует), но сложнее с глобальными инвариантами (суммы, агрегаты) — для таких инвариантов чаще нужны сериализуемость или явные блокировки/версионирование. Рекомендации практические - Если нужна строгая гарантия сохранения глобальной инварианты (например, суммарный баланс никогда не должен теряться), используйте либо SERIALIZABLE (с ретраями), либо явные блокировки на затрагиваемые строки (SELECT ... FOR UPDATE) или оптимистическую проверку версий при обновлении. - Для высоконагруженных чтений предпочитайте MVCC + application-level проверку/версионирование и ретраи, а не понижать уровень согласованности. Если нужно, приведу пример SQL (SELECT ... FOR UPDATE или версия-столбец) для защиты от этой конкретной гонки.
Сценарий (исходно): два счёта A и B, балансы A=100100100, B=100100100 (итого 200200200).
Иллюстрация аномалии (последовательность операций):
1. T1: BEGIN;
2. T1: SELECT SUM(balance) FROM accounts; — читает сумму 200200200.
3. T2: BEGIN;
4. T2: UPDATE accounts SET balance = balance - 50 WHERE id='A'; — теперь A=505050 (внутри T2).
5. T2: UPDATE accounts SET balance = balance + 50 WHERE id='B'; — B=150150150.
6. T2: COMMIT; — изменения видимы остальным после коммита.
7. T1 (не перечитывая балансы, полагаясь на ранее прочитанный суммарный результат): выполняет нормирующее обновление, например: UPDATE accounts SET balance = SUM/2 = 200/2=100200/2 = 100200/2=100 для A и B — в итоге A и B снова становятся по 100100100.
Результат: перевод 505050 выполненный T2 «потерян» — T1 переписал строки, основываясь на устаревших данных.
Пояснение аномалии:
- Это пример “lost update” (потерянного обновления), возникающего потому что T1 использует устаревшую информацию и перезаписывает изменения T2.
- Также здесь присутствует эффект неповторимого чтения/фантома: T1 не видит коммит T2 при принятии решения и тем самым нарушается согласованность логики приложения.
Какие уровни изоляции/механизмы это предотвращают
- READ COMMITTED: предотвращает грязные чтения, но не защищает от lost update/неповторяемых чтений/фантомов. В данном примере READ COMMITTED ничего не даст, если явных блокировок нет.
- REPEATABLE READ (MVCC snapshot): T1 будет видеть снимок на момент начала транзакции, то есть не увидит изменения T2 — это предотвращает неповторяемые чтения, но не защищает от всех конфликтов записи; поведение зависит от СУБД (MySQL/InnoDB, PostgreSQL имеют отличия).
- SERIALIZABLE: обеспечивает сериализуемость — аномалий типа lost update/fanтом/write skew не будет; возможны abort’ы транзакций при конфликте (требует повторной попытки).
- Пессимистическое блокирование (SELECT ... FOR UPDATE): блокирует строки, которые собираются обновлять; если T1 взял FOR UPDATE на A и B, T2 будет ждать или получит ошибку — предотвращает потерю обновлений, но снижает параллелизм.
- Optimistic concurrency (версионное сравнение, WHERE version = old_version): при обновлении проверяется версия; если кто-то уже изменил строку, обновление отклоняется — приложение повторяет транзакцию. Эффективно при низкой конкуренции.
- MVCC + проверка write-write: многие MVCC реализации запрещают коммит, если есть конфликт записей (последний коммит выигрывает или один из коммитов откатывается) — защищает от простого lost update при конфликте по тем же строкам.
Компромисс согласованность ↔ производительность
- Чем выше изоляция (SERIALIZABLE), тем выше согласованность и меньше аномалий, но: больше блокировок/конфликтов → ниже параллелизм, выше задержки и частота откатов/повторов.
- READ COMMITTED: хороший компромисс для нагрузок с частыми чтениями — высокая пропускная способность, но приложение должно быть готово к неповторимым чтениям и lost updates (решаются на уровне приложения или через SELECT FOR UPDATE / оптимистическую проверку).
- Пессимистическое блокирование: работает стабильно при высокой конфликтности, но снижает пропускную способность и вызывает ожидания/deadlock.
- Оптимистический контроль версий: минимально влияет на чтения, лучше при низкой конкуренции; требует обработки отказов и повторов транзакций.
- MVCC: очень хорош для чтений (не блокирует), но сложнее с глобальными инвариантами (суммы, агрегаты) — для таких инвариантов чаще нужны сериализуемость или явные блокировки/версионирование.
Рекомендации практические
- Если нужна строгая гарантия сохранения глобальной инварианты (например, суммарный баланс никогда не должен теряться), используйте либо SERIALIZABLE (с ретраями), либо явные блокировки на затрагиваемые строки (SELECT ... FOR UPDATE) или оптимистическую проверку версий при обновлении.
- Для высоконагруженных чтений предпочитайте MVCC + application-level проверку/версионирование и ретраи, а не понижать уровень согласованности.
Если нужно, приведу пример SQL (SELECT ... FOR UPDATE или версия-столбец) для защиты от этой конкретной гонки.