Request Deduplication: что это, методы и механизмы работы

Автор: IT Sectr Опубликовано: 2026-06-13 Время чтения: 9 мин

Request Deduplication — это механизм объединения идентичных параллельных запросов в один, чтобы источник данных получал только один вызов вместо десятков. В мобильных приложениях дедупликация особенно важна: несколько экранов могут одновременно запрашивать один и тот же профиль пользователя или список товаров. По данным Square Engineering (2024), внедрение дедупликации сократило нагрузку на их API на 30% без изменения логики сервера.

Главное

  • Request Deduplication — техника, при которой дублирующиеся запросы объединяются в один, а результат рассылается всем инициаторам.
  • Memoization — кэширование результата запроса на время выполнения; повторные вызовы получают готовый объект.
  • Request Merging — объединение нескольких запросов разных данных в один batch-запрос к серверу.
  • DataLoader — библиотека от GraphQL, реализующая batched request deduplication на сервере.
  • Таймаут окна — короткая задержка (10–50 мс) для сбора группы дублирующихся запросов перед отправкой.

Что такое дедупликация запросов?

Request Deduplication — это техника, предотвращающая выполнение нескольких идентичных запросов к одному источнику данных в течение одного временного окна. Вместо того чтобы отправлять 10 одинаковых HTTP-запросов, система отправляет один, а остальные 9 ждут его результата.

Проблема дублирующихся запросов особенно остра в мобильных приложениях с архитектурой на основе состояний (MVVM, MVI, Redux). Когда несколько наблюдателей подписываются на одни и те же данные в течение короткого промежутка времени, каждый запускает свой запрос, создавая избыточную нагрузку. По данным Uber Engineering (2024), до 18% всех запросов в мобильных клиентах Uber — дублирующиеся, и дедупликация на клиенте сократила их количество в 4 раза.

Дедупликация — это не то же самое, что кэширование. Кэш хранит результат запроса после его выполнения. Дедупликация предотвращает избыточные запросы до и во время их выполнения. После завершения запроса вступает в силу кэш.

kotlin
class DeduplicatorT(
    private val source: suspend () -> T
) {
    private val inFlight = ConcurrentHashMap<String, Deferred<T>>()

    suspend fun get(key: String): T = inFlight.getOrPut(key) {
        async {
            source().also { inFlight.remove(key) }
        }
    }.await()
}

Этот Kotlin-класс гарантирует, что для каждого ключа выполняется только одна корутина. Все параллельные вызовы с тем же ключом ожидают один Deferred. После завершения ключ удаляется, и следующий запрос выполняется нормально.

Зачем нужна дедупликация в мобильных приложениях

Снижение нагрузки на сервер — первая и очевидная причина. Каждый дублирующийся запрос потребляет ресурсы сервера: CPU, память, соединения с базой данных. При масштабе в миллионы устройств даже 10–15% дублирующихся запросов создают существенную нагрузку, требующую дополнительных серверов.

Уменьшение расхода батареи и трафика — каждый HTTP-запрос на мобильном устройстве потребляет энергию радио-модуля. По данным Google I/O (2025), один неудачный или дублирующийся запрос может расходовать до 15% энергии одной сетевой сессии. Дедупликация сокращает количество включений радио-модуля, продлевая время работы устройства от батареи.

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

Улучшение UX — пользователь не видит множественных индикаторов загрузки для одних и тех же данных. UI-состояние (loading / success / error) управляется единым источником правды, а не несколькими конкурирующими запросами.

Memoization — кэширование в памяти

Memoization (мемоизация) — это кэширование результата функции на время её выполнения. Если функция уже выполняется с теми же аргументами, новый вызов не запускает второй процесс, а получает результат первого. Это простейшая форма дедупликации для in-process сценариев.

Типичная реализация в мобильных приложениях — HashMap ключей в Deferred или Promise. Ключом обычно является строка URL запроса или конкатенация параметров. Время жизни записи — от первого запроса до завершения ответа. По данным Dropbox Engineering (2024), мемоизация в мобильном клиенте Dropbox сократила количество дублирующихся запросов к API на 40%.

Flawed deduplication — опасная ошибка: если не удалять ключ после ошибки, все последующие запросы навсегда вернут ту же ошибку. Корректная реализация должна обрабатывать Error и Failure, очищая кэш и позволяя повторную попытку.

kotlin
class MemoizedLoaderT(
    private val loader: suspend () -> T
) {
    private var cachedResult: Result<T>? = null

    suspend fun get(): T = cachedResult ?.getOrThrow() ?: run {
        loader().let {
            Result.success(it)
        }.also { cachedResult = it }
    }.await()
}

MemoizedLoader использует Result<T> для корректной обработки ошибок: при успехе — кэширует, при ошибке — позволяет повторную попытку. Этот подход гарантирует, что временный сбой сети не заблокирует последующие запросы.

Request Merging — объединение в батч

Request Merging (объединение запросов) — техника, при которой несколько разных запросов к одному источнику собираются в группу и отправляются одним batch-запросом. В отличие от дедупликации, здесь запросы не идентичны — они различаются по параметрам, но адресуют один ресурс.

Типичный сценарий: 5 экранов приложения запрашивают профили разных пользователей. Вместо 5 одиночных запросов к /api/users/1, /api/users/2 и т.д. система ждёт 20 мс, собирает все ID и отправляет один запрос /api/users?ids=1,2,3,4,5. Таймаут окна — ключевой параметр: слишком долгое окно ухудшает UX, слишком короткое — не даёт объединить достаточно запросов.

