Request Deduplication — это механизм объединения идентичных параллельных запросов в один, чтобы источник данных получал только один вызов вместо десятков. В мобильных приложениях дедупликация особенно важна: несколько экранов могут одновременно запрашивать один и тот же профиль пользователя или список товаров. По данным Square Engineering (2024), внедрение дедупликации сократило нагрузку на их API на 30% без изменения логики сервера.
Главное
Request Deduplication — это техника, предотвращающая выполнение нескольких идентичных запросов к одному источнику данных в течение одного временного окна. Вместо того чтобы отправлять 10 одинаковых HTTP-запросов, система отправляет один, а остальные 9 ждут его результата.
Проблема дублирующихся запросов особенно остра в мобильных приложениях с архитектурой на основе состояний (MVVM, MVI, Redux). Когда несколько наблюдателей подписываются на одни и те же данные в течение короткого промежутка времени, каждый запускает свой запрос, создавая избыточную нагрузку. По данным Uber Engineering (2024), до 18% всех запросов в мобильных клиентах Uber — дублирующиеся, и дедупликация на клиенте сократила их количество в 4 раза.
Дедупликация — это не то же самое, что кэширование. Кэш хранит результат запроса после его выполнения. Дедупликация предотвращает избыточные запросы до и во время их выполнения. После завершения запроса вступает в силу кэш.
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 (мемоизация) — это кэширование результата функции на время её выполнения. Если функция уже выполняется с теми же аргументами, новый вызов не запускает второй процесс, а получает результат первого. Это простейшая форма дедупликации для in-process сценариев.
Типичная реализация в мобильных приложениях — HashMap ключей в Deferred или Promise. Ключом обычно является строка URL запроса или конкатенация параметров. Время жизни записи — от первого запроса до завершения ответа. По данным Dropbox Engineering (2024), мемоизация в мобильном клиенте Dropbox сократила количество дублирующихся запросов к API на 40%.
Flawed deduplication — опасная ошибка: если не удалять ключ после ошибки, все последующие запросы навсегда вернут ту же ошибку. Корректная реализация должна обрабатывать Error и Failure, очищая кэш и позволяя повторную попытку.
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 (объединение запросов) — техника, при которой несколько разных запросов к одному источнику собираются в группу и отправляются одним 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.
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 — это библиотека (изначально для 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 пользователя. Также дедупликация может маскировать проблемы с сервером, скрывая реальную частоту запросов в метриках.
Оптимальное окно — 20–50 мс для пользовательских сценариев. Этого достаточно для сбора группы запросов, но недостаточно, чтобы пользователь заметил задержку. Для фоновых операций (логи, аналитика) окно можно увеличить до 200–500 мс. Эмпирическое правило: окно не должно превышать 10% от времени выполнения одного запроса.
Да, принцип тот же: если несколько частей приложения подписываются на один WebSocket-канал, дедупликатор открывает одно соединение и рассылает сообщения всем подписчикам. RxJava Share или Kotlin SharedFlow — идеальные инструменты для дедупликации WebSocket-сообщений на клиенте.
Используйте MockWebServer (OkHttp) для Android или OHHTTPStubs для iOS. Запустите 10 параллельных запросов с одинаковыми параметрами и проверьте, что сервер получил ровно один вызов. CountDownLatch или coroutineScope помогут синхронизировать параллельные вызовы в тесте.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также