Chính sách thử lại trong phát triển di động — bản chất, chiến lược và nguyên tắc

Tác giả: IT Sectr Đã đăng: 2026-03-11 Thời gian đọc: 10 phút

Chính sách thử lại — là một tập hợp các quy tắc xác định thời điểm và cách thức ứng dụng di động tự động thử lại các cuộc gọi mạng thất bại. Với kết nối không ổn định hoặc lỗi máy chủ tạm thời, một chính sách thử lại được thiết kế tốt sẽ cải thiện độ tin cậy của ứng dụng mà không cần sự can thiệp của người dùng. Theo nghiên cứu của Google Developer Relations (2025), việc triển khai Chính sách thử lại đúng cách giúp giảm tỷ lệ yêu cầu bị mất từ 40–60% trong các ứng dụng di động có hoạt động mạng thường xuyên.

Những điểm chính

  • Chính sách thử lại — chiến lược tự động thử lại các yêu cầu khi mạng gặp sự cố hoặc lỗi máy chủ tạm thời.
  • Exponential backoff — phương pháp tăng dần thời gian trễ giữa các lần thử lại để giảm tải cho máy chủ.
  • Jitter — độ lệch ngẫu nhiên của thời gian trễ, ngăn chặn hiệu ứng bầy đàn (thundering herd).
  • Tính đẳng năng — yêu cầu chính để thử lại an toàn: một yêu cầu lặp lại không được gây ra tác dụng phụ.
  • Circuit Breaker — cơ chế dừng thử lại khi dịch vụ không khả dụng kéo dài để bảo toàn tài nguyên.

Chính sách thử lại là gì?

Chính sách thử lại — là chiến lược phần mềm xác định hành vi của máy khách khi yêu cầu mạng thất bại: lỗi nào nên thử lại, bao nhiêu lần, với độ trễ bao nhiêu và khi nào nên dừng thử. Trong ứng dụng di động, chính sách thử lại cực kỳ quan trọng do sự bất ổn định của mạng di động và các lỗi tạm thời có thể xảy ra ở phía máy chủ.

Một Chính sách thử lại cơ bản bao gồm ba tham số: số lần thử lại tối đa (maxRetries), độ trễ ban đầu (baseDelay) và chiến lược backoff. Ngoài ra, có thể chỉ định danh sách mã trạng thái HTTP cần kích hoạt thử lại và thời gian chờ để hủy tất cả các lần thử.

Theo cuốn sách “Designing Data-Intensive Applications” của Martin Kleppmann, 50% sự cố trong hệ thống phân tán là tạm thời và có thể giải quyết bằng cách thử lại. Điều này làm cho Chính sách thử lại trở thành một trong những cách hiệu quả nhất và rẻ nhất để cải thiện khả năng chịu lỗi của ứng dụng di động mà không cần thay đổi kiến trúc máy chủ.

Lỗi nào nên thử lại

Lỗi tạm thời (có thể thử lại) — là loại sự cố duy nhất mà Chính sách thử lại nên phản ứng. Chúng bao gồm hết thời gian chờ kết nối (SocketTimeoutException), máy chủ tạm thời không khả dụng (HTTP 503, 502) và lỗi DNS. Các lỗi vĩnh viễn — HTTP 400, 401, 403, 404 — không nên thử lại vì chúng cho thấy vấn đề nằm ở yêu cầu, không phải mạng hay máy chủ.

Theo AWS Architecture Blog, phân loại chính xác lỗi thành có thể thử lại và không thể thử lại là quyết định quan trọng nhất khi thiết kế Chính sách thử lại. Thử lại một yêu cầu không đẳng năng với HTTP 401 có thể dẫn đến khóa tài khoản, và thử lại HTTP 400 có thể tạo ra dữ liệu trùng lặp. Luôn cấu hình rõ ràng danh sách mã cần thử lại.

Các chiến lược thử lại cơ bản

Khoảng cố định — chiến lược đơn giản nhất: mỗi lần thử lại diễn ra sau cùng một khoảng thời gian. Ví dụ, với độ trễ 2 giây, ứng dụng thử lại yêu cầu sau 2, 2, 2 giây. Khoảng cố định dễ triển khai và có thể dự đoán trước, nhưng tạo ra tải đồng đều lên máy chủ khi xảy ra lỗi hàng loạt.

Khoảng tăng dần — độ trễ tăng tuyến tính sau mỗi lần thử lại: lần đầu sau 1 giây, lần hai sau 2 giây, lần ba sau 3 giây, v.v. Chiến lược này cho máy chủ nhiều thời gian hơn để phục hồi khi gặp lỗi liên tiếp, nhưng vẫn có thể dự đoán được đối với nhiều máy khách cùng thất bại.

Chiến lượcCông thức độ trễThời gian tích lũy (3 lần)Ứng dụng
Cố địnhdelay = D3 × DKịch bản đơn giản, timeout cục bộ
Tăng dầndelay = N × D6 × DGiảm tải dần dần
Hàm mũdelay = D × 2^N7 × DLỗi hàng loạt, dịch vụ đám mây
Hàm mũ + Jitterdelay = random(0, D × 2^N)thay đổiTải cao, microservice

Việc chọn chiến lược phụ thuộc vào bản chất của ứng dụng. Đối với các tác vụ nền đồng bộ dữ liệu trên thiết bị di động, chiến lược hàm mũ với jitter là tối ưu — nó mang lại xác suất thành công cao nhất với tải tối thiểu lên máy chủ và thiết bị người dùng.

Exponential Backoff và Jitter

Exponential backoff — chiến lược trong đó độ trễ giữa các lần thử lại tăng gấp đôi sau mỗi lần thử. Nếu độ trễ ban đầu là 1 giây, chuỗi độ trễ sẽ là 1, 2, 4, 8, 16 giây. Điều này cho máy chủ thời gian phục hồi tăng theo cấp số nhân.

Jitter — độ lệch ngẫu nhiên của độ trễ, ngăn chặn các yêu cầu thử lại đồng thời từ nhiều máy khách (vấn đề bầy đàn). Không có jitter, hàng nghìn máy khách với cùng Chính sách thử lại sẽ thử lại yêu cầu cùng lúc, tạo ra tải đột biến lên máy chủ. Jitter phân tán các lần thử lại theo thời gian.

Triển khai trong Kotlin với coroutines

Coroutines của Kotlin cho phép triển khai exponential backoff với jitter mà không chặn luồng chính. Hàm retry từ kotlinx-coroutines nhận điều kiện thử lại và một khối chứa nội dung yêu cầu, tự động quản lý độ trễ và số lần thử.

kotlin
suspend fun RetryPolicy.executeWithRetry(
    block: suspend () -> Result<T>
): Result<T> {
    var lastError: Throwable? = null
    repeat(maxRetries + 1) { attempt ->
        try {
            return block()
        } catch (e: Exception) {
            if (!isRetriable(e) || attempt == maxRetries) {
                return Result.failure(e)
            }
            val delay = (baseDelayMs * (1 shl attempt))
                .toLong()
            val jitteredDelay = (delay * (0.5 + Random.nextDouble())).toLong()
            delay(jitteredDelay)
            lastError = e
        }
    }
    return Result.failure(lastError!!)
}

Hàm executeWithRetry nhận một lambda với lệnh gọi mạng và thực thi nó với exponential backoff và jitter. Nếu lỗi không thể thử lại hoặc vượt quá số lần thử tối đa, hàm trả về lỗi. Độ trễ được nhân với hệ số ngẫu nhiên từ 0,5 đến 1,5 để phân phối đều các lần thử lại.

Circuit Breaker và dừng thử lại

Circuit Breaker — mẫu thiết kế ngăn chặn các yêu cầu thử lại vô tận khi dịch vụ không khả dụng kéo dài. Khi số lượng lỗi vượt quá ngưỡng, Circuit Breaker chuyển sang trạng thái OPEN và trả về lỗi ngay lập tức mà không thực thi yêu cầu, cho máy chủ thời gian phục hồi.

Trong ứng dụng di động, Circuit Breaker đặc biệt hữu ích khi API không khả dụng do bảo trì theo kế hoạch hoặc lỗi mạng của nhà mạng. Nếu không có nó, ứng dụng sẽ tiêu tốn pin và băng thông cho các lần thử lại vô tận, làm giảm trải nghiệm người dùng và rút ngắn thời lượng pin của thiết bị.

Sơ đồ trạng thái Circuit Breaker

Circuit Breaker có ba trạng thái: CLOSED (hoạt động bình thường, yêu cầu được thực thi), OPEN (sự cố, yêu cầu bị chặn) và HALF_OPEN (yêu cầu thử để kiểm tra phục hồi). Sau một thời gian chờ nhất định ở trạng thái OPEN, bộ ngắt chuyển sang HALF_OPEN và thực thi một yêu cầu — nếu thành công, quay lại CLOSED; nếu thất bại, quay lại OPEN.

kotlin
class CircuitBreaker(
    private val failureThreshold: Int = 3,
    private val timeoutMs: Long = 30000
) {
    private var state = State.CLOSED
    private var failureCount = 0
    private var lastFailureTime: Long = 0

    suspend fun T.protect(block: suspend () -> T): T {
        checkState()
        return try {
            val result = block()
            onSuccess()
            result
        } catch (e: Exception) {
            onFailure()
            throw e
        }
    }
}

Triển khai Circuit Breaker trong Kotlin bao gồm bộ đếm lỗi và bộ hẹn giờ phục hồi. Phương thức protect kiểm tra trạng thái hiện tại trước khi thực thi yêu cầu được bọc và cập nhật bộ đếm lỗi khi có sự cố. Sau khi đạt failureThreshold, tất cả yêu cầu đều bị từ chối ngay lập tức cho đến khi timeoutMs hết hạn.

Chính sách thử lại trong ứng dụng di động

Mạng di động có những đặc điểm khiến Chính sách thử lại trở nên đặc biệt quan trọng. Chuyển đổi giữa Wi-Fi và dữ liệu di động, mất tín hiệu trong tàu điện ngầm và đường hầm, chặn tạm thời ở cấp nhà mạng — tất cả các kịch bản này đều dẫn đến lỗi yêu cầu có thể được xử lý thành công bằng cách thử lại.

Trên Android, thư viện Retrofit và OkHttp cung cấp cơ chế thử lại tích hợp thông qua Interceptor. Trên iOS, tác vụ được giải quyết thông qua URLSessionConfiguration và ủy quyền tùy chỉnh. Đối với phát triển đa nền tảng, Ktor (KMP) bao gồm hỗ trợ thử lại tích hợp với các chiến lược có thể cấu hình.

Triển khai trên iOS với Combine

Combine — framework của Apple cho lập trình phản ứng. Toán tử retry trong Combine lặp lại publisher một số lần nhất định khi có lỗi, nhưng không cho phép cấu hình độ trễ giữa các lần thử lại. Để có Chính sách thử lại đầy đủ, sử dụng kết hợp tùy chỉnh giữa catchflatMap với độ trễ.

swift
extension Publisher {
    func retryWithBackoff(
        retries: Int = 3,
        baseDelay: TimeInterval = 1.0
    ) -> AnyPublisher<Output, Failure> {
        return self.catch { error -> AnyPublisher in
            guard retries > 0 else {
                return Fail(error).eraseToAnyPublisher()
            }
            return Just(())
                .delay(for: .seconds(baseDelay), scheduler: DispatchQueue.main)
                .flatMap { self.retryWithBackoff(
                    retries: retries - 1,
                    baseDelay: baseDelay * 2
                ) }
                .eraseToAnyPublisher()
        }
        .eraseToAnyPublisher()
    }
}

Phần mở rộng retryWithBackoff cho Publisher trong Combine triển khai exponential backoff thông qua các lệnh gọi đệ quy với bộ đếm giảm dần và độ trễ nhân đôi. Toán tử delay tạo khoảng dừng giữa các lần thử lại, trong khi catch chặn lỗi và quyết định nên thử lại hay trả về lỗi.

Các lỗi điển hình với Chính sách thử lại

Lỗi đầu tiên — thử lại yêu cầu mà không kiểm tra tính đẳng năng. Nếu máy chủ đã tạo tài nguyên nhưng không trả về xác nhận do lỗi mạng, việc thử lại sẽ tạo ra bản sao. Đối với yêu cầu POST, luôn sử dụng khóa đẳng năng (Idempotency-Key) trong tiêu đề hoặc chỉ thử lại cho GET, PUT và DELETE.

Lỗi thứ hai — thử lại vô tận. Luôn đặt số lần thử tối đa (3–5 cho ứng dụng di động) và tổng thời gian chờ cho tất cả các lần thử. Việc thử lại vô tận làm hao pin và tạo ra tải ký sinh lên máy chủ, đặc biệt là khi di chuyển cơ sở dữ liệu hoặc thay đổi API.

Lỗi thứ ba — bỏ qua ngữ cảnh của ứng dụng. Nếu người dùng đóng ứng dụng hoặc chuyển sang nền, các Chính sách thử lại đang hoạt động phải được hủy đúng cách. Sử dụng coroutines với SupervisorScope hoặc Combine với vòng đời UI để tự động hủy thử lại khi đóng màn hình.

Lỗi thứ tư — không ghi nhật ký các lần thử lại. Nếu không ghi nhật ký, bạn sẽ không biết có bao nhiêu yêu cầu đã được thử lại, lỗi nào đã xảy ra và Chính sách thử lại của bạn hiệu quả đến đâu. Thêm các chỉ số: số lần thử lại, thành công sau thử lại, phân phối độ trễ. Dữ liệu này sẽ giúp điều chỉnh các tham số chiến lược tối ưu cho ứng dụng cụ thể của bạn.

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

Nên thử lại yêu cầu bao nhiêu lần trong ứng dụng di động?

Số lần thử lại tối ưu là 3–5 lần cho hầu hết các kịch bản. Đối với đồng bộ nền, 5–7 lần có thể chấp nhận được; đối với yêu cầu tương tác (ví dụ: gửi biểu mẫu), không quá 3 lần. Số lần thử lại nhiều hơn không làm tăng xác suất thành công nhưng tiêu tốn pin và lưu lượng dữ liệu của người dùng.

Exponential backoff là gì theo cách đơn giản?

Exponential backoff — là việc nhân đôi độ trễ giữa các lần thử lại: 1 giây, 2, 4, 8, 16, v.v. Nếu máy chủ bị quá tải, khoảng dừng ngắn giữa các lần thử đầu tiên cho phép nó phản hồi nhanh chóng, trong khi khoảng dừng tăng dần sau mỗi lần thử tiếp theo cho máy chủ nhiều thời gian hơn để phục hồi.

Nên thử lại những mã trạng thái HTTP nào?

Chỉ thử lại các lỗi tạm thời: 408 (Request Timeout), 429 (Too Many Requests), 502 (Bad Gateway), 503 (Service Unavailable), 504 (Gateway Timeout). Lỗi 4xx (trừ 408 và 429) cho thấy vấn đề từ phía máy khách — thử lại chúng là vô nghĩa và có thể gây nguy hiểm cho dữ liệu người dùng.

Chính sách thử lại khác Circuit Breaker như thế nào?

Chính sách thử lại quản lý việc thử lại một yêu cầu đơn lẻ khi gặp lỗi. Circuit Breaker quản lý trạng thái kết nối với dịch vụ: khi lỗi tích tụ, nó mở mạch (OPEN) và chặn các yêu cầu mới. Thử lại hoạt động ở cấp độ cuộc gọi riêng lẻ, Circuit Breaker hoạt động ở cấp độ tích hợp dịch vụ.

Làm thế nào để kiểm tra Chính sách thử lại trên thiết bị di động?

Để kiểm tra Chính sách thử lại, sử dụng NetworkInterceptor (OkHttp) trên Android và URLProtocol (URLSession) trên iOS để mô phỏng lỗi mạng. Đặt các tham số: tần suất lỗi, thời gian không khả dụng và mã phản hồi. Các bài kiểm tra đơn vị với MockWebServer (OkHttp) hoặc OHHTTPStubs (iOS) xác minh logic thử lại mà không cần mạng thực.

Tổng kết

  • Chính sách thử lại — chiến lược tự động thử lại các yêu cầu mạng khi gặp lỗi tạm thời với các tham số có thể cấu hình về độ trễ và số lần thử.
  • Exponential backoff với jitter — chiến lược cơ bản cho ứng dụng di động, giảm tải máy chủ khi xảy ra lỗi hàng loạt và ngăn chặn hiệu ứng bầy đàn.
  • Tính đẳng năng — điều kiện bắt buộc để thử lại an toàn các yêu cầu không phải GET: nếu không có nó, việc thử lại sẽ tạo ra dữ liệu trùng lặp hoặc tác dụng phụ không mong muốn.
  • Circuit Breaker bổ sung cho Chính sách thử lại bằng cách ngăn chặn việc thử lại vô tận khi dịch vụ không khả dụng kéo dài và tiết kiệm tài nguyên thiết bị.
  • Phân loại lỗi thành có thể thử lại (503, 502, timeout) và không thể thử lại (400, 401, 403) là rất quan trọng để Chính sách thử lại hoạt động chính xác.
  • Tối đa 3–5 lần thử lại trong các kịch bản tương tác và tối đa 7 lần cho đồng bộ nền là giá trị tối ưu cho ứng dụng di động theo Google Developer Relations.
  • Khuyến nghị — triển khai Chính sách thử lại với exponential backoff, Circuit Breaker và ghi nhật ký cho tất cả các yêu cầu mạng trong ứng dụng di động.

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