Ba bài trước đi qua direct, fanout và topic. Còn một kiểu nữa, và nó là kiểu duy nhất bạn có thể đi cả sự nghiệp mà không gặp. Bài này đo xem vì sao.

Khớp theo bảng header, không nhìn khoá

Headers exchange bỏ qua routing key hoàn toàn — không phải "bỏ qua như fanout để cho tiện", mà là không có chỗ nào trong thuật toán khớp đọc tới nó:

routing key ""          -> KHỚP (bỏ qua khoá)
routing key "bat-ky"    -> KHỚP (bỏ qua khoá)
routing key "loai.don"  -> KHỚP (bỏ qua khoá)

Thay vào đó, binding mang theo một bảng khoá–giá trị, và thông điệp khớp khi bảng header của nó thoả bảng đó.

ch.queueBind("kho", "hx", "", Map.of(
        "x-match", "all",     // hoặc "any"
        "loai", "don",
        "vung", "vn"));

x-match: all hay any

Bốn binding nhân bảy thông điệp, mỗi ô một lần gửi thật:

binding                     loai=don   loai=don  vung=vn  loai=don   uu-tien=1  uu-tien="1"  (không
                            vung=vn                       vung=us    (số)       (chuỗi)      header)
──────────────────────────────────────────────────────────────────────────────────────────────────
all: loai=don, vung=vn         ✓          ·         ·         ·          ·           ·          ·
any: loai=don, vung=vn         ✓          ✓         ✓         ✓          ·           ·          ·
all: loai=don                  ✓          ✓         ·         ✓          ·           ·          ·
all: uu-tien=1 (số)            ·          ·         ·         ·          ✓           ·          ·

Đọc ra ba luật:

all là phép AND, any là phép OR. Dòng một khớp đúng một thông điệp; dòng hai khớp bốn.

Header thừa trong thông điệp không sao. Dòng ba khớp cả loai=don,vung=us — binding chỉ đòi những khoá nó nêu, phần còn lại kệ. x-match bản thân nó cũng không bị coi là một khoá phải khớp.

Không có ký tự đại diện. Không có *, không có #, không so tiền tố, không so khoảng. Chỉ có bằng hoặc không bằng. Đây là khác biệt lớn nhất so với topic, và nó chỉ theo một chiều: mọi thứ headers làm được thì topic cũng làm được bằng cách nhét các chiều vào khoá, còn ngược lại thì không — topic so được tiền tố, headers thì không.

1 không khớp "1"

Dòng cuối bảng là cái bẫy đáng nhớ nhất:

binding: {x-match: all, uu-tien: 1}     ← số nguyên
thông điệp uu-tien = 1   (số)    -> KHỚP
thông điệp uu-tien = "1" (chuỗi) -> không khớp

Phép so khớp tính cả kiểu dữ liệu. Một bên gửi bằng Java đặt Map.of("uu-tien", 1), một bên khác đọc cấu hình từ YAML rồi đặt "1" — hai thông điệp trông giống hệt nhau trong log, một cái tới đích và một cái biến mất.

Ghép cái bẫy này với chuyện phần 7 đã đo — chuỗi trong header quay về client Java dưới dạng LongString chứ không phải String — và bạn có một vùng mà mắt thường không phân biệt nổi đúng sai. Với headers exchange, hãy khai kiểu của từng header ở một chỗ và dùng chung.

Ba kiểu exchange, cùng một phép đo

20 000 thông điệp, 50 hàng đợi gắn vào, mỗi thông điệp khớp đúng một đích, có publisher confirms, hâm nóng trước, trung vị 3 lượt:

Kiểu Thông lượng So với direct
direct 120 725 msg/s 100%
topic 108 049 msg/s 90%
headers 50 454 msg/s 42%

Headers chậm hơn direct 2,4 lần. Nhưng con số đáng lo hơn nằm ở chỗ khác: nó không phẳng theo số binding. Cùng phép đo, chỉ đổi số hàng đợi gắn vào exchange, mỗi thông điệp vẫn chỉ khớp đúng một đích:

Số binding topic headers
1 123 691 msg/s 99 753 msg/s
50 118 696 msg/s 51 819 msg/s
500 113 361 msg/s 7 413 msg/s

Số binding tăng 500 lần: topic mất 8%, headers mất 93% — tụt 13 lần.

Lý do nằm ở cấu trúc dữ liệu. Direct tra bảng băm theo khoá; topic đi trên cây tiền tố — phần 8 đã đo direct phẳng lì tới 500 binding, và bảng trên cho thấy topic cũng vậy. Headers thì phải duyệt qua từng binding và so từng cặp khoá–giá trị. Đó là kiểu duy nhất trong bốn kiểu không có chỉ mục thật sự, và cột phải chính là hình dạng của việc không có chỉ mục.

Và phép đo này mới chỉ dùng một header mỗi binding. Nhiều header thì mỗi binding tốn nhiều phép so hơn nữa.

Vì sao gần như không ai dùng

Gom lại thì có ba lý do, và không lý do nào là "nó hỏng":

Kém diễn đạt hơn topic — không ký tự đại diện, không gom nhóm theo tiền tố.

Chậm hơn 2,4 lần ở quy mô nhỏ, và tệ dần theo số binding trong khi hai kiểu kia đứng yên.

Khó đọc khi vận hành. Mọi binding headers đều có routing key rỗng, nên muốn biết nó khớp gì thì phải đi đọc cột arguments:

source_name  destination_name  routing_key  arguments
             hq                hq           []
hxx          hq                             [{"loai","don"},{"x-match","all"}]

Lúc gỡ lỗi nửa đêm, don.vn.# nói ngay được điều nó làm; một bảng như trên thì không.

Khi nào nó vẫn đúng

Khi các chiều phân loại thật sự độc lập và không có thứ bậc. Khoá topic ép bạn chọn một thứ tự từ trái sang phải; nếu không có chiều nào tự nhiên đứng trước, mọi thứ tự đều sai với một nhóm người dùng nào đó. Headers không bắt bạn chọn.

Khi thuộc tính để lọc đã nằm sẵn trong header vì lý do khác — content-type, phiên bản schema, mã nguồn gửi — và bạn không muốn nhân đôi chúng vào routing key, nơi chúng sẽ lệch nhau sớm muộn.

Ngoài hai trường hợp đó, hãy dùng topic. Và nếu bạn đang cân nhắc headers vì khoá topic trông xấu, thì vấn đề nằm ở cách đặt khoá — phần 10 có phần bàn về chuyện đó.

Bài sau: default exchange, cái bẫy ẩn sau một chuỗi rỗng.

Thử ba mươi giây

# xem binding headers cua ban that su doi gi
docker exec -u rabbitmq rmq rabbitmqctl -q list_bindings \
    source_name destination_name routing_key arguments

Cột routing_key rỗng còn arguments có nội dung nghĩa là binding headers. Nếu bảng đó dài hơn màn hình, bạn vừa tìm ra lý do thứ ba ở trên.