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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

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