Retry phải đi kèm idempotency, vì Celery mặc định là at-least-once: worker chết giữa chừng, hoặc acks_late bật, đều dẫn tới task chạy lại.
Retry có kiểm soát:
@shared_task(
bind=True,
autoretry_for=(requests.RequestException,),
retry_backoff=5, # 5s, 10s, 20s, 40s...
retry_jitter=True, # spread retries out
max_retries=5,
)
def charge(self, payment_id):
...- Chỉ retry lỗi tạm thời (timeout, 5xx, mất kết nối). Lỗi nghiệp vụ (thẻ bị từ chối, payload sai) retry vô nghĩa — để task fail và ghi log.
retry_backoff+retry_jittertránh cả cụm worker cùng đập vào API vừa hồi phục.max_retriesbắt buộc có; hết lượt thì đẩy sang dead-letter queue hoặc bảng chờ xử lý tay.
Idempotency — phần quan trọng hơn:
- Sinh idempotency key ổn định (thường là payment_id) và gửi kèm cho cổng thanh toán; hầu hết cổng dùng key này để nhận diện và trả về kết quả lần đầu thay vì trừ tiền lần nữa.
- Phía mình khoá bằng trạng thái + ràng buộc DB: UPDATE payment SET status='processing' WHERE id=%s AND status='pending'. Không có dòng nào bị ảnh hưởng nghĩa là task khác đã xử lý — thoát ngay.
Chốt: retry lo phần "thử lại", idempotency lo phần "chạy lại không hại". Thiếu vế thứ hai thì retry chính là nguyên nhân trừ tiền trùng.