SQL

БЛОК 7. Транзакции — 20. Транзакции и изоляция

📚 23 вопросовПройти тест →
Лекция

БЛОК 7. Транзакции — 20. Транзакции и изоляция

SQL

20. Транзакции и изоляция

🧭 Введение: почему транзакции решают не «SQL-задачу», а бизнес-риск

Когда система работает под нагрузкой, ошибки редко выглядят как «запрос не выполнился».
Гораздо чаще запросы выполняются, но данные остаются в частично обновлённом состоянии: деньги списали, но не зачислили; заказ перевели в paid, но склад не уменьшили.
В этой теме разберём основу production-подхода:
  • BEGIN / COMMIT / ROLLBACK;
  • ACID (обзорно, без избыточной теории);
  • частичные обновления и как их предотвращать;
  • блокировки и конкуренцию;
  • уровни изоляции (обзорно);
  • эталонный сценарий перевода денег.
💡 Совет: Транзакция нужна не «потому что так красиво», а потому что бизнес-операция должна завершаться целиком или не выполняться совсем.
Вывод: Надёжная работа с данными начинается с транзакционного мышления, а не с отдельных SQL-операторов.

⚠️ Проблема -> решение

Типичная проблема:
  1. разработчик делает несколько UPDATE/INSERT как отдельные запросы;
  2. один из шагов падает;
  3. часть данных уже изменилась и система попадает в неконсистентное состояние.
Решение:
  1. группировать связанные изменения в транзакцию;
  2. при ошибке делать ROLLBACK;
  3. выбирать адекватный уровень изоляции под бизнес-сценарий;
  4. контролировать блокировки и порядок работы со строками.
🟢 Если совсем просто: Одна бизнес-операция должна жить в одной транзакции.
🎯 Как понять, что этап прошёл успешно: После ошибки база остаётся в том же состоянии, что и до старта операции.

🛠️ Чем помогает и как работает

Транзакции нужны в каждом домене, где важна корректность:
  • платежи;
  • остатки на складе;
  • биллинг и подписки;
  • статусы заказов и отгрузок;
  • синхронизация нескольких связанных таблиц.
🟢 Если совсем просто: Транзакция собирает несколько запросов в один «атомарный пакет».
🎯 Как понять, что этап прошёл успешно: Вы можете объяснить для каждой критичной операции, где начинается BEGIN и где заканчивается COMMIT/ROLLBACK.
Чем помогает:
  • защищает от частичных обновлений;
  • сохраняет согласованность между таблицами;
  • упрощает разбор инцидентов;
  • снижает риск «тихих» багов при конкуренции.
Как это работает:
  • Шаг 1: открываем транзакцию (BEGIN);
  • Шаг 2: выполняем связанные запросы;
  • Шаг 3: при успехе фиксируем (COMMIT);
  • Шаг 4: при ошибке откатываем (ROLLBACK);
  • Шаг 5: контролируем изоляцию и блокировки под нагрузкой.
Вывод: Транзакция превращает набор SQL-запросов в управляемую бизнес-операцию.

📚 Ключевые термины (простыми словами)

Перед практикой синхронизируем словарь.
🟢 Если совсем просто: Без общих терминов сложно обсуждать инциденты и ревьюить SQL.
🎯 Как понять, что этап прошёл успешно: Вы уверенно различаете rollback, lock, isolation level и deadlock.
  • Transaction (транзакция) — единица работы БД, которая фиксируется целиком или откатывается.
  • BEGIN — старт транзакции.
  • COMMIT — зафиксировать изменения.
  • ROLLBACK — отменить изменения текущей транзакции.
  • Atomicity (атомарность) — «всё или ничего».
  • Consistency (согласованность) — данные после операции остаются валидными по правилам схемы.
  • Isolation (изоляция) — параллельные транзакции не ломают друг другу логику.
  • Durability (надёжность фиксации) — после COMMIT данные не теряются при сбое.
  • Lock (блокировка) — механизм, который временно ограничивает конкурентный доступ к данным.
  • Deadlock (взаимная блокировка) — транзакции ждут друг друга по кругу.
  • Isolation level (уровень изоляции) — степень защиты от аномалий параллельного чтения/записи.
  • Partial update (частичное обновление) — часть шагов операции выполнилась, часть нет.
Вывод: Эти термины покрывают 90% разговоров о транзакционных проблемах на проекте.

