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 — 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 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.
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ược | Công thức độ trễ | Thời gian tích lũy (3 lần) | Ứng dụng |
|---|---|---|---|
| Cố định | delay = D | 3 × D | Kịch bản đơn giản, timeout cục bộ |
| Tăng dần | delay = N × D | 6 × D | Giảm tải dần dần |
| Hàm mũ | delay = D × 2^N | 7 × D | Lỗi hàng loạt, dịch vụ đám mây |
| Hàm mũ + Jitter | delay = random(0, D × 2^N) | thay đổi | Tả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 — 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.
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ử.
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 — 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ị.
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.
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.
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.
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 catch và flatMap với độ trễ.
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.
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
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à 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.
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 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ụ.
Để 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ú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.
Đọc thêm