RabbitMQ: exchange, binding và routing key
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:
- Cả
notification-servicevàinventory-serviceđều phải nhậnOrderPlaced. notification-servicecó hai kênh: email và SMS, mỗi kênh một worker riêng.- Sắp tới có nhiều event:
order.placed,order.cancelled,payment.succeeded,payment.failed... mỗi service chỉ quan tâm một nhóm.
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
- Exchange: nhận message, không lưu, chỉ route.
- Binding: luật nối exchange với queue, thường kèm binding key.
- Routing key: nhãn producer gắn vào message.
- Queue: nơi lưu message chờ 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.
*khớp đúng một từ;#khớp không hoặc nhiều từ.
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ại | Luật route | Bài toán shop-app |
|---|---|---|
| Direct | routing key == binding key | Lệnh gửi email/SMS theo kênh |
| Fanout | Mọi queue đã bind | OrderPlaced đến mọi service quan tâm |
| Topic | Pattern với *, # | Nhiều event, mỗi service đăng ký theo nhóm |
| Headers | So khớp header | Route theo nhiều thuộc tính (ít dùng) |
7. Khi nào nên dùng / không nên dùng
- Topic là lựa chọn mặc định cho event giữa microservices: mở rộng tốt, vẫn dùng được như direct (binding không wildcard) hoặc fanout (
#). - Fanout khi chắc chắn mọi subscriber cần mọi message, ví dụ broadcast làm mới cache.
- Direct cho hàng đợi công việc (work queue) theo loại.
- Không nên đẩy logic nghiệp vụ vào routing quá phức tạp (headers nhiều điều kiện): khó debug.
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
- Muốn nhiều service đều nhận → nhiều queue.
- Muốn nhiều instance chia việc → nhiều consumer trên một queue.
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
- Exchange route, queue lưu, consumer xử lý.
- Direct = khớp chính xác; Fanout = phát cho tất cả; Topic = pattern
*/#. - Mỗi service một queue; nhiều instance chung queue.
- Lọc bằng binding, không lọc bằng
iftrong consumer.
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 đủ.