🧱 1. BEGIN / COMMIT / ROLLBACK: каркас безопасных изменений

Любая серьёзная операция начинается не с UPDATE, а с определения границ транзакции.
Именно границы определяют, что считать одной логической операцией.
🟢 Если совсем просто: BEGIN открывает пакет изменений, COMMIT подтверждает, ROLLBACK отменяет.
🎯 Как понять, что этап прошёл успешно: Вы можете показать SQL-блок, который либо полностью фиксируется, либо полностью откатывается.
Назначение: Объединить связанные SQL-действия в одну управляемую единицу.
Простыми словами: Сначала открываем транзакцию, потом делаем шаги операции, затем фиксируем или отменяем.
Для новичка: Если запросов несколько и они логически связаны, почти всегда нужна транзакция.
Аналогия: Это как банковский перевод в кассе: либо оформлен полностью, либо отменён.
Пример:
BEGIN; UPDATE ordersSET status = 'paid'WHERE id = 1001; INSERT INTO payments (order_id, amount, status)VALUES (1001, 2500.00, 'captured'); COMMIT;
BEGIN; UPDATE ordersSET status = 'paid'WHERE id = 1002; ROLLBACK;
🔎 Как это происходит на практике:
  • Контекст: API-ручка выполняет 2-3 SQL-запроса в рамках одной операции.
  • Действия: транзакция открывается на уровне сервиса перед первым запросом.
  • Результат: при ошибке на шаге 2 шаг 1 не остаётся «висячим».
Характеристики:
  • транзакционные границы задают целостность;
  • откат работает только до COMMIT;
  • без явных границ риск частичных изменений резко выше.
Когда использовать: Для любой операции, где несколько изменений должны быть согласованы.
Вывод: BEGIN/COMMIT/ROLLBACK — обязательный каркас для критичных бизнес-операций.

🧪 2. ACID (обзорно): что именно гарантирует БД

ACID часто воспринимают как «теория для собеседования», но это практический чек-лист риска.
Если вы понимаете ACID, вы заранее видите, где операция может сломаться под нагрузкой.
🟢 Если совсем просто: ACID описывает, почему данным можно доверять после выполнения транзакции.
🎯 Как понять, что этап прошёл успешно: Вы можете связать каждую букву ACID с реальным поведением запроса.
Назначение: Зафиксировать базовые гарантии корректной обработки данных.
Простыми словами: ACID = атомарность, согласованность, изоляция, надёжность фиксации.
Для новичка: Если «деньги списались, но не дошли» — это нарушение атомарности.
Пример:
BEGIN; UPDATE accountsSET balance = balance - 500WHERE id = 10; UPDATE accountsSET balance = balance + 500WHERE id = 20; COMMIT;
🔎 Как это происходит на практике:
  • Atomicity: без второй операции первая не должна остаться в базе.
  • Consistency: ограничения (CHECK, FK) защищают валидность данных.
  • Isolation: параллельные переводы не должны «перетирать» друг друга.
  • Durability: после COMMIT результат не исчезает после рестарта.
Характеристики:
  • ACID не отменяет необходимость корректного SQL;
  • ACID работает лучше всего с продуманными ограничениями схемы;
  • ACID не заменяет бизнес-валидацию в приложении.
Когда использовать: Всегда, когда операция влияет на деньги, остатки, статусы и отчётность.
Вывод: ACID — это инженерная модель качества данных, а не абстрактный термин.

🧩 3. Частичное обновление: почему без транзакции процесс ломается

Главная практическая боль — «полуоперации»: первый шаг прошёл, второй упал.
В результате система внешне работает, но данные уже расходятся с бизнес-логикой.
🟢 Если совсем просто: Частичное обновление — это когда операция завершилась наполовину.
🎯 Как понять, что этап прошёл успешно: Вы можете показать, как один ROLLBACK убирает риск «полуобновлённых» данных.
Назначение: Не допустить состояний, которые невозможно корректно объяснить бизнесу.
Простыми словами: Если шагов несколько, они должны откатываться и фиксироваться как единый блок.
Для новичка: Два UPDATE подряд без транзакции — уже потенциальный инцидент.
Аналогия: Это как оформить заказ без создания оплаты: в интерфейсе заказ есть, а бизнес-процесс не закрыт.
Пример:
UPDATE ordersSET status = 'paid'WHERE id = 2001; UPDATE inventorySET reserved = reserved - 1WHERE sku = 'SKU-42';
BEGIN; UPDATE ordersSET status = 'paid'WHERE id = 2001; UPDATE inventorySET reserved = reserved - 1WHERE sku = 'SKU-42'; COMMIT;
🔎 Как это происходит на практике:
  • Контекст: checkout меняет заказ, оплату и склад.
  • Действия: любой сбой на одном шаге приводит к ROLLBACK.
  • Результат: нет «половинчатых» бизнес-состояний.
