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% от енергията на една мрежова сесия. Дедупликацията намалява броя на включванията на радиомодула, удължавайки времето за работа на устройството с батерия.
Избягване на конфликти на данни — ако две дублиращи се заявки записват данни в локалното хранилище, могат да възникнат 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 и мемоизация от страна на сървъра. Групира всички заявки към един източник на данни в рамките на един tick на 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 ms за потребителски сценарии. Това е достатъчно за събиране на група заявки, но не достатъчно, за да забележи потребителят закъснението. За фонови операции (логове, аналитика) прозорецът може да се увеличи до 200–500 ms. Емпирично правило: прозорецът не трябва да надвишава 10% от времето за изпълнение на една заявка.
Да, принципът е същият: ако няколко части на приложението се абонират за един WebSocket канал, дедупликаторът отваря една връзка и разпраща съобщенията до всички абонати. RxJava Share или Kotlin SharedFlow — идеални инструменти за дедупликация на WebSocket съобщения на клиента.
Използвайте MockWebServer (OkHttp) за Android или OHHTTPStubs за iOS. Стартирайте 10 паралелни заявки с еднакви параметри и проверете, че сървърът е получил точно едно повикване. CountDownLatch или coroutineScope ще помогнат за синхронизиране на паралелните повиквания в теста.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също