Request Deduplication: khái niệm, phương pháp và cơ chế hoạt động

Tác giả: IT Sectr Đã đăng: 2026-06-13 Thời gian đọc: 9 phút

Request Deduplication là cơ chế kết hợp các yêu cầu song song giống hệt nhau thành một, để nguồn dữ liệu chỉ nhận được một lệnh gọi thay vì hàng chục. Trong ứng dụng di động, khử trùng lặp đặc biệt quan trọng: nhiều màn hình có thể đồng thời yêu cầu cùng một hồ sơ người dùng hoặc danh sách sản phẩm. Theo Square Engineering (2024), việc triển khai khử trùng lặp đã giảm tải API của họ xuống 30% mà không thay đổi logic máy chủ.

Những điểm chính

  • Request Deduplication — kỹ thuật trong đó các yêu cầu trùng lặp được hợp nhất thành một và kết quả được gửi đến tất cả người yêu cầu.
  • Memoization — lưu vào bộ nhớ đệm kết quả yêu cầu trong khi thực thi; các lệnh gọi tiếp theo nhận được đối tượng đã sẵn sàng.
  • Request Merging — kết hợp nhiều yêu cầu dữ liệu khác nhau thành một yêu cầu hàng loạt đến máy chủ.
  • DataLoader — thư viện từ GraphQL triển khai khử trùng lặp yêu cầu theo lô trên máy chủ.
  • Thời gian chờ cửa sổ — độ trễ ngắn (10–50 ms) để thu thập một nhóm các yêu cầu trùng lặp trước khi gửi.

Khử trùng lặp yêu cầu là gì?

Request Deduplication là kỹ thuật ngăn chặn việc thực thi nhiều yêu cầu giống hệt nhau đến cùng một nguồn dữ liệu trong cùng một cửa sổ thời gian. Thay vì gửi 10 yêu cầu HTTP giống hệt nhau, hệ thống gửi một yêu cầu, trong khi 9 yêu cầu còn lại chờ kết quả của nó.

Vấn đề yêu cầu trùng lặp đặc biệt nghiêm trọng trong các ứng dụng di động có kiến trúc dựa trên trạng thái (MVVM, MVI, Redux). Khi nhiều trình quan sát đăng ký cùng một dữ liệu trong một khoảng thời gian ngắn, mỗi trình kích hoạt yêu cầu riêng của nó, tạo ra tải trọng dư thừa. Theo Uber Engineering (2024), có tới 18% tổng số yêu cầu trong các ứng dụng khách di động của Uber là trùng lặp và việc khử trùng lặp phía máy khách đã giảm số lượng đó đi 4 lần.

Khử trùng lặp không giống với lưu vào bộ nhớ đệm. Bộ nhớ đệm lưu trữ kết quả yêu cầu sau khi thực thi. Khử trùng lặp ngăn chặn các yêu cầu dư thừa trước và trong quá trình thực thi của chúng. Sau khi yêu cầu hoàn thành, bộ nhớ đệm sẽ phát huy tác dụng.

kotlin
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()
}

Lớp Kotlin này đảm bảo rằng chỉ một coroutine được thực thi cho mỗi khóa. Tất cả các lệnh gọi đồng thời có cùng khóa đều chờ một Deferred duy nhất. Sau khi hoàn thành, khóa bị xóa và yêu cầu tiếp theo được thực thi bình thường.

Tại sao cần khử trùng lặp trong ứng dụng di động

Giảm tải máy chủ là lý do đầu tiên và rõ ràng nhất. Mỗi yêu cầu trùng lặp tiêu tốn tài nguyên máy chủ: CPU, bộ nhớ, kết nối cơ sở dữ liệu. Ở quy mô hàng triệu thiết bị, thậm chí 10–15% yêu cầu trùng lặp tạo ra tải đáng kể, đòi hỏi thêm máy chủ.

Giảm mức tiêu thụ pin và dữ liệu — mỗi yêu cầu HTTP trên thiết bị di động tiêu tốn năng lượng của mô-đun radio. Theo Google I/O (2025), một yêu cầu thất bại hoặc trùng lặp có thể tiêu tốn tới 15% năng lượng của một phiên mạng. Khử trùng lặp làm giảm số lần kích hoạt mô-đun radio, kéo dài thời lượng pin của thiết bị.

Tránh xung đột dữ liệu — nếu hai yêu cầu trùng lặp ghi dữ liệu vào bộ nhớ cục bộ, có thể xảy ra điều kiện cạnh tranh: yêu cầu thứ hai có thể ghi đè kết quả của yêu cầu thứ nhất bằng dữ liệu cũ. Khử trùng lặp đảm bảo rằng việc ghi vào bộ nhớ cục bộ chỉ xảy ra một lần, loại bỏ cạnh tranh.

Cải thiện UX — người dùng không thấy nhiều chỉ báo tải cho cùng một dữ liệu. Trạng thái giao diện người dùng (đang tải / thành công / lỗi) được quản lý bởi một nguồn sự thật duy nhất thay vì nhiều yêu cầu cạnh tranh.