Характеристики:
  • частичное обновление особенно опасно в интеграциях;
  • последствия часто проявляются не сразу, а в отчётах;
  • откат дешевле, чем восстановление вручную.
Когда использовать: Для всех multi-step операций в одной БД.
Вывод: Транзакции закрывают класс ошибок «выполнилось наполовину».

🔒 4. Блокировки: как БД защищает данные при конкуренции

Когда много запросов работают одновременно, БД должна координировать доступ к строкам.
Без блокировок два процесса могут принять решение на устаревших данных и сделать конфликтные изменения.
🟢 Если совсем просто: Блокировка — это «занято», пока транзакция не закончится.
🎯 Как понять, что этап прошёл успешно: Вы знаете, какие строки блокируются и как избежать долгих ожиданий.
Назначение: Защитить конкурентные операции от гонок и потери обновлений.
Простыми словами: Одна транзакция временно «захватывает» строку, другие ждут или обходят.
Для новичка: SELECT ... FOR UPDATE — базовый способ заблокировать строки перед изменением.
Пример:
BEGIN; SELECT  id,  balanceFROM accountsWHERE id IN (10, 20)FOR UPDATE; UPDATE accountsSET balance = balance - 500WHERE id = 10; UPDATE accountsSET balance = balance + 500WHERE id = 20; COMMIT;
🔎 Как это происходит на практике:
  • Контекст: несколько переводов одновременно затрагивают одни и те же счета.
  • Действия: блокируем строки перед проверкой и обновлением.
  • Результат: нет гонки «оба потока увидели старый баланс».
Характеристики:
  • блокировки удерживаются до COMMIT/ROLLBACK;
  • длинные транзакции увеличивают время ожидания других запросов;
  • разный порядок блокировки может привести к deadlock.
  • deadlock в production — нормальный технический кейс, его нужно уметь корректно ретраить.
Когда использовать: Когда решение зависит от текущего значения строки и затем эта строка обновляется.
Вывод: Управление блокировками — ключ к корректности под параллельной нагрузкой.

🧪 5. Уровни изоляции (обзорно): баланс корректности и производительности

Изоляция отвечает за то, что одна транзакция может «видеть» из действий другой.
Чем выше уровень изоляции, тем меньше аномалий, но тем выше цена по блокировкам/повторам.
🟢 Если совсем просто: Уровень изоляции — это правило, насколько «независимы» параллельные транзакции.
🎯 Как понять, что этап прошёл успешно: Вы можете обосновать, почему для сценария выбран именно этот уровень.
Назначение: Снизить риск аномалий чтения/записи в конкурентных сценариях.
Простыми словами: Это настройка «строгости» параллельного доступа.
Для новичка: В PostgreSQL по умолчанию READ COMMITTED, и для многих CRUD-задач этого достаточно.
Пример:
BEGIN; SET LOCAL TRANSACTION ISOLATION LEVEL REPEATABLE READ; SELECT  COUNT(*) AS pending_ordersFROM ordersWHERE status = 'pending'; COMMIT;
🔎 Как это происходит на практике:
  • READ COMMITTED: меньше ограничений, выше шанс «изменившихся данных между чтениями».
  • REPEATABLE READ: стабильнее повторные чтения в транзакции.
  • SERIALIZABLE: максимальная строгость, возможны retriable-ошибки, которые нужно повторять.
Короткая карта аномалий (для ревью):
  • Dirty read (грязное чтение) — почти нигде не допускается в современных OLTP-СУБД.
  • Non-repeatable read (повторное чтение изменилось) — возможно на READ COMMITTED.
  • Phantom read (в диапазоне появились новые строки) — зависит от СУБД и уровня изоляции.
  • Write skew — типичный MVCC-кейс, когда два корректных решения вместе нарушают общий инвариант.
