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 میمانند. پس از تکمیل، کلید حذف شده و درخواست بعدی بهطور عادی اجرا میشود.
کاهش بار سرور — اولین و واضحترین دلیل. هر درخواست تکراری منابع سرور (پروازنده، حافظه، اتصالات پایگاه داده) را مصرف میکند. در مقیاس میلیونها دستگاه، حتی 10–15% درخواستهای تکراری بار قابل توجهی ایجاد کرده که نیازمند سرورهای اضافی است.
کاهش مصرف باتری و ترافیک — هر درخواست HTTP در یک دستگاه موبایل انرژی ماژول رادیویی را مصرف میکند. به استناد به Google I/O (2025)، یک درخواست ناموفق یا تکراری میتواند تا 15% انرژی یک جلسه شبکه را مصرف کند. ددوپلیکیسیون تعداد روشن شدن ماژول رادیویی را کاهش داده و زمان کار دستگاه با باتری را افزایش میدهد.
جلوگیری از تعارض داده — اگر دو درخواست تکراری دادهها را در حافظه محلی بنویسند، race condition ممکن است رخ دهد: درخواست دوم میتواند نتیجه اولی را با دادههای کهنه بازنویسی کند. ددوپلیکیسیون تضمین میدهد که نوشتن در حافظه محلی یکبار انجام شود و شرایط مسابقه را از بین میبرد.
بهبود 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 (ادغام درخواستها) — تکنیکی است که چند درخواست مختلف به یک منبع در یک گروه جمع شده و به صورت یک درخواست باتچ ارسال میشوند. بر خلاف ددوپلیکیسیون، درخواستها یکسان نیستند — بر حسب پارامترها متفاوت هستند اما به یک منبع اشاره دارند.
سناریوی معمولی: 5 صفحه برنامه پروفایلهای کاربران مختلف را درخواست میکنند. به جای 5 درخواست تکی به /api/users/1، /api/users/2 و الخ، سیستم 20 میلیثانیه صبر میکند، کلیه IDها را جمع کرده و یک درخواست /api/users?ids=1,2,3,4,5 ارسال میکند. محدودیت زمان پنجره — پارامتر کلیدی: پنجره خیلی طولانی UX را بدتر میکند، پنجره خیلی کوتاه اجازه جمعآوری به میزان کافی درخواست را نمیدهد.
به استناد به Netflix Engineering (2023)، در تجمیعکننده GraphQL BFF (Backend for Frontend)، ادغام درخواستها تعداد فراخوانیهای HTTP بین لایهها را 65% و زمان پاسخ متوسط را 120 میلیثانیه کاهش داد با حذف 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()
}
}
این mixin از suspendCoroutine برای متوقف کردن هر درخواست و پنجره 30 میلیثانیه برای جمعآوری گروه استفاده میکند. پس از پایان تایمر، کلیه IDهای جمعآوری شده با یک درخواست باتچ ارسال شده و هر کوروتین نتیجه خود را دریافت میکند.
DataLoader — کتابخانهای است (ابتدا برای JavaScript/GraphQL) که batching و memoization را در طرف سرور پیادهسازی میکند. همه درخواستها به یک منبع داده را در یک تیک حلقه رویداد گروهبندی کرده و آنها را با یک فراخوانی اجرا میکند. DataLoader بهطور گسترده با GraphQL استفاده میشود اما میتواند در هر برنامه REST نیز مورد استفاده قرار گیرد.
اصل کار: همه فراخوانیهای loader.load(id) در یک میکروتاسک در یک آرایه ID جمع شده و به تابع batch ارسال میشوند. پس از دریافت نتایج، هر ID عضو ماربع خود را دریافت میکند. ذخیرهسازی در DataLoader تنها در چارچوب یک درخواست HTTP کار میکند — در درخواست بعدی کش پاک شده تا اطمینان از بهروزبودگی دادهها حاصل شود.
به استناد به Meta Engineering (2024)، پیادهسازی DataLoader در لایه GraphQL فیسبوک به الفائه رفع مشکل N+1 کمک کرد و تعداد درخواستها به پایگاه داده را از 200 به 10 در یک صفحه معمولی کاهش داد. Batch scheduling — نوآوری کلیدی DataLoader — از process.nextTick (Node.js) یا DispatchQueue.main (iOS) برای بهینهسازی گروهبندی استفاده میکند.
Memoization — برای یک پروسه (برنامه موبایل، میکروسرویس) بهینه است. در پیادهسازی ساده و برای فراخوانیهای موازی یکسان مؤثر است. نقطه ضعف — بین پروسهها یا دستگاهها کار نمیکند.
Request Merging — برای لایه BFF یا سرویس تجمیعکننده مناسب است. نیازمند پشتیبانی اندپوینتهای باتچ در سرور است. بهترین انتخاب وقتی است که فروند چندین درخواست کوچک به دادههای مختلف همان نوع انجام میدهد.
DataLoader — استاندارد ددوپلیکیسیون برای سرورهای GraphQL. به طور خودکار مشکل N+1 را حل کرده و نیازی به تنظیم دستی کش ندارد. برای هر سرور با لایه GraphQL توصیه میشود.
کش HTTP با ددوپلیکیسیون — در سطح OkHttp (Android) یا URLSession (iOS) میتوان ددوپلیکیسیون را از طریق Interceptor یا delegate تنظیم کرد. OkHttp CacheInterceptor — یک رهگیر سفارشی که بررسی میکند آیا درخواست با همان URL در حال اجرا است و آنها را ادغام میکند. این روش در سطح پایینتر از منطق کسبوکار کار کرده و تمام درخواستهای برنامه را بدون تغییر کد ویژگیها پوشش میدهد.
سوالات متداول
ددوپلیکیسیون از اجرای درخواست تکراری جلوگیری میکند در حالی که اولینی هنوز در حال اجرا است. ذخیرهسازی نتیجه را پس از اجرا ذخیره میکند. آنها یکدیگر را تکمیل میکنند: ددوپلیکیسیون از درخواستهای تکراری در زمان بارگیری محافظت میکند، کش از درخواستهای تکراری بعد از آن.
اگر کلید ددوپلیکیسیون به طور نادرست انتخاب شده باشد. به عنوان مثال، اگر همه کاربران از یک کلید استفاده کنند، اولین درخواست تمامی دیگر را بلوک میکند. کلید باید مخصوص باشد: شامل URL، پارامترها، ID کاربر. همچنین ددوپلیکیسیون میتواند مشکلات سرور را بپوشاند و تعداد واقعی درخواستها را در متریکها مخفی کند.
پنجره بهینه — 20–50 میلیثانیه برای سناریوهای کاربری. این برای جمعآوری گروه درخواست کافی است اما برای اینکه کاربر تأخیر را حس کند کافی نیست. برای عملیاتهای پسزمینه (لاگها، تحلیل) پنجره را میتوان به 200–500 میلیثانیه افزایش داد. قاعده تجربی: پنجره نباید از 10% زمان اجرای یک درخواست تجاوز کند.
بله، اصل یکسان است: اگر چند بخش برنامه در یک کانال WebSocket مشترک شوند، ددوپلیکاتور یک اتصال باز کرده و پیامها را به همه مشترکان ارسال میکند. RxJava Share یا Kotlin SharedFlow ابزارهای ایدهآل برای ددوپلیکیسیون پیامهای WebSocket در کلینت هستند.
از MockWebServer (OkHttp) برای Android یا OHHTTPStubs برای iOS استفاده کنید. 10 درخواست موازی با پارامترهای یکسان اجرا کرده و بررسی کنید که سرور دقیقاً یک فراخوانی دریافت کرده است. CountDownLatch یا coroutineScope به همگامسازی فراخوانیهای موازی در تست کمک میکنند.
نتایج
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید