Request Deduplication — este un mecanism de combinare a cererilor paralele identice într-una singură, astfel încât sursa de date primește un singur apel în loc de zeci. În aplicațiile mobile, deduplicarea este deosebit de importantă: mai multe ecrane pot solicita simultan același profil de utilizator sau lista de produse. Potrivit Square Engineering (2024), implementarea deduplicării a redus încărcarea API-ului lor cu 30% fără a modifica logica serverului.
Principalele
Request Deduplication — este o tehnică ce previne executarea mai multor cereri identice către aceeași sursă de date în aceeași fereastră de timp. În loc să trimită 10 cereri HTTP identice, sistemul trimite una, iar celelalte 9 așteaptă rezultatul acesteia.
Problema cererilor duplicate este deosebit de acută în aplicațiile mobile cu arhitectură bazată pe stări (MVVM, MVI, Redux). Când mai mulți observatori se abonează la aceleași date într-un interval scurt de timp, fiecare își lansează propria cerere, creând o încărcare suplimentară. Potrivit Uber Engineering (2024), până la 18% din toate cererile din clienții mobili Uber sunt duplicate, iar deduplicarea pe client le-a redus numărul de 4 ori.
Deduplicarea nu este același lucru cu stocarea în cache. Cache-ul stochează rezultatul cererii după executarea acesteia. Deduplicarea previne cererile excessive înainte și în timpul executării lor. După finalizarea cererii, intră în acțiune cache-ul.
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()
}
Această clasă Kotlin garantează că pentru fiecare cheie este executată o singură corutină. Toate apelurile paralele cu aceeași cheie așteaptă un singur Deferred. După finalizare, cheia este ștearsă, iar următoarea cerere se execută normal.
Reducerea încărcării serverului — primul și cel mai evident motiv. Fiecare cerere duplicată consumă resurse ale serverului: CPU, memorie, conexiuni la baza de date. La scară de milioane de dispozitive, chiar și 10–15% cereri duplicate creează o încărcare semnificativă ce necesită servere suplimentare.
Reducerea consumului de baterie și trafic — fiecare cerere HTTP pe un dispozitiv mobil consumă energie a modulului radio. Potrivit Google I/O (2025), o singură cerere eșuată sau duplicată poate consuma până la 15% din energia unei sesiuni de rețea. Deduplicarea reduce numărul de activări ale modulului radio, prelungind durata de funcționare a dispozitivului cu bateria.
Evitarea conflictelor de date — dacă două cereri duplicate scriu date în memoria locală, pot apărea race conditions: a doua cerere poate suprascrie rezultatul primei cu date învechite. Deduplicarea garantează că scrierea în memoria locală se execută o singură dată, eliminând competiția.
Improvement UX — utilizatorul nu vede indicatori multipli de încărcare pentru aceleași date. Starea UI (loading / success / error) este gestionată de o singură sursă de adevăr, nu de mai multe cereri concurente.
Memoization (memorizare) — este stocarea în cache a rezultatului unei funcții pe durata executării sale. Dacă funcția se execută deja cu aceleași argumente, un nou apel nu lansează un al doilea proces, ci primește rezultatul primului. Aceasta este cea mai simplă formă de deduplicare pentru scenarii în-proces.
Implementarea tipică în aplicațiile mobile — HashMap de chei în Deferred sau Promise. Cheia este de obicei un string URL al cererii sau concatenarea parametrilor. Durata de viață a înregistrării — de la prima cerere până la finalizarea răspunsului. Potrivit Dropbox Engineering (2024), memorizarea în clientul mobil Dropbox a redus numărul de cereri duplicate la API cu 40%.
Flawed deduplication — o eroare periculoasă: dacă nu ștergeți cheia după o eroare, toate cererile ulterioare vor returna aceeași eroare pentru totdeauna. O implementare corectă trebuie să gestioneze erorile și eșecurile, curățând cache-ul și permițând reîncercarea.
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 folosește Result<T> pentru gestionarea corectă a erorilor: la succes — stochează în cache, la eroare — permite reîncercarea. Această abordare garantează că o defecțiune temporară a rețelei nu va bloca cererile ulterioare.
Request Merging (combinarea cererilor) — tehnică în care mai multe cereri diferite către aceeași sursă sunt colectate în grup și trimise ca o singură cerere batch. Spre deosebire de deduplicare, cererile nu sunt identice — diferă prin parametri, dar se adresează aceleiași resurse.
Scenariu tipic: 5 ecrane ale aplicației solicită profilurile diferiților utilizatori. În loc de 5 cereri individuale la /api/users/1, /api/users/2 etc., sistemul așteaptă 20 ms, colectează toate ID-urile și trimite o singură cerere /api/users?ids=1,2,3,4,5. Timeout de fereastră — parametru cheie: o fereastră prea lungă degradează UX, una prea scurtă nu permite colectarea suficientor cereri.
Potrivit Netflix Engineering (2023), în agregatorul GraphQL BFF (Backend for Frontend), combinarea cererilor a redus numărul de apeluri HTTP între straturi cu 65% și timpul mediu de răspuns cu 120 ms prin eliminarea RTT-urilor suplimentare. Fereastră asincronă (debounce) — implementare standard prin corutine sau 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()
}
}
Acest mixin folosește suspendCoroutine pentru a suspenda fiecare cerere și o fereastră de 30 ms pentru a colecta grupul. După expirarea timer-ului, toate ID-urile colectate sunt trimise într-o singură cerere batch, iar fiecare corutină primește rezultatul său.
DataLoader — este o bibliotecă (inițial pentru JavaScript/GraphQL) care implementează batching și memorizare pe partea de server. Grupează toate cererile la aceeași sursă de date într-un singur tick al event loop și le execută printr-un singur apel. DataLoader este utilizat pe scară largă cu GraphQL, dar poate fi aplicat în orice aplicație REST.
Principiu de funcționare: toate apelurile loader.load(id) într-un singur microtask sunt colectate într-un array de ID-uri și transmise funcției batch. După primirea rezultatelor, fiecare ID primește elementul său din array. Stocarea în cache în DataLoader funcționează doar în cadrul unei singure cereri HTTP — la următoarea cerere, cache-ul este resetat, garantând actualitatea datelor.
Potrivit Meta Engineering (2024), implementarea DataLoader în stratul GraphQL al Facebook a eliminat problema N+1, reducând numărul de cereri la baza de date de la 200 la 10 pe o pagină tipică. Batch scheduling — inovația cheie a DataLoader — folosește process.nextTick (Node.js) sau DispatchQueue.main (iOS) pentru optimizarea grupării.
Memoization — optimă pentru un singur proces (aplicație mobilă, microserviciu). Simplu de implementat și eficientă pentru apeluri paralele identice. Dezavantaj — nu funcționează între procese sau dispozitive.
Request Merging — potrivită pentru stratul BFF sau un serviciu agregator. Necesită suport pentru endpoint-uri batch pe server. Cea mai bună alegere când frontend-ul face multe cereri mici la diferite date de același tip.
DataLoader — standard de deduplicare pentru serverele GraphQL. Rezolvă automat problema N+1 și nu necesită configurare manuală a cache-ului. Recomandat pentru orice server cu un strat GraphQL.
Cache HTTP cu deduplicare — la nivelul OkHttp (Android) sau URLSession (iOS) se poate configura deduplicarea printr-un Interceptor sau delegate. OkHttp CacheInterceptor — un interceptator personalizat care verifică dacă o cerere cu același URL este deja în execuție și le combină. Această metodă funcționează la un nivel inferior logicii de business și acoperă toate cererile aplicației fără a modifica codul funcționalităților.
întrebări frecvente
Deduplicarea previne executarea unei cereri duplicate în timp ce prima încă se execută. Stocarea în cache păstrează rezultatul după execuție. Ele se completeazĄ reciproc: deduplicarea protejează împotriva cererilor repetate în timpul încărcării, cache-ul — împotriva cererilor repetate după.
Dacă cheia de deduplicare este aleasă incorect. De exemplu, dacă toți utilizatorii folosesc aceeași cheie, prima cerere va bloca toate celelalte. Cheia trebuie să fie specifică: să includă URL, parametri, ID-ul utilizatorului. De asemenea, deduplicarea poate masca problemele cu serverul, ascunzând frecvența reală a cererilor în metrici.
Fereastra optimă — 20–50 ms pentru scenarii cu utilizator. Este suficient pentru a colecta un grup de cereri, dar nu suficient pentru ca utilizatorul să observe întârzierea. Pentru operații de fundal (loguri, analiză), fereastra poate fi mărită la 200–500 ms. Regulă empirică: fereastra nu trebuie să depășească 10% din timpul de execuție a unei singure cereri.
Da, principiul este același: dacă mai multe părți ale aplicației se abonează la același canal WebSocket, deduplicatorul deschide o singură conexiune și distribuie mesajele tuturor abonaților. RxJava Share sau Kotlin SharedFlow — instrumente ideale pentru deduplicarea mesajelor WebSocket pe client.
Folosiți MockWebServer (OkHttp) pentru Android sau OHHTTPStubs pentru iOS. Lansati 10 cereri paralele cu aceiași parametri și verificați că serverul a primit exact un apel. CountDownLatch sau coroutineScope vă vor ajuta să sincronizați apelurile paralele în test.
Concluzii
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și