Sáu bài vừa rồi dựng lên một đường ống không mất dữ liệu: ack thủ công, confirms, DLX, thử lại có độ trễ. Mỗi cơ chế trong đó đều đánh đổi cùng một thứ: thông điệp có thể tới hai lần. Bài này đo cái giá đó và ba cách trả.

RabbitMQ hứa at-least-once

Một thông điệp "cộng 100 vào số dư". Consumer làm xong việc rồi chết trước khi kịp ack, ba lần liên tiếp:

1 thông điệp, consumer chết 3 lần -> đã xử lý 3 lần, số dư = 300 (đúng ra phải 100)

Không có gì hỏng ở đây. Broker làm đúng: nó không thấy ack nên nó giao lại — chính là cơ chế đã cứu 949 thông điệp trong phần 16. Cùng một tính năng, nhìn từ phía nghiệp vụ thì thành lỗi cộng tiền ba lần.

Không có cấu hình nào biến RabbitMQ thành exactly-once. "Đúng một lần" là thứ đạt được ở consumer, không phải ở broker — bằng cách làm việc xử lý chịu được lặp lại.

Ba cửa sổ sinh ra bản sao

Cửa sổ Khi nào
Consumer làm xong nhưng chưa ack Tiến trình chết, mạng đứt, container bị thay
Thử lại sau lỗi tạm thời Chính là phần 21 — mỗi vòng là một lần xử lý nữa
Bên gửi không chắc confirm đã về Mất kết nối giữa lúc chờ; gửi lại là an toàn, không gửi lại là mất

Cửa sổ thứ ba hay bị quên: khi waitForConfirms ném ngoại lệ vì đứt kết nối, bạn không biết broker đã nhận hay chưa. Lựa chọn duy nhất an toàn là gửi lại — và chấp nhận có thể trùng.

Ba cách làm lũy đẳng

Bảng chống trùng. Lưu messageId đã xử lý, gặp lại thì bỏ qua.

if (daXuLy.add(messageId)) { lamViec(); }

Ghi đè thay vì cộng dồn. Đổi soDu += 100 thành soDu = 500, đổi INSERT thành UPSERT. Việc lặp lại cho cùng kết quả nên không cần nhớ gì cả — đây là cách rẻ nhất và nên là lựa chọn đầu tiên.

Cập nhật có điều kiện. UPDATE don SET trang_thai='DA_GIAO' WHERE id=? AND trang_thai='DANG_GIAO' — lần thứ hai đổi 0 dòng, và bạn biết ngay đó là bản sao.

Đo thông lượng cả ba trên 20 000 thông điệp:

Cách Thông lượng
Không chống trùng (cộng dồn) 131 119 msg/s
Bảng chống trùng theo messageId 135 608 msg/s
Ghi đè (UPSERT) 139 764 msg/s

Ba con số nằm trong nhiễu của nhau. Lũy đẳng không phải vấn đề thông lượng — nếu ai đó phản đối vì sợ chậm, bảng này là câu trả lời.

Cái giá thật là lưu trữ

Bảng chống trùng phải nhớ mọi khoá đã thấy. Đo trực tiếp bộ nhớ của tập khoá UUID:

Số khoá Bộ nhớ Mỗi khoá
100 000 11,2 MB 118 byte
1 000 000 116,5 MB 122 byte

Một triệu thông điệp mỗi ngày cần khoảng 116 MB chỉ để nhớ rằng mình đã thấy chúng — và đó là bản trong bộ nhớ, chưa tính chỉ mục nếu bạn để trong CSDL.

Đó là lý do "ghi đè" đáng thử trước: nó tốn 0 byte.

Giữ khoá bao lâu

Câu hỏi quyết định kích thước bảng. Khoá chỉ cần sống lâu hơn cửa sổ có thể sinh bản sao, và cửa sổ đó bằng tổng thang backoff của bạn cộng thời gian giữ trong hàng đợi.

Với thang 1 s → 5 s → 30 s → 2 phút → 10 phút của phần 21, giữ khoá 24 giờ là quá dư. Giữ vĩnh viễn là cách chắc chắn nhất để bảng chống trùng trở thành sự cố tiếp theo.

messageId không tự có

Điểm cuối, và nó làm hỏng mọi thứ ở trên nếu bỏ qua:

payload=không đặt messageId    message_id=None
payload=có đặt messageId       message_id='don-42'

RabbitMQ không sinh messageId. Không đặt thì nó là null, và bảng chống trùng của bạn sẽ chống trùng trên khoá null.

Và đừng dùng UUID ngẫu nhiên sinh lúc gửi: nếu bên gửi thử lại sau khi mất kết nối (cửa sổ thứ ba ở trên), nó sinh UUID mới và bản sao lọt qua. Khoá phải suy ra từ dữ liệu nghiệp vụ — mã đơn hàng, mã giao dịch — thứ không đổi giữa hai lần gửi cùng một sự việc.

Bài sau: khoảng trống lớn hơn — CSDL đã commit mà broker chưa nhận thông điệp, và mẫu outbox.

Thử ba mươi giây

# thong diep trong hang doi cua ban co messageId khong
curl -su guest:guest -X POST 'localhost:15672/api/queues/%2F/<ten>/get' \
  -H 'Content-Type: application/json' \
  -d '{"count":5,"ackmode":"ack_requeue_true","encoding":"auto"}' \
  | jq '.[].properties.message_id'

ack_requeue_true nghĩa là xem rồi trả lại, không nuốt mất. Ra toàn null thì bạn chưa có gì để chống trùng cả.