Объяснение транзакций SQL и ACID: COMMIT, ROLLBACK и уровни изоляции

  1. Главная
  2. Блог
  3. Базы данных и SQL

Базы данных и SQL

Объяснение транзакций SQL и ACID: COMMIT, ROLLBACK и уровни изоляции

Что произойдет, если сервер выйдет из строя на полпути перевода денег между двумя учетными записями? Транзакции – это ответ. Посмотрите, как они работают на реальном примере, что такое ACID и как выбрать уровень изоляции.

9 мин чтения
Объяснение транзакций SQL и ACID: COMMIT, ROLLBACK и уровни изоляции

Главное

  • Транзакция группирует операторы так, что все они либо завершаются успешно, либо все вместе завершаются неудачно.
  • COMMIT делает изменения постоянными, а ROLLBACK отменяет их.
  • ACID означает атомарность, согласованность, изоляцию и долговечность.
  • Более строгая изоляция предотвращает появление большего количества аномалий, но может уменьшить параллелизм, поэтому выбирайте обдуманно.

Перемещение 300 рупий из Аши в Бен требует двух обновлений: вычесть из одного счета и добавить в другой. Если система между ними дает сбой, деньги исчезают. Транзакции SQL делают это невозможным, поскольку оба шага рассматриваются как единое целое.

В этом руководстве мы рассмотрим фиксации, откаты и сбои в небольшой таблице учетных записей. Мы запускали эти примеры в SQLite, и показанные балансы получены в результате этого запуска. Основные команды одинаковы для разных баз данных, но поведение блокировки и изоляции сильно различается, поэтому мы указываем, где можно проверить документацию вашей собственной базы данных.

Что такое транзакция?

Транзакция — это последовательность одного или нескольких операторов SQL, которую база данных обрабатывает как одну логическую операцию. Вот наша таблица:

CREATE TABLE accounts (
  id INT PRIMARY KEY,
  owner TEXT,
  balance INT CHECK (balance >= 0)
);

INSERT INTO accounts VALUES (1, 'Asha', 1000), (2, 'Ben', 200);

Ограничение CHECK — это правило, согласно которому баланс никогда не может опускаться ниже нуля, которое мы будем использовать позже. Обратите внимание, что синтаксис начала транзакции различается: SQLite и PostgreSQL принимают BEGIN, MySQL также имеет START TRANSACTION, а SQL Server использует BEGIN TRANSACTION.

За пределами явной транзакции большинство баз данных используют autocommit, где каждый оператор сохраняется сразу после его завершения. Oracle является заметным исключением, поскольку сеанс там остается в транзакции до тех пор, пока вы не зафиксируете ее.

COMMIT: сделать это постоянным

BEGIN;
UPDATE accounts SET balance = balance - 300 WHERE id = 1;
UPDATE accounts SET balance = balance + 300 WHERE id = 2;
COMMIT;

-- Asha | 700
-- Ben  | 500

Пока COMMIT не будет запущено, изменения являются предварительными. После успешного завершения база данных гарантирует, что они будут сохранены. Сумма по обоим счетам осталась на уровне 1200, в этом и состоит весь смысл упражнения.

ОТКАТ: отменить все

ROLLBACK отменяет все с момента начала транзакции:

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
SELECT owner, balance FROM accounts;
-- Asha | 600   (visible inside this transaction)
-- Ben  | 500

ROLLBACK;
SELECT owner, balance FROM accounts;
-- Asha | 700   (back to where it was)
-- Ben  | 500

Откат также происходит автоматически, если соединение разрывается или сервер выходит из строя до того, как вы зафиксируете транзакцию, поэтому незавершенная транзакция не сохраняется.

Когда утверждение терпит неудачу

Вот более реалистичный случай. Аша пытается отправить 5000, которых нет на счету. Сначала идет кредит Бену, а затем дебет, что нарушает правило CHECK:

BEGIN;
UPDATE accounts SET balance = balance + 5000 WHERE id = 2;
UPDATE accounts SET balance = balance - 5000 WHERE id = 1;
-- error: CHECK constraint failed: balance >= 0