Практика по инвариантам: Инварианты лучше фиксировать ограничениями (UNIQUE, CHECK), ключами и явными блокировками, а не полагаться только на уровень изоляции.
Характеристики:
  • уровень изоляции задаётся под сценарий, а не «везде максимальный»;
  • слишком высокий уровень без необходимости ухудшает throughput;
  • выбор уровня должен быть зафиксирован в архитектурных правилах.
Практика для SERIALIZABLE:
  • ловим ошибку сериализации в приложении;
  • повторяем транзакцию целиком 1-3 раза с небольшим backoff;
  • проектируем операцию идемпотентной (или с ключом операции), чтобы ретрай не дал двойной эффект.
Когда использовать: Когда параллельные операции критично влияют на бизнес-результат.
Вывод: Изоляция — это осознанный компромисс между корректностью и производительностью.

💸 6. Пример перевода денег: эталонная транзакция

Перевод денег — самый наглядный сценарий, где видно ценность транзакции, блокировок и изоляции.
Здесь важно не только «обновить баланс», но и корректно пережить конкуренцию и ошибки.
🟢 Если совсем просто: Перевод = списать, зачислить, записать лог в одной транзакции.
🎯 Как понять, что этап прошёл успешно: Невозможно получить состояние, где деньги «пропали между счетами».
Назначение: Показать боевой шаблон для критичных финансовых операций.
Простыми словами: Операция должна быть атомарной и защищённой от гонок.
Для новичка: Сначала блокируем счета, потом проверяем баланс, потом делаем обновления.
Пример:
BEGIN; SET LOCAL TRANSACTION ISOLATION LEVEL REPEATABLE READ; SELECT  id,  balanceFROM accountsWHERE id IN (10, 20)ORDER BY idFOR UPDATE; UPDATE accountsSET balance = balance - 500WHERE id = 10  AND balance >= 500; UPDATE accountsSET balance = balance + 500WHERE id = 20; INSERT INTO transfers (from_account_id, to_account_id, amount, status)VALUES (10, 20, 500, 'completed'); COMMIT;
Production-усиление 1: проверка row count после списания
UPDATE accountsSET balance = balance - 500WHERE id = 10  AND balance >= 500; SELECT ROW_COUNT();
Если списание затронуло 0 строк, операция должна завершиться ROLLBACK с ошибкой «недостаточно средств».
Production-усиление 2: идемпотентность перевода
INSERT INTO transfers (idempotency_key, from_account_id, to_account_id, amount, status)VALUES ('trf-2026-03-05-1001', 10, 20, 500, 'pending');
idempotency_key должен быть UNIQUE, чтобы повторный ретрай не создал двойной перевод.
🔎 Как это происходит на практике:
  • Контекст: несколько параллельных переводов между одними и теми же счетами.
  • Действия: блокируем строки и применяем единый транзакционный сценарий.
  • Результат: нет потери/дублирования суммы и есть аудиторский след.
Характеристики:
  • нужна проверка бизнес-условий (например, достаточность баланса);
  • нужен единый порядок блокировки для снижения риска deadlock;
  • нужен лог операции для аудита и расследований.
Когда использовать: Для платежей, бонусных баллов, внутреннего биллинга и любых переводов между сущностями.
Вывод: Транзакционный перевод денег — базовый шаблон для критичных операций в OLTP-системе.

🚩 7. Красный флаг: не держим транзакцию во время внешних вызовов

Самая дорогая ошибка в production — открыть BEGIN, затем делать HTTP/queue/API вызов и только потом продолжать SQL.
Такая транзакция держит блокировки дольше, растит конфликты и может «положить» throughput под нагрузкой.
🟢 Если совсем просто: Снаружи транзакции делаем сеть, внутри транзакции делаем только БД.
🎯 Как понять, что этап прошёл успешно: В транзакционном блоке нет внешних вызовов, только SQL к одной БД.
Антипаттерн:
BEGIN; UPDATE ordersSET status = 'processing'WHERE id = 7001; SELECT pg_sleep(5); COMMIT;
Правильный подход:
  • внешний вызов выполняется до транзакции или после неё;
  • если нужно гарантированно отправить событие после COMMIT, используем outbox-паттерн (см. ниже).
Вывод: Короткая транзакция = меньше блокировок, меньше конфликтов, стабильнее прод.

🧷 8. SAVEPOINT: частичный откат внутри транзакции

