Вы переводите деньги с одного счёта на другой. С первого счёта сумма списалась, а зачислить её на второй не удалось: сеть упала, сервис завис или база вернула ошибку. В итоге деньги исчезли из одного баланса и не появились в другом. Для таких сценариев и нужны транзакции: они позволяют выполнять связанные операции как единое целое и не оставлять базу в неконсистентном состоянии. В этой статье разберём, как работают транзакции, что означают четыре гарантии ACID и почему из этих четырёх букв при реальной эксплуатации приложений больше всего проблем доставляет буква I — изоляция.
ВАМ ПРИШЛО ПРИГЛАШЕНИЕ 💌
Приходите к нам в соцсети поделиться своим мнением и почитать, что пишут другие. А ещё там выходит дополнительный контент, которого нет на сайте — шпаргалки, опросы и разная дурка. В общем, вот тележка, вот ВК — велком!
Что такое транзакция и что происходит при COMMIT
Транзакция — это логическая единица работы с базой данных: несколько связанных операций выполняются как одно целое. Если всё прошло успешно, изменения фиксируются. Если что-то пошло не так, их можно отменить и вернуть базу к состоянию до начала транзакции.
По умолчанию многие СУБД работают в режиме autocommit: каждый отдельный SQL-запрос автоматически выполняется в собственной транзакции и сразу фиксируется. Для простых операций это удобно, но в бизнес-логике вроде перевода денег нужен контроль сразу над несколькими запросами.
Например, перевод 1000 рублей можно выполнить так:
BEGIN; -- Открытие транзакции
UPDATE accounts SET balance = balance - 1000
WHERE account_id = 1;
UPDATE accounts SET balance = balance + 1000
WHERE account_id = 2;
COMMIT; -- Фиксация изменений
Здесь COMMIT говорит базе: обе операции завершились успешно, изменения можно сделать постоянными. Если второй UPDATE завершится ошибкой, вместо COMMIT выполняют ROLLBACK — база отменяет изменения этой транзакции и возвращается к состоянию, которое было до BEGIN.
В реальном приложении COMMIT и ROLLBACK обычно вызываются из кода в зависимости от результата выполнения операции. Именно это не даёт ситуации, когда деньги уже списали, а зачислить их не успели.
Что означают четыре буквы ACID
ACID — это четыре свойства, которые описывают, как транзакции должны вести себя в надёжной реляционной базе данных. Каждая буква отвечает за свою гарантию.
- Атомарность (Atomicity) гарантирует, что транзакция выполняется целиком или полностью откатывается. Если при переводе денег списание прошло, а зачисление завершилось ошибкой, база не должна оставлять только первую операцию.
- Согласованность (Consistency) означает, что после завершения транзакции база остаётся в состоянии, которое соответствует заданным ограничениям и правилам. Например, при
CHECK (balance >= 0)нельзя записать отрицательный баланс. - Изолированность (Isolation) определяет, насколько параллельные транзакции видят изменения друг друга. Если одна транзакция ещё не зафиксировала списание, другая не должна случайно принять это промежуточное состояние за окончательное. Именно здесь начинаются основные компромиссы между корректностью и производительностью.
- Долговечность (Durability) гарантирует сохранность изменений после
COMMIT. Если клиент уже получил ответ об успешном переводе, перезагрузка сервера не должна стереть эту операцию.
Большую часть этих гарантий СУБД обеспечивает сама. А вот с изоляцией разработчику приходится выбирать подходящий уровень в зависимости от того, какие параллельные операции допустимы в конкретном бизнес-процессе.
Какие аномалии ловит параллельная работа с данными
Когда несколько транзакций одновременно работают с одними и теми же данными, они могут мешать друг другу. В зависимости от уровня изоляции возникают разные аномалии: одна транзакция может увидеть чужие незакоммиченные изменения, получить разные результаты одного запроса или случайно затереть обновление другой транзакции.
Грязное чтение
Транзакция A читает баланс, который транзакция B только что изменила, но ещё не закоммитила. Затем B из-за сбоя внешней интеграции делает ROLLBACK. Получается, A успела поработать с данными, которые в базе официально никогда не существовали.
Неповторяющееся чтение
Транзакция A запрашивает баланс и видит 5000. Пока она работает, транзакция B списывает 1000 и делает COMMIT. A повторяет тот же запрос и получает уже 4000. В рамках одной транзакции одно и то же чтение даёт разные результаты.
Фантомное чтение
Транзакция A запрашивает все операции по счёту за сегодня и получает 10 записей. Параллельно транзакция B добавляет новую операцию и делает COMMIT. Когда A повторяет SELECT с тем же условием, она получает уже 11 записей. Новая строка — это и есть «фантом».
Потерянное обновление
Транзакции A и B одновременно читают баланс 5000. A прибавляет 1000 и записывает 6000. B делает то же самое, опираясь на своё старое значение 5000, и тоже записывает 6000. В итоге два пополнения превратились в одно — изменение A было затёрто обновлением B.
Аномалия сериализации (write skew)
Бизнес-правило: общий баланс двух счетов клиента не должен становиться отрицательным. На счетах 1 и 2 лежит по 1000. Транзакция A проверяет общий баланс 2000, разрешает списание 1500 со счёта 1 и выполняет его. Одновременно B видит те же 2000, разрешает списание 1500 со счёта 2 и тоже выполняет его. Каждая транзакция по отдельности проходит проверку, но вместе они нарушают общее правило: суммарный баланс становится −1000.
Теоретически эти конфликты легко перепутать, особенно если просто смотреть на таблицу уровней изоляции. На практике достаточно один раз воспроизвести грязное чтение, фантом или потерянное обновление в параллельных транзакциях, чтобы увидеть разницу. Такой подход можно отработать на курсах по базам данных от Практикума — там подобные сценарии разбираются на практике, а не только в теории.
Четыре уровня изоляции по стандарту SQL
Стандарт SQL определяет четыре уровня изоляции транзакций. Они задают, какие изменения параллельных транзакций можно увидеть и какие конфликты допускаются при одновременной работе с одними данными. Чем выше уровень изоляции, тем строже гарантии, но обычно тем меньше возможностей для параллельного выполнения операций.
Read Uncommitted
Самый слабый уровень изоляции. Транзакция может читать изменения, которые другие транзакции ещё не зафиксировали. Поэтому возможно грязное чтение. При этом на уровне СУБД сохраняются базовые гарантии целостности самих операций записи, но рассчитывать на согласованную картину данных нельзя.
Read Committed
Транзакция видит только данные, которые уже были зафиксированы. Грязное чтение становится невозможным, но повторный SELECT в рамках одной транзакции может вернуть другой результат: параллельная транзакция успела изменить и закоммитить данные между двумя запросами. Это и есть неповторяющееся чтение.
Repeatable Read
Повторное чтение уже прочитанных строк в рамках транзакции должно возвращать тот же результат. Уровень защищает от неповторяющегося чтения. При этом стандарт SQL допускает фантомное чтение: другая транзакция может добавить новые строки, которые попадут в повторную выборку по тому же условию.
Serializable
Самый строгий уровень. Результат параллельного выполнения транзакций должен быть эквивалентен одному из вариантов, в котором они выполнялись бы последовательно. Это устраняет аномалии конкурентного доступа, включая фантомные чтения и write skew. Цена — снижение параллелизма: при высокой конкуренции транзакции чаще блокируются или получают ошибки сериализации и требуют повторного выполнения.
| Уровень изоляции | Грязное чтение | Неповторяющееся чтение | Фантомное чтение | Аномалия сериализации |
| Read Uncommitted | возможно | возможно | возможно | возможно |
| Read Committed | невозможно | возможно | возможно | возможно |
| Repeatable Read | невозможно | невозможно | возможно | возможно |
| Serializable | невозможно | невозможно | невозможно | невозможно |
Полезный блок со скидкой
По промокоду: KOD (можно просто нажать) — вы получите скидку на все курсы Практикума.
Если хочется писать запросы с блокировками и понимать, что происходит в базе под нагрузкой, посмотрите курс SQL для разработки. Если пока не хватает базы бэкенда, начните с курса Python-разработчик.
Бесплатные вводные курсы в Практикуме тоже есть — например, Основы работы с базами данных и SQL.
Как уровни изоляции реализованы в PostgreSQL
Реляционные СУБД могут по-разному реализовывать уровни изоляции, поэтому поведение PostgreSQL стоит рассматривать отдельно от формулировок стандарта SQL. В PostgreSQL по умолчанию используется Read Committed. Уровень Read Uncommitted поддерживается синтаксически, но фактически работает так же, как Read Committed, поэтому на практике разработчику доступны три разных режима изоляции.
PostgreSQL использует MVCC — многоверсионность данных. У одной строки могут существовать несколько версий, а транзакции работают со своими снимками данных. Благодаря этому чтение не блокирует запись, а запись не блокирует обычное чтение.
Есть важный нюанс: в PostgreSQL на уровне Repeatable Read фантомные чтения невозможны, хотя стандарт SQL их допускает. При этом на Repeatable Read и Serializable транзакция может завершиться ошибкой сериализации, например could not serialize access due to concurrent update. В таком случае приложение должно обработать ошибку и повторить всю транзакцию целиком, а не только упавший запрос.
Например, транзакцию можно явно запустить с уровнем Serializable:
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT balance
FROM accounts
WHERE account_id = 1;
UPDATE accounts
SET balance = balance - 1000
WHERE account_id = 1;
COMMIT;
Как уровни изоляции реализованы в MySQL
В MySQL за конкурентный доступ отвечает движок InnoDB, и его поведение отличается от PostgreSQL. По умолчанию InnoDB использует Repeatable Read, поэтому перенос кода между СУБД может изменить поведение приложения при параллельных запросах.
На уровне Repeatable Read обычный SELECT читает снимок данных, сформированный при первом чтении внутри транзакции. Для блокирующих запросов InnoDB использует gap locks — блокировки промежутков между индексными записями — и next-key locks, которые объединяют блокировку записи с соседним диапазоном. Поэтому другая транзакция может не получить возможность вставить строку в заблокированный диапазон. При точном поиске по уникальному индексу gap lock не нужен.
Если переключиться на Read Committed, gap locks для обычных операций чтения отключаются, а каждый новый SELECT получает свежий снимок данных. Поэтому фантомные чтения становятся возможными.
На уровне Serializable InnoDB ужесточает обычные операции чтения: при отключённом autocommit обычные SELECT фактически выполняются как блокирующие SELECT … FOR SHARE.
Например, при списании денег можно явно заблокировать строку до изменения:
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
-- Блокируем строку до конца транзакции
SELECT balance
FROM accounts
WHERE account_id = 1
FOR UPDATE;
UPDATE accounts
SET balance = balance - 1000
WHERE account_id = 1;
COMMIT;
На таких нюансах — блокирующих чтениях, SELECT … FOR UPDATE и поведении транзакций под нагрузкой — и строится практическая работа с конкурентным доступом. Если хочется глубже разобраться в SQL и научиться проектировать запросы с учётом таких сценариев, это можно отработать на практике на курсах по SQL от Практикума.
Что ломается в проде и как это чинить
На локальной машине транзакции могут работать без сюрпризов, а под реальной нагрузкой внезапно показать все слабые места конкурентного доступа.
- Транзакция падает с ошибкой сериализации. В логах появляется
serialization failure, а операция не завершается. Такая ошибка означает, что параллельные транзакции создали конфликт, который СУБД не может безопасно разрешить. Приложение должно автоматически повторить всю транзакцию, ограничив число попыток и добавив экспоненциальную задержку между ними. Молча проглатывать ошибку нельзя: операция просто не выполнится. - Счётчик финансовых операций теряет инкременты. Две транзакции читают одно значение, прибавляют к нему единицу и записывают результат — один инкремент может затереть другой. Надёжнее выполнить изменение непосредственно в базе:
UPDATE accounts SET balance = balance + 1000, либо заблокировать строку черезSELECT … FOR UPDATEперед изменением. - Две сессии блокируют друг друга и получают deadlock. Например, одна транзакция сначала блокирует строку
A, потом пытается получитьB, а другая делает всё в обратном порядке. Каждая ждёт ресурс, который удерживает другая. Помогают единый порядок обращения к строкам во всей кодовой базе и минимальное время жизни транзакций. - Длинная транзакция удерживает старый снимок данных. В PostgreSQL это может мешать очистке старых версий строк, из-за чего база начинает занимать больше места. Поэтому обращения к внешним API, долгие вычисления и другую медленную работу лучше выполнять за пределами транзакционного блока.
Наконец, ретраи требуют отдельной проверки на идемпотентность, то есть на то, что повторный запуск одной и той же операции не изменит результат и не создаст побочный эффект второй раз. Например, если транзакция успела провести платёж, но приложение не получило ответ и запустило её повторно, без такой защиты клиент может получить двойное списание. Поэтому повторный запуск должен быть безопасным: например, операция должна иметь уникальный идентификатор, по которому система понимает, что такой платёж уже обработан.
Какой уровень изоляции выбрать под задачу
Проектирование архитектуры вынуждает команду самостоятельно выбрать уровень изоляции, балансируя между скоростью работы и консистентностью.
| Задача | Рекомендуемый уровень | Риски | Дополнительные действия |
| Лента новостей и отчеты только на чтение | Read Committed | Чтение немного устаревших версий строк | Контроль нагрузки на диски |
| Списание денег и обновление остатков на складе | Read Committed с блокировками | Потерянные обновления при слабой изоляции | Использование SELECT FOR UPDATE |
| Бронирование билетов и уникальных слотов | Repeatable Read / Serializable | Ошибки сериализации (could not serialize access) | Настройка механизма ретраев на клиенте |
| Аналитический запрос по большому диапазону | Repeatable Read | Блокировка очистки старых версий строк | Запуск запросов на репликах базы данных |
Повышать уровень изоляции следует точечно для конкретных транзакций с критичной бизнес-логикой. Глобальное повышение уровня до Serializable для всего приложения вызывает резкое падение пропускной способности из-за постоянных блокировок. Большая часть повседневных операций прекрасно работает на значениях по умолчанию при правильном использовании явных блокировок.
Частые вопросы про уровни изоляции транзакций
Чем Repeatable Read отличается от Serializable
Repeatable Read гарантирует стабильность повторных чтений в рамках транзакции, но в зависимости от СУБД и реализации может допускать некоторые конфликты записи, например write skew. Serializable требует, чтобы результат параллельного выполнения был эквивалентен последовательному, поэтому такие аномалии не допускаются.
Почему в PostgreSQL и MySQL разные уровни по умолчанию?
СУБД используют разные механизмы управления параллельным доступом. PostgreSQL по умолчанию работает на Read Committed и использует MVCC. В MySQL движок InnoDB по умолчанию использует Repeatable Read и сочетает снимки данных с блокировками, включая gap locks.
Замедляет ли Serializable работу базы?
Более строгая изоляция может снизить производительность при высокой конкуренции: транзакции чаще сталкиваются с конфликтами, откатываются и требуют повторного выполнения. Поэтому Serializable не стоит включать без необходимости для всех операций подряд.
Нужно ли менять уровень изоляции, если в приложении есть ORM?
ORM всё равно отправляет в базу SQL-запросы, поэтому правила изоляции никуда не исчезают. Если бизнес-операция требует определённых гарантий при конкурентном доступе, уровень изоляции нужно учитывать и при настройке ORM. Особенно это важно для операций, где несколько чтений и записей должны выполняться согласованно.
Как проверить текущий уровень изоляции в своей базе
В PostgreSQL нужно выполнить команду SHOW transaction_isolation;. В MySQL для получения значения используется команда SELECT @@transaction_isolation;.
Изоляция транзакций — это компромисс между строгой согласованностью данных и возможностью выполнять операции параллельно. Поэтому уровень изоляции стоит выбирать с учётом конкретной бизнес-операции, а не выставлять один режим для всего приложения. Начните с простого: проверьте уровень изоляции по умолчанию в вашей СУБД и попробуйте выполнить критичную финансовую операцию одновременно из двух транзакций. Такой тест быстро показывает, какие конфликты возникают в реальных условиях и нужна ли дополнительная защита. Понимание транзакций, ACID и уровней изоляции помогает проектировать системы, которые не разваливаются при первой же гонке данных. Глубже разобраться в архитектуре хранения данных и паттернах работы с ними можно на курсе «Инженер данных» от Практикума.
Советуем дальше почитать
Собеседование на Python-разработчика в 2026 году — вопросы по базам данных и ORM, включая транзакции, ACID и проблемы конкурентного доступа, и по проблеме N+1.
Куда расти бэкенд-разработчику в 2026–2027 — пять направлений роста для мидла и сеньора, от архитектуры до MLOps; системный дизайн на границе мидл/сеньор.
Нормализация базы данных: 1НФ, 2НФ, 3НФ на примерах — как проектировать таблицы, чтобы данные не противоречили сами себе; ограничения PRIMARY KEY, FOREIGN KEY, CHECK.
Оконные функции SQL: синтаксис и примеры — OVER, PARTITION BY, ROW_NUMBER, LAG и типичные ошибки; следующий шаг после базовых запросов.
Data Engineer: кто это, чем он занимается и как стать инженером данных: ETL, хранилища, озёра данных.
Бонус для читателей
Если вам интересно погрузиться в мир IT и при этом немного сэкономить, держите наш промокод на курсы Практикума. Он даст вам скидку при оплате, поможет с льготной ипотекой или безлимитом на маркетплейсах. Ладно, окей, это просто скидка, без остального, но хорошая.