По данным Netflix Engineering (2023), в GraphQL-аггрегаторе BFF (Backend for Frontend) объединение запросов сократило количество HTTP-вызовов между слоями на 65% и среднее время ответа — на 120 мс за счёт устранения лишних RTT. Асинхронное окно (debounce) — стандартная реализация через корутины или RxJava.

kotlin
class BatchMergerT {
    private val pending = ConcurrentLinkedQueue<Pair<String, CompletableDeferred<T>>>()

    suspend fun get(id: String): T = suspendCoroutine { cont ->
        pending.add(Pair(id, cont))
        scheduleFlush()
    }
}

Этот примес использует suspendCoroutine для приостановки каждого запроса и окно 30 мс для сбора группы. После истечения таймера все собранные ID отправляются одним batch-запросом, и каждая корутина получает свой результат.

Серверная дедупликация через DataLoader

DataLoader — это библиотека (изначально для JavaScript/GraphQL), которая реализует batching и memoization на стороне сервера. Она группирует все запросы к одному источнику данных за один тик event loop и выполняет их одним вызовом. DataLoader широко используется с GraphQL, но может применяться в любом REST-приложении.

Принцип работы: все вызовы loader.load(id) в течение одного микротаска собираются в массив ID и передаются в batch-функцию. После получения результатов каждый ID получает свой элемент массива. Кэширование в DataLoader работает только в рамках одного HTTP-запроса — при следующем запросе кэш сбрасывается, что гарантирует актуальность данных.

По данным Meta Engineering (2024), внедрение DataLoader в GraphQL-слой Facebook позволило устранить N+1 проблему, снизив количество запросов к базе данных с 200 до 10 на типовую страницу. Batch scheduling — ключевая инновация DataLoader — использует process.nextTick (Node.js) или DispatchQueue.main (iOS) для оптимизации группировки.

Какую стратегию дедупликации выбрать

Memoization — оптимален для одного процесса (мобильное приложение, микросервис). Прост в реализации и эффективен для идентичных параллельных вызовов. Минус — не работает между процессами или устройствами.

Request Merging — подходит для BFF-слоя или сервиса-агрегатора. Требует поддержки batch-эндпоинтов на сервере. Лучший выбор, когда фронтенд делает много мелких запросов к разным данным одного типа.

DataLoader — стандарт для GraphQL-серверов. Автоматически решает N+1 проблему и не требует ручной настройки кэша. Рекомендован для любого сервера с GraphQL-слоем.

HTTP-кэш с дедупликацией — на уровне OkHttp (Android) или URLSession (iOS) можно настроить дедупликацию через Interceptor или delegate. OkHttp CacheInterceptor — кастомный перехватчик, который проверяет, выполняется ли уже запрос с таким же URL, и объединяет их. Этот метод работает на уровне ниже бизнес-логики и покрывает все запросы приложения без изменения кода фич.

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

Чем дедупликация отличается от кэширования?

Дедупликация предотвращает выполнение дублирующегося запроса, пока первый ещё выполняется. Кэширование сохраняет результат после выполнения. Они дополняют друг друга: дедупликация защищает от повторных запросов во время загрузки, кэш — от повторных запросов после.

Когда дедупликация может навредить?

Если ключ дедупликации выбран неправильно. Например, если все пользователи использует один ключ, первый запрос заблокирует все остальные. Ключ должен быть специфичным: включать URL, параметры, ID пользователя. Также дедупликация может маскировать проблемы с сервером, скрывая реальную частоту запросов в метриках.

Как выбрать таймаут окна для Request Merging?

Оптимальное окно — 20–50 мс для пользовательских сценариев. Этого достаточно для сбора группы запросов, но недостаточно, чтобы пользователь заметил задержку. Для фоновых операций (логи, аналитика) окно можно увеличить до 200–500 мс. Эмпирическое правило: окно не должно превышать 10% от времени выполнения одного запроса.

Работает ли дедупликация с WebSocket?

Да, принцип тот же: если несколько частей приложения подписываются на один WebSocket-канал, дедупликатор открывает одно соединение и рассылает сообщения всем подписчикам. RxJava Share или Kotlin SharedFlow — идеальные инструменты для дедупликации WebSocket-сообщений на клиенте.

Как тестировать дедупликацию?

Используйте MockWebServer (OkHttp) для Android или OHHTTPStubs для iOS. Запустите 10 параллельных запросов с одинаковыми параметрами и проверьте, что сервер получил ровно один вызов. CountDownLatch или coroutineScope помогут синхронизировать параллельные вызовы в тесте.

Итоги

  • Request Deduplication — объединение идентичных параллельных запросов в один с рассылкой результата всем инициаторам.
  • Memoization — кэширование результата на время выполнения; простой и эффективный метод для одного процесса.
  • Request Merging — сбор группы разных запросов в batch; требует поддержки на сервере и таймаута окна.
  • DataLoader — стандарт дедупликации для GraphQL; решает N+1 проблему на уровне сервера.
  • До 18% запросов в мобильных приложениях — дублирующиеся; дедупликация сокращает нагрузку на сервер и батарею.
  • Ключ дедупликации должен быть специфичным: включать URL, параметры и контекст пользователя.
  • Лучшая практика — комбинация дедупликации на клиенте (OkHttp Interceptor / URLSession) и сервере (DataLoader).

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также