Гонка событий (Race Condition)
Гонка событий — ситуация, при которой результат работы системы зависит от порядка выполнения операций. Если несколько процессов одновременно работают с одним состоянием, итог может быть непредсказуемым.
Одна из самых распространённых проблем в современных системах. Может возникать в:
- микросервисах;
- распределённых системах;
- очередях сообщений;
- асинхронных API;
- frontend-приложениях;
- потоковой обработке данных.
Сложность гонки событий: проблема проявляется нестабильно. Система может работать корректно, а затем случайно выдать:
- двойное списание денег;
- отрицательный остаток;
- потерю обновления;
- дублирование операции;
- рассинхронизацию между сервисами.
Пример гонки событий
На складе остался один товар. Два пользователя одн овременно нажимают кнопку «Купить». Оба запроса читают остаток как 1. Оба успешно проходят проверку и уменьшают остаток.
В результате:
- продано два товара вместо одного;
- остаток становится отрицательным;
- система теряет консистентность.
Data Race vs Race Condition
- Data Race — низкоуровневая проблема многопоточного доступа к памяти.
- Race Condition — логическая проблема порядка выполнения операций.
В enterprise-системах аналитики и архитекторы в основном работают с Race Condition.
Когда возникает
несколько процессов работают с одним состоянием (например, одновременно читают и изменяют);
- хотя бы один процесс изменяет данные;
- порядок выполнения не контролируется;
- нарушение порядка доставки (события отправляются в одном порядке, а доставляются — в другом.);
- итог зависит от случайных факторов;
- отсутствие координации между сервисами (каждый сервис видит только локальное состояние.)
- deadlock (несколько транзакций блокируют друг друга)
Типичные признаки
- «плавающие» баги;
- редкие ошибки;
- невозможность стабильно воспроизвести проблему;
- случайные дубли;
- потеря части данных;
- периодическая рассинхронизация.
Race condition — не обязательно ошибка разработчика.
В монолитной синхронной системе вероятность гонки ниже. В распределённой архитектуре становится практически неизбежной.
Причины:
- сетевые задержки;
- ретраи;
- повторная доставка сообщений;
- независимая работа сервисов;
- параллельные консьюмеры;
- различная скорость обработки.
Ключевая проблема — порядок выполнения нельзя гарантировать.
Гонка всегда связана с одним принципом:
корректность системы завис ит от последовательности событий.
Если изменение порядка приводит к разному результату — система подвержена гонке.
В архитектуре
Backend и микросервисы
Самый частый источник гонок — параллельные запросы к одному ресурсу.
Например:
- несколько сервисов обновляют один заказ;
- несколько обработчиков меняют баланс;
- несколько консьюмеров читают одну очередь.
Особенно опасны:
- shared state;
- общие таблицы;
- асинхронные обработчики;
- фоновые задачи.
Базы данных
При использовании транзакций гонка не исчезает.
Типовые проблемы:
- Lost Update (ситуация, когда изменения одного процесса перезаписываются изменениями другого, и часть данных теряется);
- dirty read (чтение данных, которые были изменены другой транзакцией, но ещё не зафиксированы (могут быть отменены);
- non-repeatable read (повторное чтение одной и той же записи внутри транзакции возвращает разные значения, потому что другая транзакция изменила данные);
- перезапись изменений;
- конфликт версий.
Классическая ошибка:
SELECT balance FROM accounts;
UPDATE accounts SET balance = ?;
Между чтением и обновлением другой процесс может изменить данные.
Брокеры сообщений
Брокеры не гарантируют отсутствие гонок.
Проблемы:
- задвоенные события;
- доставка не по очереди;
- повторная доставка;
- параллельные консьюмеры.
Например:
- событие отмены заказа пришло раньше события создания;
- одно сообщение обработалось дважды;
- retry изменил порядок обработки.
Frontend
Race condition часто возникает даже в UI.
Пример:
- Пользователь быстро меняет поле профиля.
- Отправляется два запроса.
- Второй запрос завершается раньше первого.
- Первый ответ перезаписывает новое значение старым.
Пользователь видит устаревшие данные.
Особенно часто это происходит при:
- optimistic update (когда интерфейс показывает результат изменения сразу, не дожидаясь ответа сервера);
- debounce (механизм задержки выполнения действия до момента, пока пользователь не прекратит выполнять повторяющиеся действия (например, ввод текста);
- асинхронных запросах;
- локальном кеше.
Распределенные системы и стриминги
В распределённых системах гонка — нормальное состояние среды.
Причины:
- eventual consistency;
- независимые сервисы;
- сетевые лаги;
- разные каналы доставки;
- репликация.
В стриминг-системах события могут приходить позже/раньше; дублироваться; теряться; переупорядочиваться.
Потоковая обработка почти всегда строится с учётом гонки событий.
Примеры
Микросервисы с общей БД
Сервис заказов и сервис оплаты работают с одной таблицей.
Сервис оплаты меняет статус заказа:
status = 'paid'
Сервис заказов одновременно обновляет адрес доставки.
Оба сервиса:
- читают одну запись;
- меняют локальную копию;
- сохраняют объект полностью.
Последний UPDATE уничтожает изменения другого сервиса.
Как решать
- обновлять только нужные поля;
- использовать оптимистическая блокировка;
- отказаться от общей БД.
Event-driven архитектура
Сервис публикует:
OrderCreatedOrderCancelled
Потребитель получает их в обратном порядке.
- Клиент сначала получает письмо: «Заказ отменён»
- А затем: «Заказ успешно создан»
Как решать
- партицирование по
orderId; - сериализация обработки;
- версионирование событий;
- occurredAt timestamp.
Event Sourcing
Проекция получает:
PaymentAcceptedOrderCreated
Если платёж пришёл раньше создания заказа:
- агрегат не существует;
- обработчик падает;
- проекция становится неконсистентной.
Как решать
- буферизация out-of-order событий;
- очередь ожидания / очередь отложенных событий;
- единый источник событий.
Frontend с optimistic update
UI сразу показывает новое значение без ожидания ответа сервера. Пользователь меняет поле дважды. Второй запрос выполняется быстрее первого. Первый ответ приходит позже и перезаписывает новое состояние.
Как ре шать
- версионирование;
- AbortController;
- повторное получение данных после изменения;
- оптимистическая конкуренция.
Как выявлять гонку событий
Главная проблема race condition — нестабильность.
Ошибка может проявляться:
- раз в неделю;
- только под нагрузкой;
- только при определённой задержке сети;
- только в продакшн.
Выявлять:
- Мониторинг и анализ логов (логировать sequence операций; версию ; timestamps;correlationId; retry count)
- Трассировка (позволяет увидеть: порядок вызовов, задержки, ретраи, конфликтующие операции)
- Нагрузочное тестирование
- Хаосное тестирование (в систему искусственно добавляют: лаги; packet loss; смена порядка отправки; дублирование доставки)
Как предотвращать гонку событий
Идемпотентность
Идемпотентная операция даёт одинаковый результат при повторной обработке.
Обычно реализуется через:
- idempotency key;
- уникальный eventId;
- таблица дедубликации.
Без идемпотентности невозможно безопасно работать с retry.
Оптимистическая блокировка
Запись хранит версию:
version = 5
При обновлении проверяется:
WHERE version = 5
Если запись уже изменилась:
- UPDATE не выполнится;
- система обнаружит конфликт.
Это основной механизм защиты от Lost Update.
Пессимистическая блокировка
Запись блокируется на время транзакции. Другие операции ждут освобождения.
Плюс - строгая консистентность.
Минусы:
- блокировки;
- деградация производительности;
- deadlock.
Transactional Outbox
Проблема:
- запись в БД сохранилась;
- событие в брокер не отправилось.
Outbox решает это через атомарную запись:
- бизнес-данные;
- событие в outbox table.
Отдельный publisher читает outbox и публикует сообщения.
Сериализация обработки
Если порядок критичен — события должны обрабатываться последовательно.
Обычно используется:
- партицирование по ключу;
- единый консьюмер;
Например, все события заказа идут в одну партицию Kafka.
Saga Pattern
Saga координирует распределённую транзакцию между сервисами.
Каждый шаг:
- выполняет локальную операцию;
- публикует событие;
- при ошибке запускает компенсацию.
Saga не устраняет гонку полностью, но помогает контролировать состояние распределённого процесса.
Роль системного аналитика
Ошибки проектирования часто появляются ещё на этапе требований.
Например, аналитику следует:
- выявлять конкурентные сценарии;
- определять критичность порядка событий;
- фиксировать требования к консистентности;
- понимать гарантии доставки сообщений;
- учитывать retry и duplicate delivery.
Ключевые вопросы при проектировании:
- может ли операция выполниться дважды;
- допустима ли eventual consistency;
- что произойдёт при out-of-order событиях;
- какие данные являются источником истины;
- нужна ли о птимистичная блокировка
- требуется ли идемпотентность.
Частые ошибки
- Надежда на frontend-валидацию (Frontend не защищает от параллельных запросов. Все проверки должны выполняться на backend.)
- Отсутствие идемпотентности
- Общая БД между сервисами (создаёт сильную связанность и повышает вероятность Lost Update.)
- Полное обновление записи (безопаснее обновлять только нужные поля.)
- Игнорирование гарантий брокера (ошибочно считать, что сообщения не дублируются; порядок всегда сохраняется; retry безопасен.)
- Отсутствие версионирования
Материалы
- Небезопасная многопоточность или Race Condition
- Что такое состояние гонки (гонка данных, race condition)
- Почему стоит проверять приложения на устойчивость к race condition
- Быстрее улитки или Race Conditions в Websocket-ах
- Разница между Data Race и Race Condition
- Race Condition убил SQLite в нашем проекте: как мы пришли к RediSearch
- Race condition в веб-приложениях
- Concurrency testing — отлавливаем состояния гонки
- Борьба с гонками (race conditions) в JavaScript на примере работы с кешем
- Concurrency и паттерны ошибок, скрытые в коде: Deadlock
- Разбор основных концепций параллелизма
Видео
- Проблемы многопоточности: Deadlock, Race Condition. Кооперативная многозадачность
- Гонки данных или race condition (PostgreSQL, isolation level, Python)
- Дополнительные главы практической безопасности: Веб-безопасность — атаки на race condition
- Race condition и как с ним бороться // Демо-занятие курса «Symfony Framework»
- Нина Лукина — Пишем тесты на race conditions, deadlocks и остальной concurrency hell