Иногда операция в целом должна завершиться успешно, но один побочный шаг может быть опциональным.
Для таких случаев используют SAVEPOINT и ROLLBACK TO SAVEPOINT.
🟢 Если совсем просто: SAVEPOINT — это «точка возврата» внутри одной транзакции.
🎯 Как понять, что этап прошёл успешно: Вы можете откатить локальный шаг, не отменяя всю операцию.
Пример:
BEGIN; UPDATE accountsSET updated_at = NOW()WHERE id = 10; SAVEPOINT after_balance; INSERT INTO audit_log (entity, action)VALUES ('accounts', 'balance_updated'); ROLLBACK TO SAVEPOINT after_balance; COMMIT;
Вывод: SAVEPOINT даёт тонкий контроль: общий успех операции + локальный откат побочных действий.

🧠 9. Lost update и шаблон read-modify-write

Lost update — классическая проблема конкуренции, когда одно изменение «перетирает» другое.
Особенно часто проявляется в логике «прочитал -> вычислил -> записал».
🟢 Если совсем просто: Два потока читают старое значение и пишут разные новые, одно обновление теряется.
🎯 Как понять, что этап прошёл успешно: В критичных read-modify-write операциях есть защита (FOR UPDATE или optimistic locking).
Плохой сценарий (потеря обновления):
  1. T1 читает balance = 1000;
  2. T2 читает balance = 1000;
  3. T1 пишет 900;
  4. T2 пишет 800 -> итог перетёр изменение T1.
Шаблон защиты (пессимистично):
BEGIN; SELECT id, balanceFROM accountsWHERE id = 10FOR UPDATE; UPDATE accountsSET balance = balance - 100WHERE id = 10; COMMIT;
Вывод: Lost update — одна из главных причин, почему FOR UPDATE нужен не «для красоты», а для корректности.

⚖️ 10. Optimistic locking: альтернатива тяжёлым блокировкам

Для высоконагруженных API часто используют оптимистичный контроль версий (version/updated_at).
Идея: обновление проходит только если версия совпадает с ожидаемой.
🟢 Если совсем просто: Если кто-то успел изменить запись раньше вас, ваш UPDATE не применится.
🎯 Как понять, что этап прошёл успешно: При конфликте версия не совпадает, приложение видит rowcount = 0 и обрабатывает конфликт.
Пример:
UPDATE ordersSET  status = 'paid',  version = version + 1WHERE id = 1001  AND version = 7;
Если изменено 0 строк, нужно перечитать запись и повторить бизнес-логику (или вернуть 409 Conflict).
Вывод: Optimistic locking хорошо масштабируется и прозрачно сигнализирует о конфликте параллельных изменений.

🧵 11. Очереди и воркеры: FOR UPDATE SKIP LOCKED (PostgreSQL)

В задачах типа job-queue важно, чтобы воркеры не ждали друг друга на одних и тех же строках.
SKIP LOCKED позволяет каждому воркеру брать только свободные задачи.
🟢 Если совсем просто: Воркеры параллельно «разбирают очередь», не блокируя друг друга ожиданием.
🎯 Как понять, что этап прошёл успешно: Каждый воркер получает свой пакет задач без конфликтов и долгих wait.
Пример:
BEGIN; WITH picked AS (  SELECT id  FROM jobs  WHERE status = 'new'  ORDER BY created_at  LIMIT 50  FOR UPDATE SKIP LOCKED)UPDATE jobs jSET status = 'processing'FROM picked pWHERE j.id = p.idRETURNING j.*; COMMIT;
Вывод: SKIP LOCKED — production-стандарт для параллельной обработки очередей в PostgreSQL.

📦 12. Outbox pattern: надёжный мост из БД во внешние события

Проблема интеграций: COMMIT прошёл, а публикация в брокер не ушла, или ушла дважды.
Outbox решает это через одну транзакцию: доменные изменения + запись в outbox.
🟢 Если совсем просто: Сначала надёжно фиксируем событие в БД, потом отдельный воркер публикует наружу.
🎯 Как понять, что этап прошёл успешно: Нет сценария «данные изменились, а событие потерялось».
Пример:
BEGIN; UPDATE ordersSET status = 'paid'WHERE id = 9001; INSERT INTO outbox (event_type, payload, status, created_at)VALUES ('order.paid', '{\"orderId\":9001}', 'new', NOW()); COMMIT;
Отдельный воркер читает outbox, публикует в Kafka/queue/webhook и помечает событие как отправленное.
Вывод: Outbox закрывает класс багов «COMMIT прошёл, событие не ушло».

🧰 13. Autocommit, ORM и видимые границы транзакции

Частый прод-баг: команда думает, что операция транзакционная, а фактически каждый statement в autocommit.
Плюс ORM может открывать implicit-транзакции, а пул соединений требует аккуратного закрытия.
🟢 Если совсем просто: Границы транзакции должны быть явно видны в коде сервиса.
🎯 Как понять, что этап прошёл успешно: В коде и тестах явно видно, где начинается и где заканчивается транзакция.
Правило:
  • старт/конец транзакции фиксируются в сервисном слое;
  • в тестах проверяется rollback-путь;
  • соединение всегда возвращается в пул без «висячей» транзакции.
Вывод: Явные транзакционные границы в коде важны так же, как корректный SQL.

🆚 Сравнение: без транзакции vs с транзакцией

ПодходЧто происходитРискКогда уместно
Несколько независимых запросовКаждый шаг живёт отдельноЧастичное обновление, гонкиТолько для некритичных, независимых действий
Транзакция BEGIN ... COMMITШаги фиксируются вместеНиже риск, выше контрольСтандарт для связанных изменений
Транзакция + блокировки + изоляцияКонтролируется конкуренцияСложнее код, но выше корректностьФинансы, остатки, статусы под нагрузкой
Вывод: Чем выше бизнес-цена ошибки, тем жёстче должен быть транзакционный контур.

🧠 Must-Know (запомнить)

  • Транзакции держим короткими: внешние HTTP/queue/API вызовы выполняем вне транзакции.
  • Одна бизнес-операция = одна транзакция с явным BEGIN/COMMIT/ROLLBACK.
  • Без транзакции multi-step операция уязвима к частичному обновлению.
  • Для read -> check -> write используем FOR UPDATE или optimistic locking.
  • Для optimistic locking проверяем rowcount: 0 строк = конфликт версии.
  • Для перевода денег проверяем rowcount списания: 0 строк = недостаточно средств -> ROLLBACK.
  • Для снижения deadlock блокируем сущности всегда в одном порядке (например, по id).
  • Deadlock — нормальный production-кейс, его нужно ретраить в коде.
  • SERIALIZABLE может требовать повторов 1-3 раза с backoff.
  • Для очередей/воркеров используем FOR UPDATE SKIP LOCKED (PostgreSQL).
  • Для надёжной публикации событий после COMMIT используем outbox-паттерн.
  • Границы транзакции должны быть явно видны в коде сервиса и тестах (autocommit/ORM под контролем).
Вывод: Надёжность транзакций — это комбинация границ, блокировок и осознанной изоляции.

❌ Частые мифы

Миф: Транзакции нужны только для банков. ✅ Как правильно: Они нужны в любом сценарии, где несколько изменений должны быть согласованы. 📎 Почему это важно: Частичные обновления ломают не только финансы, но и статусы, отчёты, остатки.
Миф: Если запросы быстрые, транзакция не нужна. ✅ Как правильно: Скорость не гарантирует атомарность и защиту от конкуренции. 📎 Почему это важно: Инциденты возникают именно при редких ошибках в многопоточном режиме.
Миф: Надо всегда ставить SERIALIZABLE. ✅ Как правильно: Уровень изоляции выбирают под конкретный риск и нагрузку. 📎 Почему это важно: Избыточная строгость может ухудшить производительность без бизнес-пользы.
Миф: Deadlock — это «проблема БД», а не кода. ✅ Как правильно: Чаще всего deadlock вызывается разным порядком доступа к одинаковым данным в коде. 📎 Почему это важно: Архитектурное правило порядка блокировок резко снижает частоту deadlock.

🎤 Часто спрашивают на собеседованиях

