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