Разберём ACID: атомарность, согласованность, изоляцию и долговечность — и как они влияют на дизайн, скорость и надёжность транзакций.

ACID — это набор гарантий, которые даёт транзакционная система (чаще всего база данных) при выполнении операций. Эти гарантии нужны, чтобы данные оставались корректными не только в «идеальных» условиях, но и когда что‑то идёт не так: сервер перезагрузился, сеть моргнула, два пользователя одновременно нажали «Оплатить», а фоновые процессы параллельно обновляют склад.
Без чётких правил система начинает «терять реальность»: деньги могут списаться дважды, заказ — появиться без оплаты, а остатки на складе — уйти в минус. ACID задаёт договор: как система ведёт себя при сбоях и при конкурирующих действиях, и что именно считается правильным результатом.
Корректность — это когда итоговое состояние данных соответствует правилам бизнеса, даже если процесс оборвался на середине или несколько процессов работали параллельно. Пользователь не должен видеть «промежуточные» состояния, а система должна уметь либо завершить изменения полностью, либо не оставить от них следов.
commit результат сохраняется даже при сбое.Важно: ACID не «ускоряет» систему и не заменяет хороший дизайн. Он уменьшает класс ошибок, которые иначе проявятся на нагрузке и в самые неудобные моменты.
Транзакция — это единица работы с данными, которую база данных старается выполнить «как одно целое». Представьте перевод денег: нужно списать сумму с одного счёта и зачислить на другой. Если сделать только половину операции, данные окажутся в странном состоянии. Транзакция объединяет связанные шаги в один логический блок.
В упрощённом виде у транзакции есть этапы:
До commit база ещё может отменить изменения, после commit — считает их принятыми.
Транзакции нужны не только из‑за ошибок в логике приложения, но и из‑за сбоев среды:
При сбое вы часто не знаете, успел ли commit дойти до базы. Поэтому транзакционный подход позволяет безопаснее повторять операции и полагаться на предсказуемое поведение.
Одиночная запись в БД — это одна команда. Транзакция же склеивает несколько команд в один смысловой результат: либо все связанные изменения считаются выполненными, либо ни одно из них не должно «торчать» отдельно. Это снижает риск частично обновлённых данных и упрощает бизнес‑логику.
Атомарность — это обещание транзакции «всё или ничего»: если вы выполняете группу связанных изменений, система либо зафиксирует их все, либо не применит ни одного. Причём «не применит» означает не только «не будет видно пользователям», но и «в базе не останется полусделанных следов».
Представьте перевод 1 000 ₽ со счёта A на счёт B. Обычно это два шага: списать с A и зачислить на B. Если после списания случится сбой (падение процесса, перезагрузка сервера, потеря соединения), без атомарности вы получаете перекос: деньги «исчезли», потому что второй шаг не успел выполниться.
С атомарностью остаются только два корректных исхода:
commit прошёл — списание и зачисление оба считаются состоявшимися;rollback — транзакция откатывается, и баланс A возвращается к прежнему значению.На практике атомарность обеспечивается журналом транзакций и восстановлением после сбоев. Упрощённо система ведёт записи, позволяющие:
Границы транзакции должны охватывать только действительно единое действие (например, «перевод»), но не включать долгие операции: сетевые вызовы, ожидание пользователя, тяжёлые расчёты. Чем короче транзакция, тем меньше риск блокировок и тем проще системе гарантировать «всё или ничего».
Согласованность (Consistency) в ACID — про то, что после завершения транзакции данные остаются в допустимом состоянии: все обязательные правила выполнены, а «запрещённых» комбинаций в базе не появилось. Если транзакция не может привести данные к валидному состоянию, она должна завершиться ошибкой — и тогда изменения не фиксируются.
Инвариант — это правило, которое должно быть истинным всегда: например, «баланс не бывает отрицательным», «заказ не может ссылаться на несуществующего клиента», «у пользователя ровно один email и он уникален».
Часть инвариантов лучше закреплять прямо в базе данных — так они будут соблюдаться независимо от того, кто и как пишет в БД (приложение, админ‑скрипт, интеграция).
Ограничения БД — «последняя линия обороны», которая не даст записать некорректные данные:
Проверки на уровне приложения важны для удобства и бизнес‑сценариев (понятные сообщения пользователю, сложные правила, зависящие от контекста). Но их недостаточно, если данные могут записываться в обход приложения.
Consistency не обещает, что система «всегда отражает реальный мир без задержек». Она гарантирует другое: каждая успешная транзакция переводит базу из одного корректного состояния в другое, согласно заданным ограничениям и инвариантам.
Изоляция в ACID отвечает на вопрос: что увидит транзакция, если рядом в это же время кто‑то меняет те же данные? Смысл изоляции — сделать так, чтобы параллельные операции не ломали друг другу логику, а результат выглядел как при некотором последовательном выполнении.
Когда две транзакции читают и пишут одни и те же строки, база должна выбрать механизм: заставить одну подождать, дать читать «промежуточные» состояния или показать снимок данных на определённый момент. Если выбор сделан в пользу большей параллельности, появляются классические аномалии.
rollback, первая опирается на несуществующую реальность.commit между чтениями.Чем сильнее изоляция, тем чаще базе приходится удерживать блокировки, проверять конфликты или хранить версии строк (MVCC). Это уменьшает число одновременных операций и повышает задержки, особенно при «горячих» данных (счётчик, баланс, складской остаток).
Сильная изоляция обычно важнее там, где ошибка — это деньги и доверие: финансы, учёт, биллинг, инвентаризация. В менее критичных сценариях иногда допустимы более слабые уровни ради скорости — но только если приложение умеет обнаруживать и корректно обрабатывать конфликты.
Уровень изоляции определяет, насколько «видимы» для вас параллельные изменения других транзакций. Чем выше изоляция, тем меньше сюрпризов в данных, но тем больше цена: ожидания, конфликты и падение пропускной способности.
Важно: названия уровней стандартизованы, но реальная семантика часто отличается между СУБД.
Если операция денежная/учётная (балансы, лимиты, уникальность, инварианты) и ошибка дорогая — начинайте с Serializable или используйте более слабый уровень, но добавляйте явные проверки и блокировки на критических участках.
Если это каталоги, ленты, отчёты, где допустимы кратковременные расхождения, обычно хватает Read Committed: меньше конфликтов и выше скорость.
Repeatable Read полезен, когда внутри одной транзакции вы делаете несколько связанных чтений и не хотите «плавающих» результатов.
Проверьте: реализован ли уровень через блокировки или через снимки (snapshot/MVCC), защищает ли Repeatable Read от фантомов, какие аномалии всё ещё возможны, и как СУБД обрабатывает конфликты (ожидание, ошибка сериализации, автоматический откат). Это напрямую влияет на то, нужно ли приложению повторять транзакции при конфликте.
Долговечность (Durability) — это обещание системы: если транзакция завершилась commit, её результат переживёт сбой процесса, перезагрузку сервера и даже кратковременное отключение питания. Иначе говоря, «пользователь увидел успех» должно означать «данные уже не потеряются».
Почти все СУБД достигают долговечности через журнал транзакций (WAL/redo log). Вместо того чтобы немедленно переписывать «основные» страницы данных, система сначала фиксирует в журнале, какие изменения нужно применить. При восстановлении после сбоя СУБД проигрывает журнал и доводит базу до состояния, соответствующего последним подтверждённым транзакциям.
Ключевой момент — принудительная запись на стабильное хранилище. Для этого используются операции вроде fsync: они заставляют ОС действительно сбросить данные из кэша на диск. Контрольные точки (checkpoints) периодически сокращают объём журнала и ускоряют восстановление.
Чем чаще делается fsync, тем выше гарантия, но ниже пропускная способность и больше задержки. Чтобы ускориться, системы используют групповой коммит (batching), асинхронные подтверждения и настройку частоты fsync/checkpoints — но часть этих решений уменьшает реальную долговечность.
Перед тем как доверять commit, стоит спросить:
fsync и checkpoints, и как они выставлены в продакшене.Долговечность — это не абстрактная буква D, а договор между СУБД и железом о том, что «успешно» означает «уже сохранено».
Реальные СУБД добиваются свойств ACID инженерными механизмами. Чаще всего вы встретите два подхода: блокировки и MVCC. Оба могут давать изоляцию, но делают это по‑разному и с разными затратами.
Блокировки ограничивают одновременный доступ к данным, чтобы транзакции не мешали друг другу. На практике бывают:
Иногда СУБД делает эскалацию блокировок: вместо множества мелких (на строки) поднимается к более крупной (на страницу/таблицу), чтобы снизить накладные расходы. Побочный эффект — меньше параллелизма.
Если две транзакции ждут ресурсы друг друга, возникает дедлок. СУБД обычно либо автоматически прерывает одну транзакцию, либо вы ограничиваете ожидание через таймауты, после чего приложение получает ошибку и должно корректно повторить операцию.
MVCC (Multi-Version Concurrency Control) хранит несколько версий строк. Читающая транзакция видит «снимок» данных на момент старта (или на момент запроса), а пишущая создаёт новую версию. Поэтому чтения часто не блокируют записи и наоборот.
Цена — больше работы по уборке старых версий (vacuum/cleanup), а конфликты при одновременных записях могут приводить к отмене одной из транзакций.
Блокировки чаще дают изоляцию через ожидание, MVCC — через версии и проверки видимости. Поэтому один и тот же уровень (например, Read Committed) в разных СУБД может означать разные компромиссы по задержкам, конфликтам и потреблению ресурсов.
Чтобы понимать, «что болит», смотрят на:
Правильно выбрать границы транзакции — значит решить, какие изменения должны «жить и умирать вместе». Внутри транзакции держите только то, что обязано быть согласованным в один момент: например, списание со счёта и запись операции в журнал. Всё остальное (уведомления, аналитика, интеграции) лучше вынести за её пределы.
Практичная эвристика: транзакция должна быть короткой и касаться одного хранилища данных. Обновить несколько таблиц — нормально. Сходить по сети или ждать пользователя — признак неверно выбранной границы.
Не оборачивайте транзакцией:
Пока транзакция открыта, база держит ресурсы: блокировки, версии строк, место в журнале. В итоге растут задержки и вероятность конфликтов.
Сбои часто выглядят так: приложение не получило ответ и повторило запрос, хотя первая попытка могла успеть зафиксироваться. Поэтому операции, которые клиент может повторить, делайте идемпотентными: добавляйте idempotency key и храните факт обработки. Тогда повтор приведёт к тому же результату, а не к двойному списанию.
Когда нужно после commit отправить событие/сообщение, используйте шаблон Outbox: в той же транзакции, где меняете бизнес‑данные, записывайте «сообщение к отправке» в таблицу outbox. Отдельный воркер читает outbox и публикует в очередь.
Так вы избегаете сетевых вызовов внутри транзакции и сохраняете согласованность между данными и интеграциями.
Если вы проектируете сервисы с транзакциями (платежи, заказы, склад), удобно быстро проверять границы транзакций, уровни изоляции и идемпотентность на работающем прототипе. В TakProsto.AI можно собрать веб‑и серверное приложение «из чата»: фронтенд на React, бэкенд на Go и PostgreSQL — то есть на стеке, где темы ACID, MVCC и блокировок возникают в реальной практике.
Полезная связка для экспериментов — planning mode (чтобы заранее описать сценарии транзакций и точки конфликтов) и snapshots/rollback (чтобы безопасно откатывать изменения в логике и схеме при проверках под нагрузкой). При необходимости можно выгрузить исходники и продолжить разработку вне платформы.
Распределённая транзакция — это попытка сделать «один commit» сразу для нескольких независимых систем (например, двух баз данных или БД и очереди). Она почти всегда сложнее и дороже локальной транзакции: больше сетевых обменов, больше точек отказа, сложнее восстановление.
Классический протокол — Two‑Phase Commit (2PC). В нём есть координатор (управляет процессом) и участники (ресурсы, которые должны зафиксировать изменения).
Prepare (голосование): координатор спрашивает участников, готовы ли они зафиксировать изменения. Каждый участник либо отвечает «да» и записывает состояние «готов к commit» в свой журнал, либо «нет».
Commit/Abort (решение): если все сказали «да», координатор рассылает команду commit. Если кто-то сказал «нет» (или не ответил вовремя), координатор решает abort и рассылает rollback.
Главная цена 2PC — блокировки и ожидание. Участник, ответивший «да» на prepare, часто вынужден держать ресурсы до получения финального решения. Если координатор недоступен, участники могут зависнуть в «подвешенном» состоянии.
Добавьте частичные отказы (сеть разделилась, один узел перегружен, другой перезапустился) — и диагностика становится заметно сложнее.
Часто практичнее отказаться от «глобального ACID» и перейти к управляемой согласованности:
rollback в прямом смысле, поэтому важно продумать бизнес‑семантику отмены.В обоих подходах критичны идемпотентность, корректные повторы и явное моделирование переходных состояний (например, «платёж в обработке»).
ACID — это не бесплатная «магия». Чем строже требования к изоляции и согласованности, тем чаще база вынуждена ждать: блокировать строки, откатывать конфликты, писать больше данных в журнал транзакций. В результате растут задержки и падает пропускная способность.
Самые заметные потери производительности появляются в трёх ситуациях:
Часто проблему решают не ослаблением гарантий, а дисциплиной работы с данными:
Проблемы изоляции часто проявляются только при реальном параллелизме. Нагрузочные тесты должны имитировать пиковые сценарии (много одновременных оплат/резервов) и измерять не только среднюю задержку, но и хвосты (p95/p99), частоту дедлоков, время ожидания блокировок.
Ослаблять гарантии можно там, где бизнес выдерживает неточность: отчёты «на вчера», кэшируемые витрины, аналитика. Но для денег, остатков, лимитов и прав доступа компромиссы опасны: лучше оптимизировать конфликты и границы транзакций, чем терять смысл бизнес‑правил.
Транзакции могут «ломаться» не только из‑за багов, но и из‑за нагрузки, блокировок, конкуренции и неожиданных повторов. Поэтому ACID — это ещё и про проверяемость: вы должны уметь быстро понять, что произошло с транзакцией, почему и как часто.
Минимальный набор метрик, который обычно окупается уже в первые инциденты:
Добавьте корреляцию в логи и трейсы: request_id → transaction_id (или хотя бы признак «внутри транзакции») и ключевые события: BEGIN, COMMIT, ROLLBACK, повтор. В распределённых системах важно видеть, какие внешние вызовы были сделаны внутри транзакции и не «утекли» ли побочные эффекты при rollback.
Помимо обычных юнит‑тестов нужны сценарии «два клиента одновременно»: потерянные обновления, чтение устаревших данных, двойное списание. Полезны стресс‑тесты с параллельными потоками и намеренными паузами между чтением и записью.
Проверьте: выбран ли уровень изоляции под сценарий, есть ли ретраи на дедлок/конфликт, настроены ли таймауты (на запрос и на транзакцию), и идемпотентны ли критичные операции при повторе запроса.
ACID — не «галочка» в документации СУБД, а набор обещаний, которые вы даёте пользователю: деньги не пропадут, заказ не задвоится, а данные не окажутся в странном промежуточном состоянии. Практичнее всего начинать не с механизмов (MVCC, блокировки), а с ответа на вопрос: какой сбой для продукта неприемлем.
Опишите гарантию простыми фразами: «после подтверждения заказа он не исчезнет», «повторный запрос оплаты не создаст вторую оплату». Зафиксируйте: границы транзакции, выбранный уровень изоляции и что будет при повторе запроса (идемпотентность).
Переоценка нужна при росте нагрузки, появлении микросервисов, новых интеграциях и SLA. Часто меняется допустимый компромисс между задержками и строгостью гарантий.
Если вы развиваете продукт быстро и хотите безопасно итеративно проверять транзакционные сценарии (включая откаты, повторы и конкуренцию), TakProsto.AI может быть удобной средой: приложения разворачиваются на российских серверах, данные не уходят за пределы страны, а в зависимости от масштаба доступны тарифы free/pro/business/enterprise.
Смежные материалы: /blog/transactions-basics и /blog/isolation-levels-guide
ACID — это набор гарантий для транзакций:
commit данные удовлетворяют ограничениям и инвариантам.commit результат переживает сбой.Он важен там, где ошибка означает деньги, доверие или юридически значимые последствия.
Нет. ACID уменьшает класс ошибок, но не делает систему автоматически «правильной».
Практика:
Транзакция объединяет несколько операций в один логический результат.
commit изменения могут быть отменены.rollback откатывает все изменения транзакции.commit изменения считаются успешными и должны сохраниться (durability).Если при сбое вы не уверены, дошёл ли commit, безопаснее опираться на идемпотентность и корректные повторы.
Потому что при параллельной работе появляются аномалии:
Изоляция определяет, какие из этих эффектов допустимы, и какой ценой (ожидания, конфликты, откаты).
Ориентируйтесь на цену ошибки и характер нагрузки:
Уточняйте семантику именно вашей СУБД: одинаковые названия уровней могут вести себя по‑разному.
Сделайте транзакции короткими и «про одно действие»:
Плохой признак — транзакция, которая ждёт пользователя или делает HTTP-запросы: вы удерживаете ресурсы БД и резко повышаете риск конфликтов и дедлоков.
Потому что при сбоях клиент может повторить запрос, не зная, успел ли первый commit.
Практичный подход:
Это критично для оплат, бронирований и любых операций «нельзя дважды».
Используйте Outbox:
Так вы не делаете сетевые вызовы внутри транзакции и сохраняете согласованность «данные ↔ событие». Доставка обычно получается «как минимум один раз», поэтому потребителям нужна дедупликация.
Не всегда. 2PC даёт «единый commit», но дорогой и рискованный:
Часто практичнее:
Смотрите не только на «среднее», а на конфликты и хвосты задержек:
rollback и причины;В логах/трейсах полезна корреляция request_id → «BEGIN/COMMIT/ROLLBACK/RETRY», чтобы быстро понимать судьбу операции.