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.

Đây là nguyên nhân số một của câu "tôi gửi rồi mà consumer không nhận". Trước khi đi đọc log broker, hãy in ra routing key thật và khoá binding thật rồi so từng ký tự — kể cả dấu cách cuối chuỗi mà trình soạn thảo cắt mất khi bạn nhìn.

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 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.

Thử ba mươi giây

# 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.