Memoization — lưu vào bộ nhớ đệm trong bộ nhớ

Memoization là lưu vào bộ nhớ đệm kết quả của một hàm trong quá trình thực thi của nó. Nếu một hàm đã được thực thi với cùng đối số, một lệnh gọi mới không bắt đầu quy trình thứ hai mà nhận kết quả của lệnh gọi đầu tiên. Đây là hình thức khử trùng lặp đơn giản nhất cho các kịch bản trong quy trình.

Một triển khai điển hình trong ứng dụng di động là HashMap các khóa đến Deferred hoặc Promise. Khóa thường là chuỗi URL yêu cầu hoặc sự kết hợp các tham số. Vòng đời của mục nhập là từ yêu cầu đầu tiên cho đến khi phản hồi hoàn tất. Theo Dropbox Engineering (2024), memoization trong ứng dụng khách di động Dropbox đã giảm 40% các yêu cầu API trùng lặp.

Khử trùng lặp lỗi — một sai lầm nguy hiểm: nếu khóa không bị xóa sau khi có lỗi, tất cả các yêu cầu tiếp theo sẽ mãi mãi trả về cùng một lỗi. Một triển khai đúng đắn phải xử lý Error và Failure, xóa bộ nhớ đệm và cho phép thử lại.

kotlin
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 sử dụng Result<T> để xử lý lỗi chính xác: khi thành công — lưu vào bộ nhớ đệm, khi lỗi — cho phép thử lại. Cách tiếp cận này đảm bảo rằng sự cố mạng tạm thời không chặn các yêu cầu tiếp theo.

Request Merging — kết hợp theo lô

Request Merging là kỹ thuật trong đó nhiều yêu cầu khác nhau đến cùng một nguồn được thu thập thành một nhóm và gửi dưới dạng một yêu cầu hàng loạt. Không giống như khử trùng lặp, các yêu cầu ở đây không giống hệt nhau — chúng khác nhau về tham số nhưng hướng đến cùng một tài nguyên.

Một kịch bản điển hình: 5 màn hình ứng dụng yêu cầu hồ sơ của những người dùng khác nhau. Thay vì 5 yêu cầu riêng lẻ đến /api/users/1, /api/users/2, v.v., hệ thống chờ 20 ms, thu thập tất cả ID và gửi một yêu cầu /api/users?ids=1,2,3,4,5. Thời gian chờ cửa sổ là tham số chính: cửa sổ quá dài làm xấu UX, quá ngắn — không thu thập đủ yêu cầu.

Theo Netflix Engineering (2023), trong bộ tổng hợp GraphQL BFF (Backend for Frontend), việc hợp nhất yêu cầu đã giảm 65% số lượng lệnh gọi HTTP giữa các lớp và giảm 120 ms thời gian phản hồi trung bình nhờ loại bỏ RTT dư thừa. Cửa sổ không đồng bộ (debounce) là triển khai tiêu chuẩn thông qua coroutine hoặc RxJava.

kotlin
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 này sử dụng suspendCoroutine để tạm dừng mỗi yêu cầu và cửa sổ 30 ms để thu thập nhóm. Sau khi hết thời gian, tất cả ID được thu thập được gửi trong một yêu cầu hàng loạt và mỗi coroutine nhận được kết quả của nó.

Khử trùng lặp phía máy chủ qua DataLoader

DataLoader là một thư viện (ban đầu dành cho JavaScript/GraphQL) triển khai xử lý theo lô và memoization ở phía máy chủ. Nó nhóm tất cả các yêu cầu đến cùng một nguồn dữ liệu trong một lần quay vòng sự kiện và thực thi chúng bằng một lệnh gọi. DataLoader được sử dụng rộng rãi với GraphQL nhưng có thể được áp dụng trong bất kỳ ứng dụng REST nào.

Cách hoạt động: tất cả các lệnh gọi loader.load(id) trong một tác vụ vi mô được thu thập vào một mảng ID và chuyển đến hàm xử lý theo lô. Sau khi nhận được kết quả, mỗi ID nhận được phần tử mảng của nó. Bộ nhớ đệm trong DataLoader chỉ hoạt động trong một yêu cầu HTTP duy nhất — ở yêu cầu tiếp theo, bộ nhớ đệm bị xóa, đảm bảo tính tươi mới của dữ liệu.

Theo Meta Engineering (2024), việc triển khai DataLoader trong lớp GraphQL của Facebook đã loại bỏ vấn đề N+1, giảm số truy vấn cơ sở dữ liệu từ 200 xuống 10 cho mỗi trang điển hình. Lập lịch theo lô — cải tiến chính của DataLoader — sử dụng process.nextTick (Node.js) hoặc DispatchQueue.main (iOS) để tối ưu hóa việc nhóm.

Nên chọn chiến lược khử trùng lặp nào

