Phần 21 dựng vòng thử lại bằng TTL và DLX — khá nhiều bộ phận. Spring AMQP cho bạn cùng ý tưởng bằng bốn dòng cấu hình. Bài này đo xem bốn dòng đó tốn gì.
Bốn dòng
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true
max-attempts: 3
initial-interval: 500ms
Listener ném ngoại lệ thì Spring bắt lại, chờ, gọi lại — tối đa 3 lần, rồi mới trả về cho container để đẩy sang DLX.
Cùng kết quả, gấp một nghìn lần thời gian
Một trăm thông điệp, trong đó 10% luôn lỗi, một consumer:
| Thời gian cả mẻ | Listener được gọi | Vào nghĩa địa | |
|---|---|---|---|
| Có retry (3 lần, chờ 500 ms) | 10,12 s | 120 lần | 10 |
| Không retry, hỏng là sang DLX | 0,01 s | 100 lần | 10 |
Cột cuối giống hệt nhau: mười thông điệp hỏng, cả hai cách đều đưa đủ mười cái sang nghĩa địa. Chỉ khác thời gian, và khác một nghìn lần.
Con số 10,11 giây chênh lệch không phải chi phí ẩn nào cả — nó là phép nhân đơn giản:
10 thông điệp hỏng × 2 lần thử thêm × 500 ms = 10 000 ms
Nó chặn cả những thông điệp phía sau
Đặt một thông điệp hỏng đứng đầu, năm thông điệp tốt xếp sau:
tot-0 sau 1.03 s
tot-4 sau 1.03 s
Thông điệp tốt đầu tiên phải đợi 1,03 giây — đúng bằng thời gian ba lần thử của cái hỏng. Đây chính là bức tranh mà phần 20 đã đo với client thuần, chỉ khác là lần này hàng đợi có tiến triển chứ không kẹt vĩnh viễn.
Tăng concurrency chỉ làm mờ vấn đề đi: với 4 consumer, một thông điệp hỏng chiếm một luồng trong 1 giây, còn ba luồng kia vẫn chạy. Nhưng nếu 10% thông điệp hỏng thì trung bình lúc nào cũng có một phần luồng của bạn đang ngồi chờ — và phần 25 đo được rằng thêm consumer không miễn phí.
Khi nào retry trong listener vẫn đúng
Khi độ trễ rất ngắn và lỗi rất hiếm. Chờ 50 ms để thử lại một truy vấn CSDL vừa timeout là hợp lý; nó rẻ hơn nhiều so với dựng hàng đợi chờ, và ở tần suất thấp thì phần luồng bị chiếm không đáng kể.
Khi thứ tự xử lý quan trọng. Đẩy thông điệp sang hàng đợi chờ nghĩa là nó quay lại sau những thông điệp đến sau nó. Nếu thứ tự trong một luồng nghiệp vụ phải giữ nguyên, chặn tại chỗ lại là hành vi đúng — và bạn phải chấp nhận cái giá ở bảng trên.
Khi đang dựng nguyên mẫu. Bốn dòng cấu hình vẫn hơn không có gì.
Khi nào chuyển sang hàng đợi chờ
Ngay khi thang backoff của bạn vượt quá một giây, hoặc tỉ lệ lỗi vượt quá vài phần trăm. Cách của phần 21 — reject sang DLX, hàng đợi chờ có TTL, quay về — giữ luồng consumer rảnh trong suốt thời gian chờ, nên thang 1 s → 5 s → 30 s không tốn gì cả.
Với Spring, việc đó là một RepublishMessageRecoverer trỏ vào exchange của hàng đợi chờ, thay cho RejectAndDontRequeueRecoverer mặc định:
@Bean
MessageRecoverer recoverer(RabbitTemplate tpl) {
return new RepublishMessageRecoverer(tpl, "x-cho-500", "");
}
Và nhớ điều phần 21 đo được: mỗi mức độ trễ phải có hàng đợi chờ riêng, nếu không thông điệp hẹn nửa giây sẽ nằm chờ sau thông điệp hẹn năm giây.
Bảng chọn
| Tình huống | Cách |
|---|---|
| Lỗi hiếm, chờ dưới 100 ms | retry trong listener |
| Thang backoff nhiều giây trở lên | hàng đợi chờ (phần 21) |
| Lỗi không bao giờ tự hết | không retry, sang DLX ngay |
| Cần biết đã thử mấy lần | x-death.count, không phải bộ đếm trong bộ nhớ |
Dòng cuối đáng nhắc lại: retry của Spring là không trạng thái theo mặc định — bộ đếm sống trong luồng đang xử lý. Tiến trình chết là bộ đếm về 0, và thông điệp bắt đầu lại từ đầu.
Bài sau: kiểm thử với Testcontainers — chạy broker thật trong test thay vì mock, và giữ thời gian chạy test ở mức chấp nhận được.
Thử ba mươi giây
# consumer cua ban co dang ngoi cho khong: so unacked cao ma deliver_get thap
curl -su guest:guest 'localhost:15672/api/queues/%2F/<ten>' \
| jq '{unacked: .messages_unacknowledged,
rate_giao: .message_stats.deliver_get_details.rate,
rate_ack: .message_stats.ack_details.rate}'
unacked đứng yên ở một con số nhỏ trong khi hai rate gần bằng 0 nghĩa là các luồng consumer đang ngủ trong Thread.sleep của retry — chứ không phải đang bận làm việc.