Содержание
Транзакция — это группа изменений в базе данных, которая выполняется целиком или не выполняется вовсе. Уровень изоляции определяет, что параллельные транзакции видят друг у друга. Если транзакции и изоляцию настроить небрежно, то при одновременных запросах появляются двойные списания, проданные дважды товары и отрицательные остатки — ошибки, которые не воспроизводятся на тестах и проявляются под нагрузкой.
Разберём, что такое ACID, какие бывают аномалии, чем отличаются уровни изоляции и как защищаться от гонок.
Что такое транзакция и ACID?
Транзакция объединяет несколько операций в одну: например, списать деньги со счёта, создать заказ и уменьшить остаток. Свойства ACID описывают гарантии, которые даёт база данных.
| Свойство | Что означает | Пример |
|---|---|---|
| Atomicity — атомарность | Всё или ничего | Нет заказа без списания и списания без заказа |
| Consistency — согласованность | Данные соответствуют правилам | Остаток не может стать отрицательным, если есть ограничение |
| Isolation — изоляция | Параллельные транзакции не мешают друг другу | Два покупателя не купят последний товар одновременно |
| Durability — долговечность | Зафиксированное не пропадёт | После подтверждения заказ переживёт перезапуск сервера |
Какие аномалии возникают при параллельной работе?
Аномалии возникают, когда несколько транзакций одновременно читают и меняют одни и те же данные. Самая частая в веб-приложениях — потерянное обновление: два запроса прочитали остаток 1, оба решили, что товар есть, и оба уменьшили его.
- потерянное обновление — одно изменение затирает другое;
- неповторяемое чтение — в одной транзакции одна и та же строка читается с разными значениями;
- фантомы — повторный запрос возвращает новые строки, добавленные другой транзакцией;
- перекос записи — две транзакции проверяют условие по разным строкам и вместе нарушают правило: например, оба дежурных снимают себя с дежурства.
Фото: Unsplash
Чем отличаются уровни изоляции?
Уровни изоляции — это компромисс между защитой от аномалий и производительностью. По умолчанию PostgreSQL использует Read Committed, MySQL с InnoDB — Repeatable Read. Ни один из них сам по себе не защищает от всех гонок в бизнес-логике.
| Уровень | Что видит транзакция | От чего защищает |
|---|---|---|
| Read Committed | Только зафиксированные изменения, на момент каждого запроса | От чтения «грязных» данных |
| Repeatable Read | Снимок данных на начало транзакции | Плюс неповторяемое чтение |
| Serializable | Результат как при последовательном выполнении | От всех аномалий, ценой повторов |
На строгом уровне Serializable база может прервать транзакцию с ошибкой сериализации — приложение должно уметь её повторить.
Как защититься от гонок на практике?
На практике от гонок защищаются тремя способами: атомарными операциями в самом запросе, явной блокировкой строки или оптимистической блокировкой по номеру версии. Выбор зависит от того, как часто происходят конфликты.
// 1. Атомарное обновление с условием: остаток не уйдёт в минус
$updated = DB::table('products')
->where('id', $productId)
->where('stock', '>=', $qty)
->decrement('stock', $qty);
if ($updated === 0) {
throw new OutOfStock($productId);
}
// 2. Явная блокировка строки до конца транзакции
DB::transaction(function () use ($accountId, $amount) {
$account = Account::whereKey($accountId)->lockForUpdate()->first();
$account->balance -= $amount;
$account->save();
});
Правила:
- Проверку и изменение делайте одним запросом или под блокировкой, а не «прочитал — подумал — записал».
- Держите транзакции короткими: никаких внешних HTTP-запросов внутри.
- Ставьте ограничения в базе — уникальные индексы и проверки — как последнюю линию обороны.
- Для повторных запросов используйте ключи идемпотентности — подробнее в статье идемпотентность платежей и вебхуков.
Как проверить, что гонок нет?
Гонки не видны в обычных тестах, потому что запросы там идут последовательно. Проверяют их параллельными тестами и нагрузочными сценариями, которые одновременно отправляют много одинаковых запросов, — например, 50 попыток купить последний товар.
- пишите тесты, запускающие одну операцию из нескольких процессов одновременно;
- включайте такие сценарии в нагрузочное тестирование;
- следите в мониторинге за взаимными блокировками и ошибками сериализации;
- на ревью отдельно смотрите код, работающий с деньгами и остатками.
О безопасности финансовых операций подробнее — в статье разработка финтех-продукта, а о выборе СУБД — в материале SQL или NoSQL.
Коротко
- Транзакция выполняет группу изменений целиком или не выполняет вовсе.
- Без правильной изоляции появляются потерянные обновления, фантомы и перекосы записи.
- Уровни изоляции — компромисс между защитой и производительностью; Serializable требует повторов.
- От гонок защищают атомарные запросы, блокировка строк и оптимистическая блокировка.
- Гонки проверяют параллельными тестами, а не обычными.
Частые вопросы
Почему не использовать Serializable везде?
Строгий уровень снижает параллельность и приводит к ошибкам сериализации, которые приложение должно уметь повторять. Его включают точечно для операций, где аномалии недопустимы, а не для всего приложения.
Можно ли делать HTTP-запросы внутри транзакции?
Лучше не надо: транзакция будет держать блокировки всё время ожидания ответа, а при откате внешний вызов уже не отменить. Внешние действия выполняют после фиксации транзакции, например через очередь.
Что такое взаимная блокировка?
Ситуация, когда две транзакции ждут друг друга. База обнаруживает её и прерывает одну из транзакций. Чтобы снизить риск, блокируйте строки в одинаковом порядке и делайте транзакции короткими.



