Базы данных

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 гарантирует, что после подтверждения бронирования отключение питания не уничтожит данные

Предварительные знания

  • The Relational Model: Tables, Keys, and Relationships

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 ReadNon-repeatable ReadPhantom 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
ACID: четыре столпа надёжности

0

1

Войти