Request Deduplication — је механизам обједињања идентичних паралелних захтева у један, тако да извор података добија само један позив уместо десетина. У мобилним апликацијама дедупликација је посебно важна: више екрана могу истовремено захтевати исти профил корисника или листу производа. Према Square Engineering (2024), примена дедупликације је смањила оптерећење њиховог API-ја за 30% без промене логике сервера.
Главно
Request Deduplication — је техника која спречава извршавање више идентичних захтева истом извору података у једном временском прозору. Уместо да шаље 10 идентичних HTTP захтева, систем шаље један, а преосталих 9 чека његов резултат.
Проблем дуплирајућих захтева је посебно изражен у мобилним апликацијама са архитектуром заснованом на стању (MVVM, MVI, Redux). Када више посматрача претплаћу исте податке у кратком временском року, сваки покреће сопствени захтев, стварајући прекомерно оптерећење. Према Uber Engineering (2024), до 18% свих захтева у мобилним клијентима Uber-а су дупликати, а дедупликација на клијенту је смањила њихов број четири пута.
Дедупликација није исто што и кеширање. Дика чува резултат захтева након његовог извршавања. Дедупликација спречава сувишне захтеве пре и током њиховог извршавања. Након завршетка захтева, на сназу ступа дика.
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()
}
Ова Котлин класа гарантује да се за сваки кључ извршава само једна корутина. Сви паралелни позиви са истим кључем чекају један Deferred. Након завршетка кључ се брише, а следећи захтев се извршава нормално.
Смањење оптерећења сервера — први и очигледан разлог. Сваки дуплирајући захтев троши ресурсе сервера: CPU, меморију, везе са базом података. У размери милиона уређаја, чак и 10–15% дуплирајућих захтева ствара значајно оптерећење које захтева додатне сервере.
Смањење потрошње батерије и промета — сваки HTTP захтев на мобилном уређају троши енергију радио модула. Према Google I/O (2025), један неуспешан или дуплирајући захтев може потрошити до 15% енергије једне мрежне сесије. Дедупликација смањује број укључивања радио модула, продужавајући време рада уређаја на батерију.
Избегавање сукоба података — ако два дуплирајући захтева упишу податке у локалну меморију, могу настати race conditions: други захтев може преписати резултат првог застарелим подацима. Дедупликација гарантује да се упис у локалну меморију извршава једном, елиминишући трку.
Побољшање UX-а — корисник не види вишеструких индикатора учитавања за исте податке. UI стање (loading / success / error) се управља једним извором истине, а не више конкурентних захтева.
Memoization (мемоизација) — је кеширање резултата функције током њеног извршавања. Ако се функција већ извршава са истим аргументима, нови позив не покреће други процес, већ добија резултат првог. Ово је најједноставнији облик дедупликације за сценарије у оквиру једног процеса.
Типична имплементација у мобилним апликацијама — HashMap кључева у Deferred или Promise. Кључ је обично ниска URL захтева или конкатенација параметара. Животни век уноса — од првог захтева до завршетка одговора. Према Dropbox Engineering (2024), мемоизација у мобилном клијенту Dropbox-а је смањила број дуплирајућих захтева ка API-ју за 40%.
Flawed deduplication — опасна грешка: ако не уклоните кључ након грешке, сви каснији захтеви ће заувек враћати исту грешку. Исправна имплементација мора да обради грешке и неуспехе, чистећи дику и омогућавајући поновни покушај.
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 ms, свибира све ID-јеве и шаље један захтев /api/users?ids=1,2,3,4,5. Тајмаут прозора — кључни параметар: превише дуг прозор погоршава UX, превише кратак не дозвољава прикупљање довољно захтева.
Према Netflix Engineering (2023), у GraphQL BFF (Backend for Frontend) агрегатору, обједињање захтева је смањило број HTTP позива између слојева за 65% и просечно време одговора за 120 ms уклањањући сувишне 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 ms за сакупљање групе. Након истека тајмера, сви сакупљени ID-јеви шаљу се једним batch захтевом, и свака корутина добија сопствени резултат.
DataLoader — је библиотека (изворно за JavaScript/GraphQL) која имплементира batching и memoization на страни сервера. Групише све захтеве истом извору података у једном тику event loop-а и извршава их једним позивом. DataLoader се широко користи са GraphQL-ом, али се може примењивати у свакој REST апликацији.
Принцип рада: сви позиви loader.load(id) у једном микрозадатку се сакупљају у низ ID-јева и прослеђују батч функцији. Након пријема резултата, сваки 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 ms за корисничке сценарије. Ово је довољно за прикупљање групе захтева, али не превише да би корисник приметио кашњење. За позадинске операције (логови, аналитика) прозор се може повећати на 200–500 ms. Емпиријско правило: прозор не треба да прелази 10% времена извршавања једног захтева.
Да, принцип је исти: ако више делова апликације претплаћу један WebSocket канал, дедупликатор отвара једну везу и разиље поруке свим претплатницима. RxJava Share или Kotlin SharedFlow — идеални алати за дедупликацију WebSocket порука на клијенту.
Користите MockWebServer (OkHttp) за Android или OHHTTPStubs за iOS. Покрените 10 паралелних захтева са истим параметрима и проверите да је сервер примио тачно један позив. CountDownLatch или coroutineScope ће помоћи у синхронизацији паралелних позива у тесту.
Закључак
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође