Кейс: сетевой N+1

Ситуация

Процесс формировал сводный результат по списку объектов. Внутри обхода он повторно получал связанные данные: часть — через удалённые вызовы платформы, часть — из локального хранилища.

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

Число обращений росло вместе с размером входного списка. Это оказался тот же N+1, только не внутри ORM-загрузки одной сущности.

Диагностика

Я выписал обращения внутри цикла и разделил их на две группы:

  • данные, которые уже можно загрузить набором из локального repository/cache;
  • данные, для которых остаётся только одиночный удалённый вызов.

Отдельно нашёл повторяющиеся идентификаторы: один и тот же удалённый результат не требовалось получать заново для каждой строки.

Мой вклад

Я заранее собрал идентификаторы, массово загрузил доступные локально данные и построил карту для обогащения результата:

Map<Long, Details> detailsByOwner = detailsRepository
        .findByOwnerIdIn(ownerIds)
        .stream()
        .collect(toMap(Details::ownerId, identity()));

Для оставшегося удалённого API я сначала выделил уникальные идентификаторы, один раз получил результат для каждого из них и повторно использовал его при сборке строк. Batch-контракта у этого вызова не было.

Результат

Повторные обращения к данным платформы исчезли из цикла, а оставшиеся удалённые вызовы перестали дублироваться для одинаковых идентификаторов. Бизнес-результат не изменился.

Технический вывод

Если внутри цикла есть обращение к repository, cache или удалённому API, это повод искать N+1. Возможные решения: предварительная массовая загрузка, дедупликация ключей, кэш в рамках операции, batch endpoint или изменение контракта.

Массовый запрос тоже имеет ограничения: размер набора, timeout, частичные ошибки и память.

Связанные заметки