Hình dung routing key như một chiếc chìa, còn binding key là ổ khoá: chìa chỉ mở đúng ổ khớp với nó từng rãnh một. Mài lệch một chút — một chữ viết hoa, một dấu cách thừa — thì chìa không mở được ổ nào, mà chẳng có chuông nào kêu. Và nếu nhiều ổ cùng nhận một dạng chìa, mỗi ổ mở ra một bản. Đó là toàn bộ direct exchange. Bốn kiểu exchange của RabbitMQ khác nhau ở đúng một chỗ: cách so khoá. Bài này mổ kiểu đơn giản nhất — direct, so khoá bằng phép bằng chính xác — và đo hai thứ hay bị đồn đoán về nó.
Một khoá, một đích
ch.queueBind("kho", "don-hang", "moi");
ch.queueBind("ke-toan", "don-hang", "thanh-toan");
ch.queueBind("van-chuyen", "don-hang", "giao");
gửi 3 thông điệp, 3 khoá khác nhau -> kho=1 ke-toan=1 van-chuyen=1
Đây là toàn bộ ý tưởng của direct exchange: routing key của thông điệp phải bằng đúng khoá của binding. Không có mẫu, không có ký tự đại diện, không phân biệt hoa thường theo kiểu "thông minh".
Chép chứ không chia
Hai hàng đợi cùng bind một khoá, gửi một thông điệp:
gửi 1 thông điệp khoá 'moi' -> kho=1 ke-toan=1
Một thông điệp vào, hai bản ra. Phần 3 đã chạm vào chuyện này và nó đáng nhắc lại vì đây là gốc của hiểu nhầm phổ biến nhất về RabbitMQ: exchange chép thông điệp sang mọi đích khớp, nó không chia việc. Muốn chia việc thì cho nhiều consumer vào cùng một hàng đợi — bài sau đo cả hai kiểu cạnh nhau.
Một hàng đợi nghe nhiều khoá
for (String k : new String[]{"moi", "thanh-toan", "giao"})
ch.queueBind("tong-hop", "don-hang", k);
bind 3 khoá, gửi 4 thông điệp -> tong-hop=3
Cái thứ tư mang khoá khac và không đi đâu cả. Một hàng đợi có bao nhiêu binding tuỳ ý; đây là cách dựng "hàng đợi tổng hợp" nghe vài loại sự kiện mà không cần đổi bên gửi.
Khớp đúng từng ký tự
Gửi năm biến thể của cùng một chữ vào một binding khoá moi:
gửi 5 biến thể: "moi", "Moi", "MOI", "moi ", " moi"
nhận được: 1
Chỉ đúng một cái tới đích. Bốn cái còn lại — khác hoa thường, thừa một dấu cách — biến mất không để lại gì. Không lỗi, không log, không bộ đếm nào nhúc nhích.
Cách bắt tận tay: bật mandatory và gắn ReturnListener như phần 4 đã đo — broker sẽ trả về 312 NO_ROUTE kèm chính cái routing key đã gửi.
Khoá rỗng vẫn là một khoá
bind khoá "" rồi gửi khoá "" -> nhận được 1
Chuỗi rỗng không phải "không có khoá", nó là một khoá bình thường có độ dài 0. Hữu ích khi bạn muốn một direct exchange hành xử như fanout cho đúng một hàng đợi — nhưng nếu đó là ý định thì hãy dùng fanout cho người sau đọc hiểu.
Default exchange: dùng được, bind thì không
ch.queueBind("thu-bind", "", "khoa-tu-dat");
-> channel 403 ACCESS_REFUSED - operation not permitted on the default exchange
Default exchange là một direct exchange, nhưng broker khoá nó lại: bạn không thêm binding vào đó được. Mọi binding của nó do broker tự tạo, mỗi hàng đợi một cái, khoá trùng tên hàng đợi — phần 5 đã thấy chúng trong list_bindings.
Nghĩa là với default exchange, routing key bắt buộc là tên hàng đợi. Không có cách nào khác.
500 binding không làm chậm gì cả
Câu hỏi tự nhiên: exchange có nhiều binding thì định tuyến có chậm đi không? Đo 20 000 thông điệp có publisher confirms, hâm nóng trước, trung vị 3 lượt:
| Số hàng đợi gắn vào exchange | Thông lượng |
|---|---|
| 1 | 132 701 msg/s |
| 50 | 136 230 msg/s |
| 500 | 140 709 msg/s |
Số binding tăng 500 lần, thông lượng không đổi — chênh lệch 6% nằm trong nhiễu và thậm chí nghiêng về phía nhiều binding hơn.
Lý do: direct exchange không duyệt danh sách binding. Nó tra bảng băm theo routing key, nên chi phí không phụ thuộc vào tổng số binding mà chỉ phụ thuộc vào số đích khớp. Ở phép đo trên, mỗi thông điệp luôn chỉ tới đúng một hàng đợi.
Đó cũng là lời cảnh báo cho bài sau: chi phí thật không nằm ở kiểu exchange hay số binding, mà ở số hàng đợi mà thông điệp thật sự rơi vào. Fanout sẽ cho thấy điều đó rất rõ.
Khi nào direct là đúng
Khi bạn phân loại theo một chiều duy nhất và rời rạc: loại sự kiện, mức độ ưu tiên, mã vùng. Danh sách khoá hữu hạn, ai cũng đọc hiểu, và thêm một loại mới chỉ tốn một queueBind.
Khi cần lọc theo nhiều chiều — don-hang.vn.gap — thì đó là việc của topic exchange, phần 10.
Còn khi đang truy một thông điệp thất lạc, xem toàn bộ bản đồ định tuyến của broker trong một lệnh:
# xem toan bo ban do dinh tuyen cua broker trong mot lenh
docker exec -u rabbitmq rmq rabbitmqctl -q list_bindings \
source_name source_kind destination_name routing_key
Dòng nào có source_name rỗng là binding ngầm của default exchange. Còn nếu bạn đang tìm một thông điệp thất lạc, hãy so cột routing_key ở đây với chuỗi bên gửi thật sự truyền vào — chúng khác nhau nhiều hơn bạn tưởng.
Mẫu số chung
Phép khớp bằng-chính-xác kèm một cú trượt trong im lặng là một quả mìn: một routing key lệch một ký tự — một chữ hoa, một dấu cách cuối — thì khớp không đích nào, mà "không khớp" nhìn giống hệt "chưa gửi gì". Đó là lý do "tôi publish rồi mà consumer chẳng thấy" là bí ẩn số một. Cùng kiểu trượt im lặng ấy ở khắp nơi: một map/dict tra không thấy thì trả nil/mặc-định chứ không báo lỗi, một switch không nhánh nào khớp thì rơi tọt qua, một khoá cấu hình gõ sai lặng lẽ dùng giá trị mặc định, một tên biến môi trường sai chính tả. Liều thuốc luôn giống nhau: làm cho nhánh "không khớp" kêu to lên (ở đây là mandatory + ReturnListener, chỗ khác là một phép kiểm "khoá này bắt buộc phải có"), và so từng ký tự của chuỗi thật, đừng tin con mắt — trình soạn thảo giấu dấu cách cuối, còn "Moi" với "moi" trông gần như nhau khi liếc qua.
Điều thứ hai: chi phí của một thao tác do cái nó thật sự chạm vào quyết định, không phải cái tồn tại. 500 binding cũng nhanh như 1 vì direct exchange tra bảng băm chứ không duyệt danh sách, nên giá bám theo số đích khớp chứ không theo tổng số binding — đó chính là lý do người ta dựng chỉ mục: biến "quét tất cả" thành "nhảy thẳng tới đáp án". Nên câu hỏi hiệu năng đúng luôn là "thao tác này đụng vào bao nhiêu thứ?", không phải "có bao nhiêu thứ tồn tại?" — cùng khác biệt giữa chỉ mục CSDL và quét toàn bảng, giữa HashMap và duyệt List. Và nó dẫn thẳng tới hoá đơn thật của bài sau: cái tốn kém là số hàng đợi một thông điệp rơi vào, chứ không phải kiểu exchange hay số binding treo trên đó.