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

Нагрузка ломает не классы, а границы ответственности
В системе бронирования один популярный ресурс внезапно становится недоступен: запросы на чтение проходят, а подтверждение заказа начинает ждать блокировки. При этом CPU может быть занят всего на 40%, а база данных уже накапливает очередь транзакций. Первым ломается не отдельный класс доменной модели, а неверно выбранная граница изменения данных.
Эта статья — разбор для систем, где несколько запросов одновременно меняют один бизнес-объект: заказ, остаток товара, лимит, баланс, расписание или статус заявки. В качестве опорной инфраструктуры ниже используется PostgreSQL 16; конкретные таймауты, ORM и способ доставки сообщений зависят от приложения.
Под нагрузкой доменная модель должна отвечать не только на вопрос «что разрешено», но и на вопрос «кто имеет право изменить это одновременно с кем».
Сначала зафиксируйте инварианты
Проектирование обычно начинают с сущностей: User, Order, Product, Payment. Под нагрузкой полезнее начать с утверждений, которые нельзя нарушить даже при двух параллельных запросах.
- остаток товара не становится отрицательным;
- один платёж не подтверждает два разных заказа;
- заказ не переходит из «отменён» обратно в «оплачен»;
- лимит клиента не превышен после завершения операции;
- событие о подтверждённом заказе не публикуется раньше фиксации транзакции.
Каждый инвариант нужно связать с механизмом защиты. Часть проверок должна находиться в доменном коде, часть — в базе данных, а часть — в протоколе обмена сообщениями. Проверка только в приложении не защищает от второго процесса, ручного SQL-запроса или гонки между репликами.
Инвариант — условие, которое должно оставаться истинным после каждой завершённой операции. Например, «баланс не ниже нуля» — инвариант, а «кнопка оплаты отображается синей» — нет.
Опишите операцию как короткую транзакцию
Для каждой команды выпишите последовательность действий: какие данные читаются, какие изменяются, какие внешние вызовы выполняются. Это быстро показывает опасные места.
- Выделите команду. Не «обновить заказ», а «зарезервировать товар», «подтвердить оплату» или «отменить бронь». У команды должен быть один понятный бизнес-результат.
- Назовите изменяемые данные. Запишите таблицы и строки, которые затрагивает операция. Если команда меняет заказ, остаток, бонусный счёт и журнал аудита, это четыре возможных источника блокировок.
- Определите порядок захвата ресурсов. Все конкурирующие операции должны брать блокировки в одинаковом порядке. Иначе две транзакции могут ждать друг друга.
- Уберите внешние вызовы из транзакции. Платёжный шлюз, HTTP-запрос и публикация в брокер не должны удерживать блокировку строки. Сначала зафиксируйте состояние, затем запустите внешнюю работу через надёжный механизм доставки.
- Зафиксируйте повторяемость. Повтор команды после таймаута не должен дважды списывать деньги или создавать две резервации. Для этого применяют идемпотентный ключ операции и уникальное ограничение в базе.
Проверка результата проста: для каждой команды должна существовать короткая транзакция, список изменяемых ресурсов, правило повтора и понятный ответ на вопрос «что произойдёт при одновременном вызове».
Найдите горячую точку до выбора паттерна
Горячая точка — строка, агрегат или таблица, к которым часто обращаются параллельные операции. Типичный пример — одна строка остатка для популярного товара. Десятки запросов могут одновременно читать её и пытаться изменить. Добавление ещё одного сервера приложения не устранит конфликт: все процессы продолжат конкурировать за один ресурс в PostgreSQL.
Для каждой команды соберите четыре показателя:
- число запросов в секунду и долю операций записи;
- среднее и перцентильное время ожидания блокировки;
- длительность транзакции от начала до commit;
- число повторов из-за конфликтов, таймаутов и дедупликации.
Замеры нельзя подменять средним временем ответа. Среднее скрывает хвост: десять быстрых запросов и один запрос, который ждал 20 секунд, могут дать приемлемое среднее, но неприемлемый пользовательский опыт. Снимайте p95 и p99, отдельно для чтения, записи и ожидания блокировок.
Что измерять в PostgreSQL 16
Начните с планов запросов и статистики ожиданий. Для тяжёлых запросов смотрите фактический план через EXPLAIN с анализом, но не запускайте экспериментальный вариант на боевой базе без оценки побочных эффектов. Проверяйте, использует ли запрос индекс, сколько строк он реально читает и не превращается ли точечное изменение в последовательное сканирование.
Для нагрузочного теста зафиксируйте железо, версию PostgreSQL, размер набора данных, число параллельных клиентов, долю чтения и записи, длительность прогрева и сценарий ошибок. Без этих условий сравнение «до и после» не доказывает улучшение.
Выберите способ согласованности
У доменной команды обычно есть несколько вариантов защиты. Их нельзя выбирать по популярности: цена ошибки различается. Для остатков товара потеря обновления критична, для счётчика просмотров она может быть приемлемой.
| Подход | Что защищает | Цена | Когда применять |
|---|---|---|---|
| Оптимистическая проверка версии | Обнаруживает, что запись изменилась после чтения | Повтор операции или ошибка при конфликте | Конфликты редкие, а повтор безопасен |
| Пессимистическая блокировка | Не даёт конкурентной операции изменить строку раньше текущей | Ожидание, рост очереди и риск взаимных блокировок | Ресурс дефицитный, ошибка дороже ожидания |
| Атомарное условное обновление | Проверяет условие и меняет значение одной операцией | Сложнее выразить многосоставное правило | Нужна короткая операция над одной строкой |
| Последовательная обработка по ключу | Убирает конкуренцию внутри одного ключа | Очередь и задержка для горячего ключа | События можно обрабатывать асинхронно |
Критерий выбора — стоимость конфликта и допустимая задержка. Если операция меняет один счётчик, атомарного обновления часто достаточно. Если она затрагивает несколько связанных сущностей, сначала проверьте, не слишком широкая ли граница агрегата. Если заказ и склад нельзя согласовать в одной короткой транзакции, это уже вопрос процесса, а не только блокировки.
Где модель ломается первой
Слишком большой агрегат
Если изменение одного товарного предложения блокирует весь каталог, модель связывает независимые операции. Признак — растёт время ожидания строк, которые бизнес считает разными. Разделяйте агрегат там, где операции действительно могут быть независимыми, но не разрывайте инвариант, который обязан проверяться атомарно.
Слишком маленькие агрегаты
Обратная ошибка — разнести единое правило по нескольким таблицам и сервисам. Тогда система сначала сохраняет заказ, потом отдельно меняет лимит, а при сбое оставляет промежуточное состояние. Если данные должны измениться вместе, транзакционная граница должна это позволять.
Чтение с реплики сразу после записи
Реплика может отставать от основной базы. Пользователь подтвердил заказ, обновил страницу и увидел старый статус. Это не обязательно ошибка доменной модели, но модель должна явно учитывать момент согласованности: критическое чтение после записи выполняется из источника с нужной гарантией или получает состояние из результата команды.
Событие опубликовано раньше commit
Обработчик может получить событие о заказе, который потом откатился. Надёжный вариант — сохранить событие вместе с изменением доменных данных, а отправить его после фиксации. Доставка всё равно может быть повторной, поэтому получатели должны обрабатывать события идемпотентно.
Неограниченные повторы
Повтор после таймаута полезен, пока он ограничен и различает временную ошибку от бизнес-отказа. Иначе перегруженная база получает ещё больше одинаковых запросов. Для каждого повтора задайте лимит, задержку, случайное рассеивание и метрику окончательно отклонённых операций.
Граничные случаи и цена решения
Особенно опасны горячие ключи: один популярный товар, один клиент с большим числом операций, один календарный слот или общий баланс магазина. Партиционирование таблицы не делает такой ключ параллельным автоматически. Если все операции всё равно меняют одну строку, узкое место остаётся.
Асинхронная обработка тоже не бесплатна. Она снижает время пользовательского запроса, но добавляет очередь, задержку видимости результата, повторную доставку и необходимость объяснять пользователю промежуточный статус. Не отправляйте в очередь команду, результат которой должен быть известен до ответа, без явного контракта состояния.
Не пытайтесь лечить блокировки увеличением таймаутов. Большой таймаут маскирует очередь и дольше удерживает ресурсы. Сначала сокращайте транзакцию, проверяйте индексы, выравнивайте порядок доступа и измеряйте реальную причину ожидания.
Что делать дальше
Возьмите одну самую дорогую команду — например, резервирование товара или подтверждение платежа — и нарисуйте граф её чтений, записей, блокировок и внешних вызовов. Затем прогоните сценарий с несколькими конкурентными клиентами на копии данных PostgreSQL 16.
После теста смотрите не только на RPS. Зафиксируйте p95 и p99 задержки, ожидание блокировок, долю повторов, число нарушенных бизнес-инвариантов и время обработки очереди. Следующий шаг — проверить тот же сценарий при росте размера данных и появлении горячего ключа. Именно там станет видно, выдерживает ли доменная модель нагрузку или просто хорошо выглядит на диаграмме.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: PostgreSQL 16 Documentation: Using EXPLAIN, PostgreSQL 16 Documentation: Explicit Locking
Частые вопросы
Нужно ли сразу переходить на микросервисы, если доменная модель не выдерживает нагрузку?
Что выбрать для изменения остатка: блокировку или версию записи?
Почему база загружена не полностью, а запросы уже тормозят?
Что будет, если повторить команду после таймаута?
Можно ли читать сразу после записи с реплики?
Как понять, что агрегат получился слишком большим?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.