ROLLBACK;

-- Asha | 700
-- Ben  | 500

Без этой сделки Бен оставил бы себе 5000, и бухгалтерские книги были бы неправильными. Помните об одной важной детали: неудачный оператор не всегда отменяет транзакцию сам по себе. Например, в SQLite и MySQL более ранние инструкции могут оставаться ожидающими, поэтому ваш код должен выполнить откат. Вместо этого PostgreSQL помечает всю транзакцию как прерванную и отказывается от дальнейших операторов, пока вы не откатитесь.

В коде приложения всегда связывайте транзакцию с обработкой ошибок. Вот идея Python, и наше руководство по обработке исключений Python объясняет синтаксис:

try:
    conn.execute("BEGIN")
    conn.execute(debit_sql, values)
    conn.execute(credit_sql, values)
    conn.execute("COMMIT")
except Exception:
    conn.execute("ROLLBACK")
    raise

Точки сохранения

Точка сохранения отмечает позицию внутри транзакции, поэтому вы можете отменить только часть работы:

BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 1;
SAVEPOINT bonus;
UPDATE accounts SET balance = balance + 999 WHERE id = 2;
ROLLBACK TO bonus;
UPDATE accounts SET balance = balance + 50 WHERE id = 2;
COMMIT;

-- Asha | 650
-- Ben  | 550

Бонус 999 был отменен, а остальная часть транзакции была продолжена и зафиксирована. Это полезно для дополнительных шагов в длительном процессе.

Объяснение свойств ACID

Недвижимость Обещание В нашем переводе
атомарность Все или ничего Оба обновления происходят, или ни одно не происходит
Консистенция Правила остаются в силе и после этого Никакой баланс не может нарушить правило CHECK
Изоляция Параллельная работа не мешает Остальные не видят полузаконченной передачи
Долговечность Зафиксированные данные выдерживают сбой Отключение электроэнергии после COMMIT ничего не теряет

Последовательность обычно понимают неправильно. База данных применяет только объявленные вами правила, такие как ограничения и внешние ключи. Это не помешает вашему коду перевести неверную сумму. Надежность обычно достигается с помощью журнала упреждающей записи, в котором изменения безопасно записываются на диск до подтверждения фиксации.

Что происходит с параллельными транзакциями?

Мы открыли два подключения к одному файлу SQLite. Первый запустил транзакцию и обновил строку с 1 до 2, еще не зафиксировав ее. Вот что увидело второе соединение:

Connection B reads:   1                     -- no uncommitted data
Connection B writes:  database is locked    -- it has to wait
(A commits)
Connection B reads:   2                     -- now it sees the change

SQLite позволяет одновременно записывать только одну запись, что проще, чем серверные базы данных, где множество транзакций могут одновременно записывать разные строки. Но принцип тот же: другие сеансы не должны видеть незафиксированные изменения при обычных настройках, а конфликтующие записи должны подождать. В нашем тесте мы установили для второго соединения немедленное разрывание, а не ожидание, поэтому ошибка появилась сразу.

Чтение аномалий

Когда транзакции перекрываются, могут возникнуть определенные проблемы. Стандарт SQL определяет их с точки зрения того, что может наблюдать транзакция:

Аномалия Что происходит
Грязное чтение Вы читаете изменение, которое не зафиксировала другая транзакция, и позже оно может быть отменено.
Неповторяемое чтение Вы читаете строку дважды в одной транзакции и получаете разные значения, потому что кто-то совершил коммит между ними.
Фантомное чтение Вы запускаете один и тот же запрос дважды и получаете лишние или недостающие строки, потому что кто-то вставил или удалил некоторые из них.

Другая проблема — потеря обновления. Две сессии считывают баланс 100, каждая добавляет 10 и каждая записывает 110. Одно обновление фактически исчезло, когда ответ должен был быть 120.

Уровни изоляции SQL

Уровни изоляции позволяют вам обменивать строгость на параллелизм. Вот что стандарт SQL позволяет на каждом уровне:

