Request Deduplication — to mechanizm łączenia identycznych równoległych żądań w jedno, aby źródło danych otrzymywało tylko jedno wywołanie zamiast kilkunastu. W aplikacjach mobilnych deduplikacja jest szczególnie ważna: kilka ekranów może jednocześnie żądać tego samego profilu użytkownika lub listy produktów. Według Square Engineering (2024), wdrożenie deduplikacji zmniejszyło obciążenie ich API o 30% bez zmiany logiki serwera.
Najważniejsze
Request Deduplication — to technika zapobiegająca wykonaniu kilku identycznych żądań do jednego źródła danych w obrębie jednego okna czasowego. Zamiast wysyłać 10 identycznych żądań HTTP, system wysyła jedno, a pozostałe 9 czeka na jego wynik.
Problem duplikujących się żądań jest szczególnie dotkliwy w aplikacjach mobilnych z architekturą opartą na stanach (MVVM, MVI, Redux). Gdy kilku obserwatorów subskrybuje te same dane w krótkim odstępie czasu, każdy uruchamia własne żądanie, tworząc nadmierne obciążenie. Według Uber Engineering (2024), aż 18% wszystkich żądań w mobilnych klientach Uber to duplikaty, a deduplikacja po stronie klienta zmniejszyła ich liczbę czterokrotnie.
Deduplikacja to nie to samo co buforowanie. Pamięć podręczna przechowuje wynik żądania po jego wykonaniu. Deduplikacja zapobiega nadmiernym żądaniom przed i w trakcie ich wykonywania. Po zakończeniu żądania wchodzi w grę pamięć podręczna.
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()
}
Ta klasa Kotlin gwarantuje, że dla każdego klucza wykonywana jest tylko jedna korutyna. Wszystkie równoległe wywołania z tym samym kluczem oczekują na jednego Deferred. Po zakończeniu klucz jest usuwany, a następne żądanie jest wykonywane normalnie.
Zmniejszenie obciążenia serwera — pierwszy i oczywisty powód. Każde duplikujące się żądanie zużywa zasoby serwera: CPU, pamięć, połączenia z bazą danych. W skali milionów urządzeń nawet 10–15% duplikujących się żądań tworzy znaczne obciążenie wymagające dodatkowych serwerów.
Oszczędność baterii i transferu — każde żądanie HTTP na urządzeniu mobilnym zużywa energię modułu radiowego. Według Google I/O (2025), jedno nieudane lub duplikujące się żądanie może pochłonąć do 15% energii pojedynczej sesji sieciowej. Deduplikacja zmniejsza liczbę włączeń modułu radiowego, wydłużając czas pracy urządzenia na baterii.
Unikanie konfliktów danych — jeśli dwa duplikujące się żądania zapisują dane w pamięci lokalnej, mogą wystąpić race conditions: drugie żądanie może nadpisać wynik pierwszego nieaktualnymi danymi. Deduplikacja gwarantuje, że zapis do pamięci lokalnej jest wykonywany jednokrotnie, eliminując ścigi.
Poprawa UX — użytkownik nie widzi wielokrotnych wskaźników ładowania dla tych samych danych. Stan UI (loading / success / error) jest zarządzany przez jedno źródło prawdy, a nie przez kilka konkurujących ze sobą żądań.
Memoization (memoizacja) — to buforowanie wyniku funkcji na czas jej wykonywania. Jeśli funkcja jest już wykonywana z tymi samymi argumentami, nowe wywołanie nie uruchamia drugiego procesu, lecz otrzymuje wynik pierwszego. To najprostsza forma deduplikacji dla scenariuszy wewnątrzprocesowych.
Typowa implementacja w aplikacjach mobilnych — HashMap kluczy w Deferred lub Promise. Kluczem jest zazwyczaj string URL żądania lub konkatenacja parametrów. Czas życia rekordu — od pierwszego żądania do zakończenia odpowiedzi. Według Dropbox Engineering (2024), memoizacja w mobilnym kliencie Dropbox zmniejszyła liczbę duplikujących się żądań do API o 40%.
Flawed deduplication — niebezpieczny błąd: jeśli nie usuniesz klucza po błędzie, wszystkie kolejne żądania na zawsze zwrócą ten sam błąd. Prawidłowa implementacja powinna obsługiwać błędy i awarie, czyszcząc pamięć podręczną i umożliwiając ponowne próby.
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 używa Result<T> do prawidłowej obsługi błędów: przy sukcesie — buforuje, przy błędzie — umożliwia ponowną próbę. To podejście gwarantuje, że tymczasowa awaria sieci nie zablokuje kolejnych żądań.
Request Merging (łączenie żądań) — technika, w której kilka różnych żądań do jednego źródła jest zbieranych w grupę i wysyłanych jako jedno żądanie batch. W przeciwieństwie do deduplikacji, żądania nie są identyczne — różnią się parametrami, ale odnoszą się do jednego zasobu.
Typowy scenariusz: 5 ekranów aplikacji żąda profili różnych użytkowników. Zamiast 5 pojedynczych żądań do /api/users/1, /api/users/2 itd. system czeka 20 ms, zbiera wszystkie ID i wysyła jedno żądanie /api/users?ids=1,2,3,4,5. Limit czasowy okna — kluczowy parametr: zbyt długie okno pogarsza UX, zbyt krótkie nie pozwala zebrać wystarczającej liczby żądań.
Według Netflix Engineering (2023), w agregatorze GraphQL BFF (Backend for Frontend) łączenie żądań zmniejszyło liczbę wywołań HTTP między warstwami o 65%, a średni czas odpowiedzi — o 120 ms dzięki eliminacji zbędnych RTT. Asynchroniczne okno (debounce) — standardowa implementacja przez korutyny lub 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()
}
}
Ta domieszka używa suspendCoroutine do wstrzymania każdego żądania i okna 30 ms do zebrania grupy. Po upływie timera wszystkie zebrane ID są wysyłane jednym żądaniem batch, a każda korutyna otrzymuje swój wynik.
DataLoader — to biblioteka (pierwotnie dla JavaScript/GraphQL), która implementuje batching i memoizację po stronie serwera. Grupuje wszystkie żądania do jednego źródła danych w ciągu jednego ticka event loop i wykonuje je jednym wywołaniem. DataLoader jest szeroko używany z GraphQL, ale może być stosowany w każdej aplikacji REST.
Zasada działania: wszystkie wywołania loader.load(id) w ciągu jednego mikrozadania są zbierane w tablicę ID i przekazywane do funkcji batch. Po otrzymaniu wyników każdy ID otrzymuje swój element tablicy. Buforowanie w DataLoader działa tylko w ramach jednego żądania HTTP — przy następnym żądaniu pamięć podręczna jest czyszczona, co gwarantuje aktualność danych.
Według Meta Engineering (2024), wdrożenie DataLoader w warstwie GraphQL Facebooka pozwoliło wyeliminować problem N+1, zmniejszając liczbę żądań do bazy danych z 200 do 10 na typowej stronie. Batch scheduling — kluczowa innowacja DataLoader — używa process.nextTick (Node.js) lub DispatchQueue.main (iOS) do optymalizacji grupowania.
Memoization — optymalny dla jednego procesu (aplikacja mobilna, mikrousługa). Prosty w implementacji i skuteczny dla identycznych równoległych wywołań. Minus — nie działa między procesami ani urządzeniami.
Request Merging — odpowiedni dla warstwy BFF lub serwisu agregującego. Wymaga obsługi endpointów batch po stronie serwera. Najlepszy wybór, gdy frontend wykonuje wiele małych żądań do różnych danych tego samego typu.
DataLoader — standard deduplikacji dla serwerów GraphQL. Automatycznie rozwiązuje problem N+1 i nie wymaga ręcznej konfiguracji pamięci podręcznej. Zalecany dla każdego serwera z warstwą GraphQL.
Pamięć podręczna HTTP z deduplikacją — na poziomie OkHttp (Android) lub URLSession (iOS) można skonfigurować deduplikację przez Interceptor lub delegate. OkHttp CacheInterceptor — niestandardowy przechwytywacz, który sprawdza, czy żądanie z tym samym URL jest już wykonywane, i łączy je. Ta metoda działa na poziomie poniżej logiki biznesowej i obejmuje wszystkie żądania aplikacji bez zmiany kodu funkcji.
Często zadawane pytania
Deduplikacja zapobiega wykonaniu duplikującego się żądania, gdy pierwsze jest jeszcze wykonywane. Buforowanie przechowuje wynik po wykonaniu. Uzupełniają się nawzajem: deduplikacja chroni przed powtarzającymi się żądaniami podczas ładowania, pamięć podręczna — przed powtarzającymi się żądaniami po.
Jeśli klucz deduplikacji zostanie wybrany nieprawidłowo. Na przykład, jeśli wszyscy użytkownicy używają jednego klucza, pierwsze żądanie zablokuje wszystkie pozostałe. Klucz musi być specyficzny: zawierać URL, parametry, ID użytkownika. Deduplikacja może też maskować problemy z serwerem, ukrywając rzeczywistą częstotliwość żądań w metrykach.
Optymalne okno — 20–50 ms dla scenariuszy użytkownika. To wystarczy, aby zebrać grupę żądań, ale nie na tyle długo, by użytkownik zauważył opóźnienie. Dla operacji w tle (logi, analityka) okno można zwiększyć do 200–500 ms. Zasada empiryczna: okno nie powinno przekraczać 10% czasu wykonania pojedynczego żądania.
Tak, zasada jest ta sama: jeśli kilka części aplikacji subskrybuje ten sam kanał WebSocket, deduplikator otwiera jedno połączenie i rozsyła wiadomości do wszystkich subskrybentów. RxJava Share lub Kotlin SharedFlow — idealne narzędzia do deduplikacji wiadomości WebSocket po stronie klienta.
Użyj MockWebServer (OkHttp) dla Android lub OHHTTPStubs dla iOS. Uruchom 10 równoległych żądań z tymi samymi parametrami i sprawdź, czy serwer otrzymał dokładnie jedno wywołanie. CountDownLatch lub coroutineScope pomogą zsynchronizować równoległe wywołania w teście.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również