Базы данных
ACID: четыре столпа надёжности
2012 год. Knight Capital Group. Баг в трейдинговом алгоритме запустил 4 миллиона рыночных ордеров за 45 минут. Частично выполненные транзакции без rollback. Компания потеряла 440 миллионов долларов за полчаса торгов - больше, чем заработала за всё предыдущее десятилетие. ACID - это не академия. Это разница между ошибкой программиста и катастрофой компании.
- **Knight Capital, 2012** - отсутствие атомарных rollback в трейдинговой системе стоило 440 млн долларов за 45 минут
- **Booking.com и авиакомпании** - Isolation предотвращает double-booking: два пользователя не могут купить билет на одно место
- **PostgreSQL (WAL + fsync)** - Durability гарантирует, что после подтверждения бронирования отключение питания не уничтожит данные
Предварительные знания
Jim Gray и рождение транзакций
В 1976 году Джим Грей из IBM Research опубликовал фундаментальную работу по транзакционным системам. Он первым формализовал понятия ACID - хотя сам термин появился позже, в 1983 году, от Тео Хэрдера и Андреаса Рейтера. Грей получил премию Тьюринга в 1998 году именно за транзакционные системы. В 2007 году он бесследно исчез в Тихом океане во время одиночного плавания - и по сей день считается пропавшим без вести.
Atomicity: всё или ничего
2012 год. Knight Capital Group. Трейдинговый алгоритм отправил 4 миллиона ордеров за 45 минут - частично, без rollback. Компания потеряла 440 миллионов долларов за полчаса. Не вирус, не взлом - просто транзакция без атомарности. **Atomicity** гарантирует: либо обе операции выполнятся, либо ни одна.
Слово **"атом"** в переводе с греческого - "неделимый". Транзакция атомарна: снаружи она выглядит как одна операция. Нет промежуточного состояния, где деньги уже списаны, но ещё не зачислены.
**Что если сервер упал прямо во время COMMIT?** СУБД использует **Write-Ahead Log (WAL)**: перед записью данных на диск сначала записывается лог операций. При восстановлении после сбоя СУБД читает WAL и либо доигрывает незавершённый COMMIT, либо откатывает его.
**Длинные транзакции - зло.** Пока транзакция открыта, она может блокировать строки и не давать другим транзакциям работать. BEGIN → ... сложная логика 10 секунд ... → COMMIT = 10 секунд блокировки. Держите транзакции как можно короче.
Транзакция: BEGIN → UPDATE (списать 500 у Alice) → сервер упал → UPDATE (зачислить 500 Bob) не выполнен → COMMIT не вызван. Что произойдёт с балансом Alice после перезапуска сервера?
Consistency: данные всегда валидны
**Consistency** в ACID означает: база данных всегда переходит из одного валидного состояния в другое. Невозможно оставить данные в промежуточном, некорректном состоянии. Если транзакция нарушает любое правило целостности - она откатывается целиком.
Consistency обеспечивается через **ограничения (constraints)** - правила, которые БД проверяет автоматически при каждой записи.
**Trigger** - функция, которая автоматически выполняется при INSERT, UPDATE или DELETE. Позволяет реализовать сложные бизнес-правила, которые не выразить через CHECK.
| Constraint | Что проверяет | Когда использовать |
|---|---|---|
| NOT NULL | Значение обязательно | Имя, email, дата создания |
| CHECK | Произвольное условие | Баланс >= 0, возраст > 0, статус IN (...) |
| UNIQUE | Нет дубликатов | Email, username, номер телефона |
| FOREIGN KEY | Ссылка на существующую запись | user_id → users.id |
| TRIGGER | Любая логика (функция) | Дневные лимиты, аудит, каскадные обновления |
**Consistency в ACID vs Consistency в CAP** - это разные вещи! ACID Consistency = каждая транзакция оставляет данные в валидном состоянии (constraints). CAP Consistency = все узлы кластера видят одни и те же данные одновременно (репликация). Не путайте.
Таблица accounts имеет CHECK (balance >= 0). Транзакция: BEGIN → UPDATE (balance = balance - 1000, текущий balance = 500) → COMMIT. Что произойдёт?
Isolation: изоляция транзакций
Когда тысячи пользователей работают с БД одновременно, их транзакции не должны мешать друг другу. **Isolation** определяет, насколько одна транзакция «видит» изменения, сделанные другой, незавершённой транзакцией.
Без изоляции возникают аномалии - ситуации, когда параллельные транзакции получают некорректные результаты.
| Уровень изоляции | Dirty Read | Non-repeatable Read | Phantom Read | Скорость |
|---|---|---|---|---|
| READ UNCOMMITTED | Возможен | Возможен | Возможен | Максимальная |
| READ COMMITTED | Защищён | Возможен | Возможен | Высокая |
| REPEATABLE READ | Защищён | Защищён | Возможен | Средняя |
| SERIALIZABLE | Защищён | Защищён | Защищён | Низкая |
**PostgreSQL по умолчанию: READ COMMITTED.** Это означает, что каждый SELECT внутри транзакции видит только закоммиченные данные, но между двумя SELECT-ами результат может измениться (non-repeatable read). Для финансовых операций часто нужен SERIALIZABLE.
**Практическое правило:** 95% веб-приложений отлично работают на READ COMMITTED. SERIALIZABLE нужен для финансовых транзакций, бронирования (где double-booking = катастрофа) и инвентаризации. Чем выше изоляция - тем больше блокировок и ниже throughput.
**Deadlock** - когда транзакция A ждёт строку, заблокированную B, а B ждёт строку, заблокированную A. СУБД обнаруживает deadlock и принудительно откатывает одну из транзакций. Код должен быть готов повторить откатанную транзакцию.
Уровень изоляции READ COMMITTED. Транзакция A: BEGIN → SELECT balance (500) → ... ждёт ... → SELECT balance (?). Между двумя SELECT другая транзакция изменила баланс на 300 и сделала COMMIT. Что вернёт второй SELECT?
Durability: данные переживут любой сбой
Сделан COMMIT. Сервер подтвердил: «транзакция завершена». Через секунду - отключилось электричество. **Durability** гарантирует: после успешного COMMIT данные сохранены навсегда, даже если сервер сгорит в следующую миллисекунду.
Как это возможно? Ведь запись на диск - не мгновенная операция. Данные проходят через несколько уровней кеширования: кеш приложения → буферный пул СУБД → кеш файловой системы (page cache) → кеш контроллера диска → диск. Сбой на любом уровне = потеря данных. **Write-Ahead Log (WAL)** решает эту проблему.
**fsync** - системный вызов, который заставляет ОС записать данные из кеша на физический диск. Без fsync, данные могут «висеть» в кеше ОС и пропасть при отключении питания. PostgreSQL вызывает fsync для каждой WAL-записи при COMMIT.
**fsync = off** - команда, которую нельзя запускать в production. Без fsync база будет работать значительно быстрее (нет ожидания диска), но при внезапном сбое данные могут быть повреждены безвозвратно. Используйте только для тестовых сред и импорта данных.
**Репликация** - дополнительный уровень durability. Даже если диск сгорит физически, данные сохранятся на replica-сервере. PostgreSQL поддерживает **synchronous replication**: COMMIT не возвращается, пока данные не записаны на replica. Это защита от потери целого сервера.
PostgreSQL: synchronous_commit = on. Клиент получил подтверждение COMMIT. Через 1 мс сервер обесточен. Что произойдёт с данными этой транзакции?
Компромиссы ACID: CAP и BASE
ACID - это идеал надёжности. Но за надёжность приходится платить: блокировки, синхронная запись на диск, ожидание реплик. В распределённых системах с миллионами запросов в секунду **полный ACID иногда слишком дорог**.
**CAP-теорема** (Эрик Брюер, 2000) утверждает: в распределённой системе невозможно одновременно гарантировать все три свойства - можно выбрать только два из трёх.
| Свойство | Что значит | Пример |
|---|---|---|
| Consistency (C) | Все узлы видят одинаковые данные | Баланс = 500 на всех серверах |
| Availability (A) | Каждый запрос получает ответ | Сайт работает, даже если узел упал |
| Partition Tolerance (P) | Система работает при разрыве сети | Сервер в USA и EU потеряли связь |
На практике **P обязательно** (сеть всегда может разорваться), поэтому выбор между **CP** (консистентность, но часть запросов может отказать) и **AP** (доступность, но данные могут быть устаревшими).
**Правило выбора:** если потеря или некорректность данных стоит денег (финансы, медицина, бронирование) - ACID. Если данные допускают временную неконсистентность (лайки, просмотры, логи, кеш) - BASE. Большинство систем используют оба подхода для разных частей.
**NewSQL** - современные БД, пытающиеся совместить ACID + горизонтальное масштабирование. Google Spanner, CockroachDB, YugabyteDB - обеспечивают ACID-транзакции на кластере из сотен серверов, но ценой задержки (latency), потому что консенсус между узлами требует времени.
ACID всегда нужен - данные должны быть абсолютно консистентны в любой момент
Для аналитики, логов, счётчиков, лент новостей и кеша BASE (eventual consistency) часто предпочтительнее. ACID-гарантии имеют стоимость: блокировки, синхронная запись, снижение throughput. Выбор между ACID и BASE - это инженерное решение, а не вопрос 'правильности'
Instagram не использует ACID-транзакции для подсчёта лайков - при 100 000+ лайков/сек блокировки убили бы производительность. Зато для платежей, бронирований и банковских переводов ACID обязателен. Хороший архитектор использует ACID там, где нужна точность, и BASE там, где нужна скорость.
Проектируется система подсчёта просмотров видео (YouTube-масштаб: миллионы просмотров в секунду). Какой подход оптимален?
Ключевые идеи
- **Atomicity** - транзакция неделима: все операции применяются или все откатываются. Knight Capital 2012 - наглядный пример цены отсутствия атомарности
- **Consistency** - constraints (CHECK, FK, NOT NULL, UNIQUE, triggers) гарантируют, что данные всегда в валидном состоянии
- **Isolation** - четыре уровня (READ UNCOMMITTED → SERIALIZABLE) определяют, насколько параллельные транзакции видят изменения друг друга. Больше изоляции = меньше аномалий, но больше блокировок
- **Durability** - WAL + fsync гарантируют: после COMMIT данные на диске, даже если сервер выключится
- **ACID vs BASE** - не бинарный выбор. Используйте ACID для критичных данных (деньги, бронирования) и BASE для масштабируемых некритичных (лайки, просмотры, логи)
Связанные темы
ACID - фундамент, на котором строятся продвинутые механизмы БД:
- Реляционная модель — Constraints из реляционной модели (PK, FK, CHECK) - основа свойства Consistency
- Зачем нужны базы данных — Вводный урок: файлы vs БД, CRUD, клиент-серверная модель - предпосылки к пониманию ACID
- CAP-теорема — CAP объясняет почему полный ACID недостижим в распределённых системах
Вопросы для размышления
- В каких ситуациях в проекте стоит использовать SERIALIZABLE уровень изоляции, а в каких хватило бы READ COMMITTED?
- Если сервер стоит в дата-центре с UPS (бесперебойным питанием) и RAID-массивом, всё ещё нужен ли WAL? Почему?
- Приведите пример из реальной жизни, где eventual consistency (BASE) - лучший выбор, чем strict consistency (ACID).
Связанные уроки
- db-04-cap — ACID-Consistency и CAP-Consistency - разные термины, один урок разъясняет оба
- db-14-mvcc — MVCC - механизм под капотом Isolation без блокировок на чтение
- db-13-transactions — Углублённые паттерны транзакций строятся поверх ACID-гарантий
- db-16-distributed-tx — 2PC и Saga - попытки сохранить ACID-атомарность в распределённых системах
- db-41-newsql — CockroachDB и Spanner - ACID поверх горизонтально масштабируемого кластера
- ds-02-cap-theorem — CAP-теорема объясняет почему полный ACID недостижим в распределённых системах
- bt-18-saga