Уровень Грязное чтение Неповторяемое чтение Фантомное чтение
Читать незафиксированные Возможно Возможно Возможно
Читать зафиксировано Предотвращено Возможно Возможно
Повторяемое чтение Предотвращено Предотвращено Возможно
Сериализуемый Предотвращено Предотвращено Предотвращено
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Точный синтаксис и поведение зависят от вашей базы данных. Значения по умолчанию также различаются: PostgreSQL и SQL Server используют фиксированное чтение, тогда как InnoDB MySQL использует повторяемое чтение. Некоторые базы данных дают более строгие гарантии, чем того требует стандарт, или рассматривают один уровень как другой. Например, в PostgreSQL чтение без фиксации ведет себя как чтение с фиксированием. Всегда читайте документацию по вашей базе данных, прежде чем полагаться на определенное поведение.

Совет: В случае потери обновления внесите изменение в одном операторе, как в UPDATE accounts SET balance = balance + 10. Выполнение вычислений в вашем приложении на основе ранее прочитанного — это то, что открывает пробел.

Тупики

Взаимная блокировка возникает, когда каждая из двух транзакций удерживает блокировку, необходимую другой. Транзакция A блокирует строку 1 и хочет получить строку 2, тогда как транзакция B блокирует строку 2 и хочет получить строку 1. Ни одна из них не может продолжаться.

Такие базы данных, как PostgreSQL, MySQL и SQL Server, обнаруживают это и отменяют одну транзакцию с ошибкой. Это нормальное поведение, и ваше приложение должно быть к этому готово. Чтобы уменьшить количество взаимоблокировок, всегда обновляйте строки в последовательном порядке, например сначала наименьший идентификатор учетной записи, сохраняйте короткие транзакции и повторяйте попытку, когда одна из них выбрана в качестве жертвы.

Лучшие практики

  • Сделайте транзакции короткими. Длинные транзакции удерживают блокировки и блокируют других пользователей.
  • Никогда не ждите пользователя или медленного API внутри транзакции. Делайте это до начала или после него.
  • Всегда обрабатывайте ошибки. Каждый путь должен заканчиваться фиксацией или откатом.
  • Добавьте ограничения. Они задают базе данных реальные правила, которые необходимо соблюдать. Хороший дизайн таблицы тоже помогает.
  • Используйте хорошие индексы. Более быстрый поиск означает, что блокировки удерживаются меньше времени.
  • Будьте осторожны с повторными попытками. Повторяйте попытки в случае взаимоблокировок, но убедитесь, что операция не может быть применена дважды.
  • Протестируйте параллелизм нарочно. Многие из этих ошибок появляются только под нагрузкой, поэтому одни только базовые интеграционные тесты могут их не выявить.

Итоги

Транзакции превращают несколько рискованных шагов в одну безопасную операцию, а ACID описывает гарантии, которые делают эту операцию заслуживающей доверия. Всякий раз, когда деловое действие затрагивает более одной строки или таблицы, спросите себя, что произойдет, если оно остановится на полпути. Если ответ вас беспокоит, оберните его в транзакцию. Для практики откройте два сеанса в локальной базе данных и попытайтесь создать аномалии самостоятельно.

Часто задаваемые вопросы

Что такое транзакция в SQL?

Транзакция — это группа операторов SQL, рассматриваемая как одна единица. Либо все они сохраняются с помощью COMMIT, либо все они отменяются с помощью ROLLBACK.

Что такое свойства ACID в базе данных?

ACID означает атомарность, последовательность, изоляцию и долговечность. Вместе они гарантируют, что транзакции выполняются по принципу «все или ничего», сохраняют правила нетронутыми, не вмешиваются и выдерживают сбои.

В чем разница между COMMIT и ROLLBACK?

COMMIT навсегда сохраняет все изменения, внесенные в транзакцию. ROLLBACK отменяет их и возвращает данные в состояние, в котором они были до начала транзакции.

Что такое грязное чтение?

