Request Deduplication — az azonos párhuzamos kérések egyetlen kéréssé egyesítésének mechanizmusa, így az adatforrás tucatnyi hívás helyett csak egy hívást kap. Mobilalkalmazásokban a deduplikáció különösen fontos: több képernyő egyszerre kérheti ugyanazt a felhasználói profilt vagy terméklistát. A Square Engineering (2024) szerint a deduplikáció bevezetése 30%-kal csökkentette az API terhelését anélkül, hogy megváltoztatta volna a szerver logikáját.
Főbb pontok
Request Deduplication — olyan technika, amely megakadályozza több azonos kérés végrehajtását ugyanahhoz az adatforráshoz egy időablakon belül. Ahelyett, hogy 10 azonos HTTP kérést küldene, a rendszer egyet küld, a többi 9 pedig vár az eredményére.
A duplikálódó kérések problémája különösen éles a mobilalkalmazásokban állapotalapú architektúrával (MVVM, MVI, Redux). Amikor több megfigyelő rövid időn belül ugyanazokra az adatokra fizet elő, mindegyik elindítja a saját kérését, többletterhelést okozva. Az Uber Engineering (2024) szerint az Uber mobil klienseiben az összes kérés akár 18%-a duplikálódik, és a kliens oldali deduplikáció négyede csökkentette a számukat.
A deduplikáció nem ugyanaz, mint a gyorsítótárazás. A gyorsítótár a kérés eredményét a végrehajtás után tárolja. A deduplikáció megakadályozza a többletkéréseket a végrehajtás előtt és alatt. A kérés befejezése után a gyorsítótár lép működésbe és tárolja az eredményt.
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()
}
Ez a Kotlin osztály garantálja, hogy minden kulcshoz csak egy korutin hajódik végre. Az összes párhuzamos hívás ugyanazzal a kulccsal egy Deferred-re vár. Befejezés után a kulcs törlődik, és a következő kérés normálisan hajódik végre.
Szerverterhelés csökkentése — az első és legnyilvánvalóbb ok. Minden duplikálódó kérés szervererőforrásokat (CPU, memória, adatbázis kapcsolatok) fogyaszt. Több millió eszköz skáláján még 10–15% duplikálódó kérés is jelentős terhelést okoz, ami további szervereket igényel.
Akkumulátor és adatforgalom megtakarítása — minden HTTP kérés egy mobileszközön energiát fogyaszt a rádiómodulból. A Google I/O (2025) szerint egy sikertelen vagy duplikálódó kérés akár egy hálózati munkamenet energiájának 15%-át is felemésztheti. A deduplikáció csökkenti a rádiómodul bekapcsolásainak számát, meghosszabbítva az eszköz akkumulátoros üzemidejét.
Adatütközések elkerülése — ha két duplikálódó kérés adatokat ír a helyi tárolóba, versenyhelyzet (race condition) alakulhat ki: a második kérés felülírhatja az első eredményét elavult adatokkal. A deduplikáció garantálja, hogy a helyi tárolóba írás egyszer történik, kiküszöbölve a versenyhelyzeteket.
UX javítása — a felhasználó nem lát több betöltő jelzőt ugyanazokhoz az adatokhoz. Az UI állapotot (loading / success / error) egyetlen igazságforrás kezeli, nem több versengő kérés.
Memoization (memorizálás) — a függvény eredményének gyorsítótárazása a végrehajtás idejére. Ha a függvény már fut ugyanazokkal az argumentumokkal, az új hívás nem indít második folyamatot, hanem megkapja az első eredményét. Ez a deduplikáció legegyszerűbb formája a folyamaton belüli forgatókönyvekhez.
Tipikus megvalósítás mobilalkalmazásokban — kulcsok HashMap-je Deferred-ben vagy Promise-ban. A kulcs általában a kérés URL stringje vagy a paraméterek összefűzése. A rekord élettartama — az első kéréstől a válasz befejeződéséig. A Dropbox Engineering (2024) szerint a memorizálás a Dropbox mobil kliensében 40%-kal csökkentette az API duplikálódó kéréseinek számát.
Flawed deduplication — veszélyes hiba: ha nem törli a kulcsot hiba után, az összes későbbi kérés örökre ugyanazt a hibát adja vissza. A helyes megvalósításnak kezelnie kell a hibákat és meghibásodásokat, törölnie kell a gyorsítótárat és engedélyeznie kell az újrapróbálkozást.
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()
}
A MemoizedLoader a Result<T> használja a helyes hibakezeléshez: siker esetén gyorsítótáraz, hiba esetén lehetővé teszi az újrapróbálkozást. Ez a megközelítés garantálja, hogy az átmeneti hálózati hiba nem blokkolja a későbbi kéréseket.
Request Merging (kérések összevonása) — technika, amelynél több különböző kérés ugyanahhoz a forráshoz egy csoportba gyűlik és egyetlen batch kérésként kerül elküldésre. A deduplikációval ellentétben a kérések nem azonosak — paramétereikben különböznek, de ugyanarra az erőforrásra hivatkoznak.
Tipikus forgatókönyv: az alkalmazás 5 képernyője különböző felhasználók profiljait kéri. 5 egyedi kérés helyett a /api/users/1, /api/users/2 stb. címekre, a rendszer 20 ms-ot vár, összegyűjti az összes ID-t és elküld egy kérést: /api/users?ids=1,2,3,4,5. Ablak időkorlát — kulcsfontosságú paraméter: a túl hosszú ablak rontja az UX-t, a túl rövid ablak nem teszi lehetővé a megfelelő számú kérés összegyűjtését.
A Netflix Engineering (2023) szerint a GraphQL BFF (Backend for Frontend) aggregátorban a kérések összevonása 65%-kal csökkentette a HTTP hívások számát a rétegek között és 120 ms-mal az átlagos válaszidőt a felesleges RTT-k kiküszöbölésével. Aszinkron ablak (debounce) — szabványos megvalósítás korutinokon vagy RxJava-n keresztül.
class BatchMergerT {
private val pending = ConcurrentLinkedQueue<Pair<String, CompletableDeferred<T>>>()
suspend fun get(id: String): T = suspendCoroutine { cont ->
pending.add(Pair(id, cont))
scheduleFlush()
}
}
Ez a mixin a suspendCoroutine-t használja az egyes kérések felfüggesztésére és egy 30 ms-os ablakot a csoport összegyűjtésére. Az időzítő lejárta után az összes összegyűjtött ID elküldésre kerül egyetlen batch kéréssel, és minden korutin megkapja a saját eredményét.
DataLoader — egy könyvtár (eredetileg JavaScript/GraphQL számára), amely batching-et és memorizálást valósít meg a szerver oldalán. Az event loop egy tickje alatt csoportosítja az összes kérést ugyanahhoz az adatforráshoz és egyetlen hívással hajtja végre őket. A DataLoader széles körben használják a GraphQL-lel, de bármely REST alkalmazásban alkalmazható.
Működési elv: az összes loader.load(id) hívás egy mikrotárgyon belül ID-k tömbjébe gyűlik és továbbítódik a batch függvénybe. Az eredmények beérkezése után minden ID megkapja a saját tömbelemét. A gyorsítótárazás a DataLoader-ben csak egy HTTP kérésen belül működik — a következő kérésnél a gyorsítótár törlődik, garantálva az adatok naprakészségét.
A Meta Engineering (2024) szerint a DataLoader bevezetése a Facebook GraphQL rétegében kiküszöbölte az N+1 problémát, 200-ról 10-re csökkentve az adatbázis lekérdezések számát egy tipikus oldalon. Batch scheduling — a DataLoader fő innovációja — a process.nextTick (Node.js) vagy DispatchQueue.main (iOS) használja a csoportosítás optimalizálására.
Memoization — optimális egyetlen folyamathoz (mobilalkalmazás, mikroszolgáltatás). Egyszerűen megvalósítható és hatékony az azonos párhuzamos hívásokhoz. Hátránya — nem működik folyamatok vagy eszközök között.
Request Merging — alkalmas BFF réteg vagy aggregátor szolgáltatás számára. A szerver oldalán batch végpontok támogatását igényli. A legjobb választás, amikor a frontend sok kis kérést küld különböző, de azonos típusú adatokhoz.
DataLoader — deduplikációs szabvány a GraphQL szerverek számára. Automatikusan megoldja az N+1 problémát és nem igényli a gyorsítótár kézi beállítását. Ajánlott minden GraphQL réteggel rendelkező szerver számára.
HTTP gyorsítótár deduplikációval — OkHttp (Android) vagy URLSession (iOS) szintjén konfigurálható deduplikáció Interceptor vagy delegate segítségével. OkHttp CacheInterceptor — egyéni elfogó, amely ellenőrzi, hogy egy kérés azonos URL-lel már fut-e és összevonja őket. Ez a módszer az üzleti logika alatti szinten működik és lefedi az alkalmazás összes kérését a funkciók kódjának megváltoztatása nélkül.
Gyakran Ismételt Kérdések
A deduplikáció megakadályozza a duplikálódó kérés végrehajtását, míg az első még fut. A gyorsítótárazás az eredményt a végrehajtás után tárolja. Kiegészítik egymást: a deduplikáció véd az ismételt kérések ellen betöltés közben, a gyorsítótár pedig az ismételt kérések ellen utána.
Ha a deduplikációs kulcs helytelenül van megválasztva. Például, ha minden felhasználó ugyanazt a kulcsot használja, az első kérés blokkolja az összes többit. A kulcsnak specifikusnak kell lennie: tartalmaznia kell az URL-t, paramétereket, felhasználó ID-t. A deduplikáció emellett elfedheti a szerverproblémákat, elrejtve a kérések valós gyakoriságát a metrikákban.
Optimális ablak — 20–50 ms a felhasználói forgatókönyvekhez. Ez elegendő a kérések csoportjának összegyűjtéséhez, de nem elég ahhoz, hogy a felhasználó észrevegye a késleltetést. Háttérműveletekhez (naplók, analitika) az ablak 200–500 ms-ra növelhető. Empirikus szabály: az ablak nem haladhatja meg egyetlen kérés végrehajtási idejének 10%-át.
Igen, az elv ugyanaz: ha az alkalmazás több része ugyanarra a WebSocket csatornára fizet elő, a deduplikátor egy kapcsolatot nyit és szétküldi az üzeneteket az összes előfizetőnek. RxJava Share vagy Kotlin SharedFlow — ideális eszközök a WebSocket üzenetek deduplikációjára a kliens oldalán.
Használja a MockWebServer (OkHttp) az Androidhoz vagy OHHTTPStubs az iOS-hez. Indítson el 10 párhuzamos kérést ugyanazokkal a paraméterekkel és ellenőrizze, hogy a szerver pontosan egy hívást kapott. CountDownLatch vagy coroutineScope segít a párhuzamos hívások szinkronizálásában a tesztben.
Összefoglaló
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is