การตัดซ้ำคำขอ (Request Deduplication) เป็นกลไกที่รวมคำขอแบบขนานที่เหมือนกันเป็นคำขอเดียว เพื่อให้แหล่งข้อมูลได้รับการเรียกเพียงครั้งเดียวแทนที่จะเป็นหลายสิบครั้ง ในแอปพลิเคชันมือถือ การตัดซ้ำมีความสำคัญเป็นพิเศษ: หลายหน้าจออาจขอโปรไฟล์ผู้ใช้หรือรายการสินค้าเดียวกันพร้อมกัน ตามข้อมูลของ Square Engineering (2024) การนำการตัดซ้ำมาใช้ช่วยลดภาระ API ของพวกเขาลง 30% โดยไม่เปลี่ยนแปลงตรรกะของเซิร์ฟเวอร์
ประเด็นสำคัญ
Request Deduplication เป็นเทคนิคที่ป้องกันการดำเนินการคำขอที่เหมือนกันหลายรายการไปยังแหล่งข้อมูลเดียวกันภายในหน้าต่างเวลาเดียวกัน แทนที่จะส่งคำขอ HTTP ที่เหมือนกัน 10 รายการ ระบบจะส่งหนึ่งรายการ ในขณะที่อีก 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 นี้รับประกันว่าแต่ละคีย์จะมีการดำเนินการ coroutine เพียงหนึ่งเดียว การเรียกพร้อมกันทั้งหมดที่มีคีย์เดียวกันจะรอ Deferred ตัวเดียว หลังจากเสร็จสิ้น คีย์จะถูกลบออก และคำขอถัดไปจะดำเนินการตามปกติ
ลดภาระเซิร์ฟเวอร์ เป็นเหตุผลแรกและชัดเจนที่สุด คำขอที่ซ้ำกันแต่ละรายการใช้ทรัพยากรเซิร์ฟเวอร์: CPU, หน่วยความจำ, การเชื่อมต่อฐานข้อมูล ในระดับอุปกรณ์หลายล้านเครื่อง แม้แต่คำขอที่ซ้ำกัน 10–15% ก็สร้างภาระที่สำคัญ ซึ่งต้องใช้เซิร์ฟเวอร์เพิ่มเติม
ลดการใช้แบตเตอรี่และข้อมูล — คำขอ HTTP แต่ละรายการบนอุปกรณ์มือถือใช้พลังงานของโมดูลวิทยุ ตามข้อมูลของ Google I/O (2025) คำขอที่ล้มเหลวหรือซ้ำกันเพียงรายการเดียวสามารถใช้พลังงานมากถึง 15% ของเซสชันเครือข่ายเดียว การตัดซ้ำช่วยลดจำนวนการเปิดใช้งานโมดูลวิทยุ ซึ่งช่วยยืดอายุแบตเตอรี่ของอุปกรณ์
หลีกเลี่ยงความขัดแย้งของข้อมูล — หากคำขอที่ซ้ำกันสองรายการเขียนข้อมูลลงในที่จัดเก็บภายใน อาจเกิดสภาวะการแข่งขัน: คำขอที่สองอาจเขียนทับผลลัพธ์ของคำขอแรกด้วยข้อมูลที่ล้าสมัย การตัดซ้ำรับประกันว่าการเขียนลงในที่จัดเก็บภายในเกิดขึ้นเพียงครั้งเดียว ซึ่งช่วยขจัดการแข่งขัน
UX ที่ดีขึ้น — ผู้ใช้ไม่เห็นตัวบ่งชี้การโหลดหลายตัวสำหรับข้อมูลเดียวกัน สถานะ UI (กำลังโหลด / สำเร็จ / ข้อผิดพลาด) ถูกจัดการโดยแหล่งความจริงเดียวแทนที่จะเป็นคำขอที่แข่งขันกันหลายรายการ
Memoization คือการแคชผลลัพธ์ของฟังก์ชันระหว่างการดำเนินการ หากฟังก์ชันกำลังทำงานอยู่แล้วด้วยอาร์กิวเมนต์เดียวกัน การเรียกใหม่จะไม่เริ่มกระบวนการที่สอง แต่จะได้รับผลลัพธ์ของกระบวนการแรก นี่เป็นรูปแบบที่ง่ายที่สุดของการตัดซ้ำสำหรับสถานการณ์ภายในกระบวนการ
การใช้งานทั่วไปในแอปพลิเคชันมือถือคือ HashMap ของคีย์ไปยัง Deferred หรือ Promise โดยปกติคีย์คือสตริง URL ของคำขอหรือการต่อพารามิเตอร์ อายุของรายการคือตั้งแต่คำขอแรกจนกว่าการตอบสนองจะเสร็จสมบูรณ์ ตามข้อมูลของ Dropbox Engineering (2024) memoization ในไคลเอนต์มือถือ Dropbox ลดคำขอ API ที่ซ้ำกันลง 40%
การตัดซ้ำที่มีข้อบกพร่อง — ข้อผิดพลาดที่อันตราย: หากไม่ลบคีย์หลังจากเกิดข้อผิดพลาด คำขอที่ตามมาทั้งหมดจะส่งคืนข้อผิดพลาดเดียวกันตลอดไป การใช้งานที่ถูกต้องต้องจัดการกับ Error และ Failure โดยล้างแคชและอนุญาตให้ลองใหม่
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 (แบ็กเอนด์สำหรับฟร้อนท์เอนด์) การรวมคำขอช่วยลดจำนวนการเรียก HTTP ระหว่างเลเยอร์ลง 65% และเวลาในการตอบสนองเฉลี่ยลง 120 มิลลิวินาทีโดยการกำจัด RTT ที่เกินจำเป็น หน้าต่างแบบอะซิงโครนัส (debounce) เป็นการใช้งานมาตรฐานผ่าน coroutine หรือ 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 ที่รวบรวมทั้งหมดจะถูกส่งในคำขอแบบแบตช์เดียว และแต่ละ coroutine จะได้รับผลลัพธ์ของมัน
DataLoader เป็นไลบรารี (เดิมสำหรับ JavaScript/GraphQL) ที่ดำเนินการแบบแบตช์และการจดจำฝั่งเซิร์ฟเวอร์ มันรวบรวมคำขอทั้งหมดไปยังแหล่งข้อมูลเดียวกันภายในหนึ่ง tick ของลูปเหตุการณ์และดำเนินการด้วยการเรียกเดียว DataLoader ถูกใช้อย่างแพร่หลายกับ GraphQL แต่สามารถนำไปใช้ในแอปพลิเคชัน REST ใดก็ได้
วิธีการทำงาน: การเรียก loader.load(id) ทั้งหมดภายในไมโครแทสก์เดียวจะถูกรวบรวมเป็นอาร์เรย์ของ ID และส่งต่อไปยังฟังก์ชันแบบแบตช์ หลังจากได้รับผลลัพธ์ แต่ละ ID จะได้รับองค์ประกอบอาร์เรย์ของมัน การแคช ใน DataLoader ทำงานเฉพาะภายในคำขอ HTTP เดียว — ในคำขอถัดไป แคชจะถูกล้าง ซึ่งรับประกันความสดใหม่ของข้อมูล
ตามข้อมูลของ Meta Engineering (2024) การนำ DataLoader มาใช้ในเลเยอร์ GraphQL ของ Facebook ช่วยขจัดปัญหา N+1 โดยลดการสืบค้นฐานข้อมูลจาก 200 เหลือ 10 ต่อหน้าเว็บทั่วไป การจัดตารางแบบแบตช์ — นวัตกรรมสำคัญของ 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 สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