Грязное чтение — это когда одна транзакция считывает изменения, которые другая еще не зафиксировала. Если эта другая транзакция откатится, прочитанные данные никогда не существовали на самом деле.

Какой уровень изоляции мне следует использовать?

Начните со значения по умолчанию для вашей базы данных, что обычно вполне подходит. Используйте сериализуемый для критической логики, где аномалии недопустимы, и будьте готовы повторить неудачные транзакции.

Продолжайте учиться

Работайте с Stack Quora

Нужна помощь с тестированием, WordPress или SEO?

Мы помогаем командам выпускать лучшее программное обеспечение и развивать свои сайты. Расскажите нам, над чем вы работаете, и мы предложим четкий следующий шаг.

Продолжайте читать

Больше руководств по тестирование, искусственный интеллект и современные разработки

Понравилась эта статья? Выберите тему и сразу приступайте к нашим новейшим руководствам.

Искусственный интеллект

Объяснение галлюцинаций искусственного интеллекта: почему языковые модели все выдумывают и как их уменьшить

Галлюцинации ИИ объяснили: что это такое, почему языковые модели их создают, распространенные типы, как уменьшить их с помощью заземления и подсказо…

Читать статью →
DevOps

Объяснение переменных среды: файлы .env, секреты и правильно выполненная конфигурация

Переменные среды объясняются на примерах Python, которые мы запускали: их чтение и установка, почему каждое значение является строкой, файлы .env, н…

Читать статью →
Разработка ПО

Объяснение ограничения скорости API: алгоритмы, заголовки и обработка ошибок 429

Ограничение скорости API объясняется с помощью проведенного нами моделирования: сегмент токенов, фиксированное окно, скользящее окно, 429 ответов, з…

Читать статью →
Автоматизация тестирования

Издевательство в Python: unittest.mock, патч и ошибки, из-за которых тесты лгут

Издевательство на Python с помощью unittest.mock, протестировано: Mock и return_value, Side_effect, исправление использования имени, спецификация и …

Читать статью →
Тестирование ПО

Объяснение покрытия кода: покрытие линий или ветвей и что на самом деле означает 100%

Покрытие кода объяснено с помощью реального запуска покрытия.py: покрытие строк, ветвей и условий, чтение отчета, почему 100%-ное покрытие все еще м…

Читать статью →
Базы данных и SQL

DELETE, TRUNCATE и DROP в SQL: различия, откат и когда использовать каждый из них

Объяснение DELETE, TRUNCATE и DROP на примере протестированного SQL: что каждый из них удаляет, откат, триггеры, сброс идентификаторов, скорость и ч…

Читать статью →
Базы данных и SQL

Объяснение представлений SQL: создавайте, используйте и избегайте ловушек, с примерами

Представления SQL, объясненные с помощью запросов, которые мы выполняли: создание представления, результаты в реальном времени, скрытие столбцов, пр…

Читать статью →
Разработка ПО

Интерфейс TypeScript и тип: реальные различия, протестированные с помощью компилятора

Интерфейс TypeScript против типа, проверенный реальным компилятором: что могут делать оба, что может делать только один, слияние объявлений, расшире…

Читать статью →
Разработка ПО

Объяснение Python *args и **kwargs: гибкие функции с понятными примерами

Python *args и **kwargs, объясненные с помощью кода, который мы запускали: упаковка и распаковка, порядок параметров, аргументы только по ключевым с…

Читать статью →
Разработка ПО

Лямбда-функции Python: синтаксис, реальное использование и когда их не следует использовать

Лямбда-функции Python, объясненные с помощью кода, который мы запускали: синтаксис, сортировка с помощью ключа, карты и фильтра, замыкания, ловушка …

Читать статью →
Искусственный интеллект

Что такое ИИ-агенты? Как они работают: можно запустить простой цикл агента

Что такое агенты ИИ? Простое руководство по циклу агента, инструментам, памяти и ограничениям, с небольшим циклом Python, который мы запустили, реал…

Читать статью →
DevOps

Мониторинг и наблюдаемость: реальная разница с журналами, метриками и трассировками

Объяснение мониторинга и наблюдаемости: известные и неизвестные сбои, журналы, метрики и трассировки, пошаговое руководство по инцидентам при оформл…

Читать статью →
DevOps

Docker Volumes против Bind Mounts: правильное сохранение данных контейнера

Объяснение томов Docker и монтирования привязки: почему исчезают данные контейнера, именованные тома, монтирование привязки, tmpfs, примеры создания…

Читать статью →
Автоматизация тестирования

TDD против BDD: ключевые различия, одна функция, созданная для обоих способов, и когда использовать каждый

TDD и BDD объясняются с помощью одной функции скидки, созданной в обоих направлениях: красно-зеленый рефакторинг с кодом, который мы запускали, сцен…

Читать статью →
Тестирование ПО

Пользовательское приемочное тестирование (UAT): процесс, контрольный список и реальные примеры

Приемочное тестирование пользователей объяснило: кто его проводит, чем оно отличается от контроля качества, семиэтапный процесс, шаблон тестового сц…

Читать статью →
Базы данных и SQL

Объяснение значений NULL в SQL: IS NULL, COALESCE, NULLIF и ловушки, которых следует избегать

Значения SQL NULL объяснены с помощью запросов, которые мы выполнили: трехзначная логика, IS NULL, COALESCE, NULLIF, как агрегаты, соединения и сорт…

Читать статью →
Базы данных и SQL

Оператор SQL CASE WHEN: синтаксис, реальные примеры и распространенные ошибки

SQL CASE WHEN объяснен с помощью запросов, которые мы выполняли: простые формы и формы для поиска, порядок условий, повороты с помощью SUM, трюки CO…

Читать статью →
Разработка ПО

Что такое JWT? Веб-токены JSON, объясненные на рабочем примере

Что такое JWT? Объяснение JSON Web Tokens: три части: токен, созданный и измененный в коде, который мы запускали, JWT и сеансы, хранилище, токены об…

Читать статью →
Разработка ПО

Объяснение цикла событий JavaScript: стек вызовов, микрозадачи и таймеры с примерами

Цикл событий JavaScript объясняется с помощью кода, который мы запускали: стек вызовов, очереди задач и микрозадач, почему setTimeout 0 не является …

Читать статью →
Разработка ПО

Генераторы Python и объяснение доходности: экономьте память с помощью реальных примеров

Генераторы Python и доходность, объясненные с помощью кода, который мы запускали: как они приостанавливаются и возобновляются, измеряют экономию пам…

Читать статью →
Искусственный интеллект

Что такое вложения? Простое объяснение векторов и косинусного сходства

Что такое вложения? Простое руководство по векторам, косинусному подобию и семантическому поиску с небольшим примером, который мы использовали, общи…

Читать статью →
DevOps

Сине-зеленое и канареечное развертывание: различия, компромиссы и как выбрать

Объяснение сине-зеленого и канареечного развертывания: как каждый из них работает, стоимость, риски и откат, стабильное группирование пользователей,…

Читать статью →
Автоматизация тестирования

Ненадежные тесты: что их вызывает и как их исправить навсегда

Ненадежные тесты объясняются на демонстрациях, которые мы запускали: основные причины, способы подтверждения и диагностики, правильные исправления в…

Читать статью →
Тестирование ПО

Нагрузочное тестирование и стресс-тестирование: различия, метрики и примеры k6

Объяснение нагрузочного и стресс-тестирования: что доказывает каждое из них, тесты на всплески и выдержки, важные метрики, почему средние значения в…

Читать статью →
Тестирование ПО

Тестовый пример и тестовый сценарий: ключевые различия, примеры и когда использовать каждый

Объяснение тестового случая и тестового сценария: четкие определения, параллельная таблица, одна функция сброса пароля, взятая из сценариев в случаи…

Читать статью →
Базы данных и SQL

Оптимизация SQL-запросов: чтение планов выполнения и исправление медленных запросов с указанием времени

Оптимизация SQL-запросов проверена на миллионе строк: читайте планы выполнения, добавляйте правильные индексы, исправляйте неразмещаемые фильтры, ис…

Читать статью →
Базы данных и SQL

SQL UNION против UNION ALL: различия, примеры и проверка производительности

SQL UNION и UNION ALL объяснены с помощью запросов, которые мы выполнили: дубликаты, правила, которым следуют оба, реальная ошибка данных, измеренна…

Читать статью →
Разработка ПО

Учебное пособие по API JavaScript Fetch: GET, POST, обработка ошибок и тайм-ауты

Учебное пособие по API JavaScript Fetch, протестированное на локальном сервере: GET и POST, почему не выдается ошибка 404, многоразовый помощник, ст…

Читать статью →
Разработка ПО

Обработка файлов Python: чтение, запись, CSV, JSON и pathlib с примерами

Обработка файлов Python объясняется с помощью кода, который мы запускали: открытые режимы, чтение больших файлов, CSV, JSON, pathlib, двоичные файлы…

Читать статью →
Разработка ПО

Классы и объекты Python: учебник ООП с реальными примерами

Классы и объекты Python, объясненные с помощью кода, который мы запускали: __init__, методы, атрибуты класса и экземпляра, наследование, свойства, м…

Читать статью →
Искусственный интеллект

Что такое большая языковая модель? Как работают LLM, объяснено без шумихи

Что такое большая языковая модель? LLM объясняется простым языком: токены, встраивания, внимание, обучение, температурные и контекстные окна, крошеч…

Читать статью →
DevOps

Команды Linux для начинающих: основное руководство по терминалу с примерами

Команды Linux для начинающих, сгруппированные по задачам: навигация, файлы, просмотр журналов, grep, каналы, разрешения, процессы и архивы, с практи…

Читать статью →
Автоматизация тестирования

Selenium vs Playwright: ключевые различия, сравнение кода и что выбрать

Сравнение Selenium и Playwright: архитектура, ожидание, поддержка браузера и языка, отладка, одинаковый тест в обоих инструментах, советы по миграци…

Читать статью →
Тестирование ПО

Разделение эквивалентности: метод проектирования тестов, объясненный на рабочих примерах

Объяснение эквивалентного разделения: как найти действительные и недействительные разделы, рабочие примеры для возрастов, паролей и купонов, проведе…

Читать статью →
Тестирование ПО

Серьезность и приоритет в тестировании программного обеспечения: различия, матрица и реальные примеры

Объяснение серьезности и приоритета при тестировании программного обеспечения: четкие определения, четырехсторонняя матрица с примерами, шкалы уровн…

Читать статью →
Базы данных и SQL

Учебное пособие по подзапросам SQL: типы, коррелирующие подзапросы и EXISTS с примерами

Руководство по подзапросам SQL с выполненными нами запросами: скалярные подзапросы, IN, FROM и SELECT, коррелированные подзапросы, EXISTS, ловушка N…

Читать статью →
Базы данных и SQL

Объяснение транзакций SQL и ACID: COMMIT, ROLLBACK и уровни изоляции

Транзакции SQL и ACID, объясненные с помощью банковского перевода, который мы выполнили: COMMIT, ROLLBACK, точки сохранения, блокировки, уровни изол…

Читать статью →
Разработка ПО

Git merge vs rebase: отличия, примеры и когда что использовать

Git merge vs rebase с реальным выводом log, сравнительной таблицей, золотым правилом rebase и безопасным рабочим процессом для команды.

Читать статью →
Разработка ПО

Генераторы списков в Python: синтаксис, примеры и когда их избегать

Генераторы списков в Python: синтаксис, фильтры, if-else, вложенные циклы, измеренное время и случаи, когда обычный цикл for лучше.

Читать статью →
Базы данных и SQL

SQL JOIN: объяснение с примерами (Inner, Left, Right, Full)

Разберите SQL JOIN шаг за шагом на примерах таблиц и запросов: INNER, LEFT, RIGHT, FULL, CROSS и SELF, а также частые ошибки и советы по производите…

Читать статью →