Memoization là tối ưu cho một quy trình duy nhất (ứng dụng di động, vi dịch vụ). Dễ triển khai và hiệu quả cho các lệnh gọi song song giống hệt nhau. Nhược điểm — không hoạt động giữa các quy trình hoặc thiết bị.

Request Merging phù hợp cho lớp BFF hoặc dịch vụ tổng hợp. Yêu cầu hỗ trợ điểm cuối hàng loạt trên máy chủ. Lựa chọn tốt nhất khi frontend thực hiện nhiều yêu cầu nhỏ cho các dữ liệu khác nhau cùng loại.

DataLoader là tiêu chuẩn cho máy chủ GraphQL. Nó tự động giải quyết vấn đề N+1 và không yêu cầu cấu hình bộ nhớ đệm thủ công. Được khuyến nghị cho bất kỳ máy chủ nào có lớp GraphQL.

Bộ nhớ đệm HTTP với khử trùng lặp — ở cấp OkHttp (Android) hoặc URLSession (iOS), khử trùng lặp có thể được cấu hình qua Interceptor hoặc delegate. OkHttp CacheInterceptor là bộ chặn tùy chỉnh kiểm tra xem yêu cầu có cùng URL đã được thực thi hay chưa và hợp nhất chúng. Phương pháp này hoạt động dưới cấp độ logic nghiệp vụ và bao phủ tất cả các yêu cầu ứng dụng mà không thay đổi mã tính năng.

Các câu hỏi thường gặp

Khử trùng lặp khác với bộ nhớ đệm như thế nào?

Khử trùng lặp ngăn chặn việc thực thi yêu cầu trùng lặp trong khi yêu cầu đầu tiên vẫn đang chạy. Bộ nhớ đệm lưu kết quả sau khi thực thi. Chúng bổ sung cho nhau: khử trùng lặp bảo vệ khỏi các yêu cầu lặp lại trong khi tải, bộ nhớ đệm bảo vệ khỏi các yêu cầu lặp lại sau đó.

Khi nào khử trùng lặp có thể gây hại?

Nếu khóa khử trùng lặp được chọn không đúng. Ví dụ, nếu tất cả người dùng sử dụng một khóa, yêu cầu đầu tiên sẽ chặn tất cả các yêu cầu khác. Khóa phải cụ thể: bao gồm URL, tham số và ID người dùng. Khử trùng lặp cũng có thể che giấu vấn đề máy chủ bằng cách ẩn tần suất yêu cầu thực tế trong các chỉ số.

Làm thế nào để chọn thời gian chờ cửa sổ cho Request Merging?

Cửa sổ tối ưu là 20–50 ms cho các kịch bản người dùng. Điều này đủ để thu thập một nhóm yêu cầu nhưng không đủ để người dùng nhận thấy độ trễ. Đối với các hoạt động nền (nhật ký, phân tích), cửa sổ có thể tăng lên 200–500 ms. Quy tắc thực nghiệm: cửa sổ không được vượt quá 10% thời gian thực thi của một yêu cầu.

Khử trùng lặp có hoạt động với WebSocket không?

Có, nguyên tắc tương tự được áp dụng: nếu nhiều phần của ứng dụng đăng ký cùng một kênh WebSocket, bộ khử trùng lặp mở một kết nối duy nhất và phân phối tin nhắn đến tất cả người đăng ký. RxJava Share hoặc Kotlin SharedFlow là những công cụ lý tưởng để khử trùng lặp tin nhắn WebSocket trên máy khách.

Làm thế nào để kiểm tra khử trùng lặp?

Sử dụng MockWebServer (OkHttp) cho Android hoặc OHHTTPStubs cho iOS. Chạy 10 yêu cầu song song với các tham số giống hệt nhau và xác minh rằng máy chủ đã nhận được chính xác một lệnh gọi. CountDownLatch hoặc coroutineScope giúp đồng bộ hóa các lệnh gọi song song trong bài kiểm tra.

Tổng kết

  • Request Deduplication — hợp nhất các yêu cầu song song giống hệt nhau thành một với phân phối kết quả đến tất cả người yêu cầu.
  • Memoization — lưu kết quả vào bộ nhớ đệm trong khi thực thi; một phương pháp đơn giản và hiệu quả cho một quy trình duy nhất.
  • Request Merging — thu thập một nhóm các yêu cầu khác nhau thành một lô; yêu cầu hỗ trợ máy chủ và thời gian chờ cửa sổ.
  • DataLoader — tiêu chuẩn khử trùng lặp cho GraphQL; giải quyết vấn đề N+1 ở cấp máy chủ.
  • Có tới 18% yêu cầu trong ứng dụng di động là trùng lặp; khử trùng lặp giảm tải máy chủ và pin.
  • Khóa khử trùng lặp phải cụ thể: bao gồm URL, tham số và ngữ cảnh người dùng.
  • Thực hành tốt nhất — kết hợp khử trùng lặp phía máy khách (OkHttp Interceptor / URLSession) và phía máy chủ (DataLoader).

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm