RabbitMQ: exchange, binding và routing key

4 câu hỏi~30 phútTừ khóa: exchange, binding, routing key, direct, fanout, topic, headers, default exchange, @RabbitListener, Declarables

Ok, học 3 loại exchange. Nhớ gắn mỗi type với một bài toán nhé.

Được. Mỗi loại exchange sẽ gắn với một bài toán của shop-app, để bạn thấy lúc nào chọn loại nào.

1. Bài toán: OrderPlaced cần đến đúng nơi

Ở bài 8.1, order-service publish OrderPlaced. Giờ có thêm yêu cầu:

2. Vấn đề

Nếu producer gửi thẳng vào từng queue, order-service phải biết có những queue nào. Thêm analytics-service là phải sửa producer. Đây đúng là coupling mà ta muốn bỏ.

3. Giải pháp: Exchange + Binding

Producer ──(message + routing key)──> Exchange
                                        │  so routing key với binding
                                        v
                                      Queue ──> Consumer

Producer chỉ biết exchange + routing key. Ai nghe gì là chuyện của binding.

Direct exchange: routing key khớp chính xác binding key.

Bài toán: notification-service nhận lệnh gửi theo kênh. Lệnh email chỉ vào queue email, SMS chỉ vào queue SMS.

                       binding "email" ──> notification.email ──> EmailWorker
notification.direct ──┤
                       binding "sms"   ──> notification.sms   ──> SmsWorker

publish(routingKey="sms") → chỉ notification.sms nhận

Fanout exchange: bỏ qua routing key, gửi cho mọi queue đã bind.

Bài toán: OrderPlaced phải đến cả notification và inventory, sau này thêm analytics chỉ cần bind thêm queue.

                 ┌─> notification.order-placed ──> notification-service
order.fanout ────┼─> inventory.order-placed    ──> inventory-service
                 └─> analytics.order-placed    ──> analytics-service (thêm sau)

Topic exchange: routing key dạng a.b.c, binding dùng wildcard.

Bài toán: shop-app có nhiều event, mỗi service đăng ký theo nhóm.

exchange shop.events (topic)

binding "order.placed"       ──> inventory.order-placed
binding "order.*"            ──> notification.order-events
binding "payment.failed"     ──> notification.payment-failed
binding "#"                  ──> audit.all

publish "order.placed"    → inventory, notification, audit
publish "order.cancelled" → notification, audit
publish "payment.failed"  → notification.payment-failed, audit

Headers exchange (ít dùng): route theo header thay vì routing key, ví dụ x-match=all, channel=sms, region=vn. Linh hoạt nhưng chậm hơn và khó đọc; đa số trường hợp topic đủ dùng.

Default exchange (tên rỗng ""): mọi queue tự bind với key là tên queue. convertAndSend("email-queue", msg) trông như "gửi thẳng vào queue", thật ra vẫn qua exchange.

4. Code từng bước

Bước 1: Dependency: spring-boot-starter-amqp (như bài 8.1).

Bước 2: Khai báo topology ở phía consumer (mỗi service khai báo queue của mình):

@Configuration
public class InventoryRabbitConfig {

    @Bean
    TopicExchange shopEvents() {
        return ExchangeBuilder.topicExchange("shop.events").durable(true).build();
    }

    @Bean
    Queue inventoryOrderPlaced() {
        return QueueBuilder.durable("inventory.order-placed").build();
    }

    @Bean
    Binding inventoryBinding(Queue inventoryOrderPlaced, TopicExchange shopEvents) {
        return BindingBuilder.bind(inventoryOrderPlaced).to(shopEvents).with("order.placed");
    }

    @Bean
    MessageConverter jsonConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

RabbitAdmin (Spring Boot tự tạo) sẽ declare exchange, queue, binding lên broker khi app kết nối. Declare lại nhiều lần không sao, miễn là thuộc tính giống nhau.

Bước 3: Producer và consumer

// order-service
rabbitTemplate.convertAndSend("shop.events", "order.placed", event);
rabbitTemplate.convertAndSend("shop.events", "order.cancelled", cancelledEvent);
// inventory-service
@RabbitListener(queues = "inventory.order-placed")
public void onOrderPlaced(OrderPlaced event) {
    inventoryService.reserve(event.orderId());
}

// notification-service: một queue nhận nhiều loại event order.*
@RabbitListener(queues = "notification.order-events")
public void onOrderEvent(OrderEvent event,
                         @Header(AmqpHeaders.RECEIVED_ROUTING_KEY) String routingKey) {
    switch (routingKey) {
        case "order.placed"    -> notifier.orderConfirmed(event);
        case "order.cancelled" -> notifier.orderCancelled(event);
        default                -> log.warn("Unhandled {}", routingKey);
    }
}

Cũng có thể khai báo ngay trên annotation:

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "notification.payment-failed", durable = "true"),
    exchange = @Exchange(name = "shop.events", type = ExchangeTypes.TOPIC),
    key = "payment.failed"))
public void onPaymentFailed(PaymentFailed event) { ... }

Bước 4: Test

curl -X POST http://localhost:8081/orders -H "Content-Type: application/json" \
  -d '{"userId":10,"items":[{"sku":"TSHIRT-01","qty":2}]}'
# Log inventory-service:    reserve order 123
# Log notification-service: order.placed → send confirmation

# Management UI → Exchanges → shop.events → tab Bindings: thấy các binding
# Publish thử từ UI routing key "order.cancelled" → chỉ notification + audit nhận

5. Cách hoạt động

publish(exchange="shop.events", key="order.placed", body)
   |
   v
shop.events (topic)
   ├─ "order.placed"  khớp? ✅ → copy vào inventory.order-placed
   ├─ "order.*"       khớp? ✅ → copy vào notification.order-events
   ├─ "payment.failed"khớp? ❌
   └─ "#"             khớp? ✅ → copy vào audit.all
   |
   không khớp binding nào → message bị bỏ (trừ khi mandatory=true, xem bài 8.3)

6. So sánh

LoạiLuật routeBài toán shop-app
Directrouting key == binding keyLệnh gửi email/SMS theo kênh
FanoutMọi queue đã bindOrderPlaced đến mọi service quan tâm
TopicPattern với *, #Nhiều event, mỗi service đăng ký theo nhóm
HeadersSo khớp headerRoute theo nhiều thuộc tính (ít dùng)

7. Khi nào nên dùng / không nên dùng

Trong shop-app có event payment.succeeded và payment.failed. Mỗi service chỉ muốn nhận đúng loại mình cần. Chẳng lẽ cho mọi consumer nghe chung một queue rồi tự if theo loại event?

Không nên. Cách đúng là tạo nhiều queue, mỗi service một queue, thay vì để mọi consumer nghe chung một queue rồi tự if (eventType == ...). RabbitMQ lọc bằng binding trước khi message đến consumer.

Ví dụ trong shop-app khi thanh toán:

                         shop.events (topic)
          ┌──────────────────┼────────────────────┐
   payment.succeeded   inventory.failed     payment.failed
          │                  │                    │
          v                  v                    v
 inventory.payment-ok   payment.refund      order.payment-failed
          │                  │                    │
  inventory-service     payment-service       order-service
  (giữ hàng)            (hoàn tiền)           (hủy đơn)

Mỗi service có queue riêng, binding đúng những routing key nó cần. Consumer không phải nhận rồi bỏ.

Điểm quan trọng hơn: nếu cả inventory và notification đều cần payment.succeeded, không cho hai service consume chung một queue:

❌ cùng một queue                       ✅ mỗi service một queue
payment.queue                           payment.succeeded
  ├─> inventory      msg1                 ├─> inventory.payment-ok    → inventory
  └─> notification   msg2                 └─> notification.payment-ok → notification
mỗi message chỉ một bên nhận            mỗi bên nhận một bản sao

Quy tắc nhớ: queue thuộc về một service. Đặt tên <service>.<event> cho dễ quản lý.