Вопрос: В чём разница между COMMIT и ROLLBACK? ✅ Ответ: COMMIT фиксирует изменения транзакции, ROLLBACK полностью отменяет их.
Вопрос: Что такое частичное обновление? ✅ Ответ: Это состояние, когда часть шагов бизнес-операции сохранилась в БД, а часть нет.
Вопрос: Зачем нужен ACID в практической разработке? ✅ Ответ: Он задаёт гарантии корректности и помогает проектировать безопасные операции под нагрузкой.
Вопрос: Когда применять SELECT ... FOR UPDATE? ✅ Ответ: Когда вы читаете значение и далее принимаете решение об обновлении этой же строки.
Вопрос: Почему возникает deadlock? ✅ Ответ: Из-за циклического ожидания блокировок, часто при разном порядке доступа к одним и тем же строкам.
Вопрос: Какой уровень изоляции обычно стоит по умолчанию в PostgreSQL? ✅ Ответ: READ COMMITTED.
Вопрос: Что важно в примере перевода денег? ✅ Ответ: Атомарность шагов, блокировка затрагиваемых счетов, проверка баланса и журналирование операции.
Вопрос: Почему SERIALIZABLE требует ретраи? ✅ Ответ: Потому что БД может отклонить транзакцию при конфликте сериализации, и её нужно повторить в приложении.
Вопрос: Зачем нужен SAVEPOINT, если уже есть ROLLBACK? ✅ Ответ: SAVEPOINT позволяет откатить только часть шагов внутри транзакции, не отменяя операцию целиком.
Вопрос: Когда лучше optimistic locking, а не FOR UPDATE? ✅ Ответ: Когда важна масштабируемость и допустим явный конфликт версий с повтором логики в приложении.
Вопрос: Для чего используют FOR UPDATE SKIP LOCKED? ✅ Ответ: Для параллельной разборки очередей воркерами без взаимного ожидания одних и тех же строк.
Вопрос: Что решает outbox-паттерн? ✅ Ответ: Он гарантирует согласованность между изменениями в БД и последующей отправкой событий во внешние системы.

🚨 Типичные ошибки

  • Выполнять связанные UPDATE/INSERT вне транзакции.
  • Держать транзакцию открытой во время долгих внешних вызовов.
  • Читать данные и обновлять их позже без блокировки в конкурентном сценарии.
  • Выбирать уровень изоляции «по умолчанию» без проверки рисков.
  • Не обрабатывать ретраи при SERIALIZABLE.
  • Блокировать одни и те же сущности в разном порядке в разных сервисах.
  • Не проверять row count после критичных обновлений.
  • Не писать запись в журнал операций при финансовых изменениях.
  • Не делать операцию идемпотентной и получать двойной эффект на ретраях.
  • Пытаться отправлять внешнее событие сразу после SQL без outbox-гарантий.
  • Полагаться на implicit-транзакции ORM и не видеть реальные границы BEGIN/COMMIT.
Вывод: Большинство проблем с транзакциями предсказуемы и устраняются дисциплиной шаблонов.

✅ Best Practices

  • Формулируйте бизнес-операцию до написания SQL и определяйте её транзакционные границы.
  • Делайте транзакции короткими: не держите их открытыми дольше необходимого.
  • Для критичных read-modify-write сценариев используйте явные блокировки.
  • Внешние вызовы (HTTP, брокер, очередь) выполняйте вне транзакции.
  • Фиксируйте единый порядок работы с сущностями (from_id < to_id) для снижения deadlock.
  • Выбирайте изоляцию под риск: не минимальную «по привычке» и не максимальную «на всякий случай».
  • Логируйте результат критичных транзакций (transfer, payment, status_change).
  • Для SERIALIZABLE заранее реализуйте стратегию повторов в приложении.
  • Для денежных и retry-чувствительных операций добавляйте idempotency_key.
  • Для интеграций «БД -> событие» используйте outbox + отдельный паблишер.
  • Для очередей воркеров используйте FOR UPDATE SKIP LOCKED в PostgreSQL.
  • Для частичного отката неосновных шагов используйте SAVEPOINT.
  • Явно фиксируйте границы транзакции в коде сервиса, не полагаясь на implicit ORM-поведение.
  • Добавляйте мониторинг lock-wait/deadlock в проде.
Вывод: Production-ready транзакции — это не только SQL, но и операционная дисциплина команды.

🏁 Заключение

Транзакции и изоляция — это фундамент надёжной OLTP-разработки.
Если вы умеете правильно задавать границы операции, работать с блокировками и осознанно выбирать уровень изоляции, система остаётся предсказуемой даже при высокой конкуренции.
Вывод: Надёжная база данных = корректные транзакции + контроль конкуренции + явные инженерные правила.
🎯

Проверьте знания

Закрепите материал — пройдите тест по теме «БЛОК 7. Транзакции — 20. Транзакции и изоляция»

Пройти тест →