Vậy nếu chạy 3 instance notification-service cùng nghe notification.order-events thì user có nhận 3 email không?

Không. Đây đúng là điểm interviewer hay gài. 3 instance cùng một queue là competing consumers: RabbitMQ chia message cho các instance (round-robin, có tính prefetch), mỗi message chỉ một instance xử lý.

notification.order-events
  ├─> instance #1   order 1, order 4
  ├─> instance #2   order 2, order 5
  └─> instance #3   order 3, order 6

Hệ quả: thứ tự xử lý giữa các instance không còn đảm bảo. Nếu cần thứ tự theo từng order, phải thiết kế thêm (một consumer duy nhất, hoặc consistent hash exchange / single active consumer). Phần chia việc trong một instance (concurrency, prefetch) học ở bài 8.3.

Phỏng vấn hỏi về exchange trong RabbitMQ thì trả lời sao?

Q1. Luồng message trong RabbitMQ?

🇻🇳 Tiếng Việt:

Producer publish message kèm routing key tới exchange. Exchange không lưu message, nó so routing key với binding để copy message vào các queue phù hợp. Consumer lấy message từ queue, xử lý xong thì ACK. Producer không gửi thẳng vào queue; ngay cả khi gửi bằng tên queue thì vẫn qua default exchange.

🇬🇧 English:

A producer publishes a message with a routing key to an exchange. The exchange does not store messages; it matches the routing key against bindings and copies the message into matching queues. Consumers read from queues and ack after processing. Producers never publish directly to a queue; even publishing by queue name goes through the default exchange.

Q2. Direct, fanout, topic khác nhau thế nào?

🇻🇳 Tiếng Việt:

Direct route khi routing key khớp chính xác binding key, ví dụ lệnh gửi email hay SMS. Fanout bỏ qua routing key, gửi cho mọi queue đã bind, ví dụ OrderPlaced đến cả notification và inventory. Topic dùng pattern với * (một từ) và # (nhiều từ), phù hợp khi có nhiều event và mỗi service đăng ký theo nhóm như order.*. Em thường dùng topic cho event giữa microservices.

🇬🇧 English:

Direct routes when the routing key exactly matches the binding key, for example email versus SMS commands. Fanout ignores the routing key and sends to every bound queue, for example OrderPlaced going to both notification and inventory. Topic uses patterns with * for one word and # for zero or more words, which fits many event types where each service subscribes to a group like order.*. I usually choose topic for events between microservices.

Q3. Hai service cùng cần một event thì thiết kế queue thế nào?

🇻🇳 Tiếng Việt:

Mỗi service một queue riêng bind vào exchange, để mỗi service nhận một bản sao. Nhiều consumer trên cùng một queue là competing consumers, mỗi message chỉ một consumer nhận, dùng để scale instance của cùng một service.

🇬🇧 English:

Each service gets its own queue bound to the exchange, so every service receives its own copy. Several consumers on one queue are competing consumers: each message goes to only one of them, which is how we scale instances of the same service.

Cách nhớ ngắn gọn


Tự kiểm tra

Binding order.* có khớp order.payment.failed không? Còn order.#?

order.* không khớp (chỉ một từ sau order). order.# khớp.

convertAndSend("email-queue", msg) có gửi thẳng vào queue không?

Không. Nó đi qua default exchange (tên rỗng), nơi mỗi queue được bind tự động với key là tên queue.

Message có routing key không khớp binding nào thì sao?

Bị exchange bỏ đi một cách im lặng, trừ khi publish với mandatory=true để broker trả về (return) cho producer, hoặc exchange có alternate exchange.

Thêm analytics-service cần OrderPlaced thì sửa ở đâu?

Chỉ ở analytics-service: khai báo queue riêng và binding order.placed vào shop.events. Producer không đổi.

Khi nào dùng headers exchange?

Khi cần route theo nhiều thuộc tính không gói gọn được trong routing key. Hiếm dùng; topic thường đủ.