RabbitMQ căn bản
Tung Bui Thanh (TungDaDev)43 mins read

Any sufficiently complex distributed system contains an ad-hoc, informally-specified, bug-ridden, slow implementation of half of a message queue. Paraphrased from Greenspun's Tenth Rule
Mình từng deploy một hệ thống microservices mà các service gọi nhau bằng REST synchronous. Mọi thứ chạy ngon lành cho đến khi traffic tăng gấp 10 lần vào Black Friday. Order Service gọi Inventory Service, Inventory gọi Payment, Payment gọi Notification... Một service chết là cả chuỗi đổ như domino. Latency p99 từ 200ms nhảy lên 15 giây. Lúc đó mình mới thật sự hiểu vì sao người ta cần message queue.
Bài này gom lại những gì mình ước được biết sớm hơn về RabbitMQ. Không dừng ở "Hello World", mà đi từ kiến trúc bên trong tới các pattern cho production với Java và Spring Boot.
# Message Queue là gì
Thử tưởng tượng bạn đang ở một nhà hàng đông khách.
Không có message queue: khách đứng trước bếp, chờ đầu bếp nấu xong món của mình thì người sau mới được gọi món. Gặp một món cầu kỳ là cả hàng đứng chờ.
Có message queue: khách ghi món ra giấy, đặt lên kệ rồi về bàn ngồi. Đầu bếp lấy giấy trên kệ theo thứ tự, nhiều đầu bếp có thể cùng lấy và nấu song song.
Cái "kệ order" đó là message queue.
# vấn đề thực tế mà Message Queue giải quyết

Vấn đề:
- Temporal coupling: tất cả service phải cùng online một lúc.
- Latency cộng dồn: 100ms + 100ms + 100ms + 100ms, tối thiểu đã 400ms.
- Cascading failure: Payment chết kéo Inventory timeout, rồi Order fail theo.
- Khó scale: lúc cao điểm, cả chuỗi service phải scale cùng nhau.
Với message queue:

Lợi ích:
- Decoupling: Order Service không cần biết ai sẽ consume.
- Async: trả response ngay, phần xử lý chạy ở background.
- Buffer: traffic tăng đột biến thì queue giữ lại, consumer xử lý dần.
- Resilience: service chết thì message vẫn nằm trong queue, chờ service sống lại xử lý tiếp.
# vì sao nên dùng message queue
- Decoupling (Giảm độ liên kết): Producer không cần biết Consumer là ai, ở đâu, viết bằng ngôn ngữ gì. Thêm bớt Consumer thoải mái mà không động vào code của Producer.
- Buffering (Đệm & giảm tải): Khi request đổ về nhanh hơn khả năng xử lý của Consumer, Queue làm bộ đệm hấp thụ. Consumer không bị quá tải (overwhelm) mà cứ lấy message ra xử lý theo sức của mình.
- Resilience (Khả năng phục hồi): Consumer lỡ chết đột ngột thì message vẫn nằm an toàn trong Queue. Khi Consumer sống lại, những message chưa được ack sẽ được giao lại để xử lý tiếp.
# RabbitMQ: bức tranh tổng thể
# RabbitMQ là gì?
RabbitMQ là một message broker mã nguồn mở, ban đầu xây quanh giao thức AMQP 0-9-1 (Advanced Message Queuing Protocol); từ bản 4.0 nó hỗ trợ thêm AMQP 1.0 như giao thức "ruột", bên cạnh MQTT và STOMP qua plugin. RabbitMQ viết bằng Erlang, ngôn ngữ do Ericsson tạo ra cho tổng đài điện thoại, nơi hệ thống gần như không được phép dừng.
Vì sao lại là Erlang? Ngôn ngữ này có những đặc tính rất hợp với Message Broker:
- Hỗ trợ hàng triệu process siêu nhẹ (lightweight processes) chạy đồng thời.
- Hot code swapping (update code hệ thống trực tiếp mà không cần reboot).
- Hỗ trợ tính toán phân tán (distributed computing) ngay từ lõi.
- Chịu lỗi (fault tolerance) tốt nhờ mô hình Supervisor Trees.
Nhờ nền Erlang này, một cluster RabbitMQ được tinh chỉnh tốt có thể xử lý hàng chục đến hàng trăm nghìn message mỗi giây với độ trễ thấp. Con số thực tế phụ thuộc nhiều vào kích thước message, loại queue, persistence và phần cứng, nên hãy tự benchmark với workload của mình.
# lịch sử phát triển
- 2007: Rabbit Technologies Ltd chính thức ra mắt RabbitMQ.
- 2010: SpringSource (khi đó thuộc VMware) mua lại Rabbit Technologies.
- 2013: Dự án được chuyển giao cho Pivotal Software (một nhánh tách ra từ VMware).
- 2019: VMware mua lại Pivotal, đưa RabbitMQ trở lại mái nhà xưa.
- Hiện tại: thuộc sở hữu của Broadcom (sau thương vụ thâu tóm VMware) nhưng vẫn giữ định hướng open-source với cộng đồng rất lớn.
# những ông lớn nào đang dùng rabbitmq?
- Reddit: codebase cũ (mã nguồn mở) dùng RabbitMQ làm hàng đợi cho các tác vụ nền như xử lý vote, comment.
- Mozilla: hệ thống Pulse, kênh phát sự kiện của hạ tầng build/CI, chạy trên RabbitMQ.
- Bloomberg, Zalando và nhiều công ty khác cũng từng chia sẻ về việc dùng RabbitMQ ở các hội thảo, dù quy mô cụ thể thì ít khi được công bố.
- Và nhiều startup đến enterprise khác...
# kiến trúc bên trong rabbitmq
Nắm chắc phần này thì sau này cấu hình hay debug sẽ dễ thở hơn nhiều.
# các thành phần chính

# các thành phần
Producer (Publisher) Ứng dụng gửi message. Lưu ý Producer không gửi tin nhắn trực tiếp vào Queue, mà gửi qua một thành phần trung gian là Exchange. Đây là điểm khác biệt quan trọng của RabbitMQ so với nhiều message queue khác. (Khi bạn "gửi thẳng vào queue" bằng cách để exchange rỗng, thực ra message vẫn đi qua default exchange, loại direct, mà mọi queue tự động được bind theo đúng tên của nó.)
Exchange Có thể coi là bộ não định tuyến. Nó nhận message từ Producer và quyết định chuyển vào Queue nào dựa trên:
- Exchange Type (loại exchange: direct, topic, fanout, headers).
- Routing Key (nhãn đính kèm trên message).
- Binding Rules (quy tắc liên kết giữa Exchange và Queue).
Exchange giống một bưu điện: nó nhìn địa chỉ trên phong bì (routing key) để phân phối thư vào đúng hòm thư (queue).
Binding Là luật liên kết giữa Exchange và Queue. Một binding định nghĩa: "chuyển các message có routing key X vào queue Y".
Queue Nơi lưu message cho đến khi có Consumer lấy đi xử lý. Một Queue có các thuộc tính quan trọng:
- Durable: bản thân queue (định nghĩa của nó) còn nguyên sau khi broker khởi động lại. Với classic queue, muốn message cũng sống sót thì message phải được gửi ở chế độ persistent (
delivery_mode = 2); quorum queue thì luôn ghi message xuống đĩa. - Exclusive: chỉ kết nối hiện tại được dùng, và queue tự xóa khi kết nối đóng.
- Auto-delete: tự biến mất khi Consumer cuối cùng hủy đăng ký (unsubscribe). Queue chưa từng có consumer nào thì không bị xoá.
- TTL (Time-To-Live): đặt thời gian sống cho message, quá hạn thì message tự bị hủy.
Consumer Ứng dụng kết nối tới Queue để nhận và xử lý message. Có hai cơ chế nhận tin nhắn:
- Push (Basic.Consume): Broker chủ động đẩy message xuống Consumer khi có tin mới. Đây là cơ chế phổ biến nhất nhờ hiệu năng cao.
- Pull (Basic.Get): Consumer chủ động kéo message về. Ít dùng vì tốn tài nguyên và độ trễ cao.
# connection & channel

Connection: là một kết nối vật lý (TCP connection) từ ứng dụng tới RabbitMQ broker. Mở mới một Connection khá tốn tài nguyên (bắt tay TCP, xác thực, bắt tay TLS...), nên cần giữ kết nối bền vững thay vì tạo mới liên tục.
Channel: là một kết nối ảo (virtual connection) chạy trên một Connection vật lý. Tạo và đóng Channel rất nhanh và tốn ít tài nguyên. Mỗi thread xử lý trong code nên dùng một Channel riêng.
Kinh nghiệm thực tế: mỗi ứng dụng chỉ nên giữ ít connection, sống lâu, và mỗi thread dùng một Channel riêng. Đừng share Channel giữa các thread vì nó không được thiết kế để thread-safe. RabbitMQ còn khuyên tách riêng connection cho publish và consume: khi broker áp flow control lên phía publish (publisher bị chặn vì broker thiếu RAM/đĩa), consumer trên connection kia vẫn ack được để giải phóng tài nguyên. Spring AMQP làm sẵn việc này nếu bật
rabbitTemplate.setUsePublisherConnection(true).
# virtual host (vhost)
Vai trò giống database schema hay namespace, dùng để phân vùng tài nguyên. Mỗi vhost có Exchange, Queue và Binding hoàn toàn độc lập. Thường dùng vhost để cô lập các môi trường (dev, staging, prod) hoặc phân quyền giữa các team/dự án trên cùng một RabbitMQ Cluster.
RabbitMQ Broker
├── vhost: /production
│ ├── exchange: order.exchange
│ ├── queue: order.processing
│ └── queue: order.notification
├── vhost: /staging
│ ├── exchange: order.exchange
│ └── queue: order.processing
└── vhost: /development
└── ...# amqp protocol
AMQP 0-9-1 là giao thức wire-level mà phần lớn client RabbitMQ (Java client, Spring AMQP) đang dùng. Các khái niệm exchange, binding, channel, ack ở trên đều đến từ đây. Hiểu nó thì lúc gặp sự cố bạn debug và tối ưu dễ hơn hẳn.
# vòng đời của một message

# message properties (thuộc tính tin nhắn)
Mỗi message AMQP gồm hai phần: properties (metadata cấu hình) và body (payload chứa dữ liệu thực tế).
// Các properties quan trọng thường dùng:
MessageProperties properties = MessagePropertiesBuilder.newInstance()
.setContentType("application/json") // Định dạng dữ liệu của body
.setContentEncoding("UTF-8") // Bảng mã hóa ký tự
.setDeliveryMode(MessageDeliveryMode.PERSISTENT) // 1=tạm thời, 2=lưu đĩa (survive restart)
.setPriority(5) // Độ ưu tiên, chỉ có tác dụng khi queue khai báo x-max-priority
.setCorrelationId("abc-123") // Dùng để định danh request-response (RPC)
.setReplyTo("response.queue") // Queue nhận phản hồi
.setExpiration("60000") // TTL thời gian hết hạn (milliseconds)
.setMessageId(UUID.randomUUID().toString()) // ID duy nhất chống lặp tin
.setTimestamp(new Date()) // Thời gian tạo tin nhắn
.setType("OrderCreatedEvent") // Tên loại sự kiện
.setAppId("order-service") // ID ứng dụng gửi tin
.build();# acknowledgment: cơ chế xác nhận tin nhắn
Phần này rất dễ bị bỏ sót nhưng lại gây ra nhiều bug nghiêm trọng trên Production.
Auto Ack (autoAck=true):
- Message bị xóa khỏi Queue ngay khi vừa gửi đến Consumer.
- Consumer crash giữa chừng thì tin nhắn đó coi như mất vĩnh viễn.
- Chỉ nên dùng cho message không quan trọng như log hoặc metrics.
Manual Ack (autoAck=false):
- Consumer phải chủ động gửi ack về broker sau khi xử lý xong tin nhắn.
- Nếu Consumer gặp sự cố trước khi kịp gửi ack, RabbitMQ tự đẩy tin nhắn quay lại queue để Consumer khác (hoặc chính nó sau khi khởi động lại) xử lý tiếp.
- Đây là cấu hình nên dùng cho môi trường Production.
// Ba loại xác nhận cơ bản:
channel.basicAck(deliveryTag, false); // Xử lý thành công, cho phép xóa tin nhắn khỏi queue
channel.basicNack(deliveryTag, false, true); // Thất bại, yêu cầu đẩy lại vào queue (requeue) để thử lại
channel.basicReject(deliveryTag, false); // Thất bại, loại bỏ tin nhắn (hoặc chuyển sang DLX) không requeueCảnh báo (Gotcha): Quên gửi ack là một trong những lỗi phổ biến nhất trên RabbitMQ. Message nằm ở trạng thái "Unacked", broker không xoá được nên RAM/đĩa cứ phình ra. Khi số message unacked chạm prefetch, consumer đó cũng ngừng nhận message mới, nhìn từ ngoài giống như consumer bị treo. Từ RabbitMQ 3.8.15 có thêm
consumer_timeout(mặc định 30 phút): quá hạn mà chưa ack thì broker đóng channel với lỗiPRECONDITION_FAILEDvà trả các message đó về queue, nên lỗi này thường lộ ra dưới dạng channel bị đóng định kỳ và message bị giao lại. Mình từng mất cả buổi chiều để debug lỗi ngớ ngẩn này.
# exchange types: bộ não định tuyến
Đây có lẽ là tính năng linh hoạt và thú vị nhất của RabbitMQ so với các message queue khác. Có 4 loại Exchange chính, ứng với các bài toán định tuyến khác nhau.
# direct exchange
Định tuyến chính xác theo Routing Key. Message chỉ chuyển đến Queue có Binding Key khớp hoàn toàn (exact match) với Routing Key của tin nhắn.

Use case: Phân phối tác vụ (task distribution), điều hướng tin nhắn đến đúng các service chuyên biệt.
// Khai báo
channel.exchangeDeclare("order.direct", BuiltinExchangeType.DIRECT, true);
channel.queueDeclare("order-processing", true, false, false, null);
channel.queueBind("order-processing", "order.direct", "order.created");
// Gửi tin nhắn
channel.basicPublish("order.direct", "order.created", null, message.getBytes());# fanout exchange
Cơ chế Broadcast: gửi message tới tất cả các Queue đang liên kết, không quan tâm Routing Key.

Use case: Phát tán sự kiện (event broadcasting), gửi các thông báo cho nhiều service chạy bất đồng bộ cùng lúc.
// Khai báo
channel.exchangeDeclare("notification.fanout", BuiltinExchangeType.FANOUT, true);
channel.queueBind("email-queue", "notification.fanout", ""); // routing key bị bỏ qua
// Publish — routing key đặt trống
channel.basicPublish("notification.fanout", "", null, message.getBytes());# topic exchange
Định tuyến linh hoạt bằng cách khớp mẫu (pattern matching) Routing Key với các ký tự đại diện (wildcard). Các từ trong Routing Key cách nhau bằng dấu chấm (.), kết hợp với:
- Ký tự
*(star): Thay thế cho đúng 1 từ. - Ký tự
#(hash): Thay thế cho 0 hoặc nhiều từ.

Use case: định tuyến linh hoạt dựa trên nhiều tiêu chí lọc.
// Khai báo
channel.exchangeDeclare("events.topic", BuiltinExchangeType.TOPIC, true);
channel.queueBind("vn-orders", "events.topic", "order.*.vn");
channel.queueBind("all-orders", "events.topic", "order.#");
channel.queueBind("all-events", "events.topic", "#");
// Publish
channel.basicPublish("events.topic", "order.created.vn", null, message.getBytes());# headers exchange
Định tuyến dựa trên thuộc tính Headers của message thay vì Routing Key. Loại này ít dùng vì phải so khớp nhiều header nên chậm hơn các loại còn lại, nhưng hữu ích khi tiêu chí lọc không gói gọn được vào một routing key.
// Liên kết queue kèm điều kiện match header
Map<String, Object> headers = new HashMap<>();
headers.put("x-match", "all"); // "all" = tất cả điều kiện phải đúng (AND), "any" = chỉ cần 1 điều kiện (OR)
headers.put("format", "pdf");
headers.put("type", "report");
channel.queueBind("pdf-reports", "docs.headers", "", headers);
// Publish tin nhắn kèm header tương ứng
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
.headers(Map.of("format", "pdf", "type", "report"))
.build();
channel.basicPublish("docs.headers", "", props, message.getBytes());# so sánh nhanh
| Exchange | Cơ chế định tuyến | Tốc độ xử lý | Use Case thực tế |
|---|---|---|---|
| Direct | Khớp chính xác | Nhanh nhất | Điểm - Điểm, hàng đợi tác vụ |
| Fanout | Gửi hàng loạt | Rất nhanh | Phát tán sự kiện (Pub/Sub) |
| Topic | Khớp theo mẫu | Nhanh | Định tuyến linh hoạt, lọc sự kiện |
| Headers | Khớp theo Header | Chậm nhất | Bộ lọc định tuyến phức tạp |
Kinh nghiệm thực tế: khoảng 90% bài toán hàng ngày chỉ cần Direct và Topic Exchange. Fanout hữu ích khi cần broadcast (ví dụ gửi notification). Headers Exchange thì hầu như không phải đụng tới.
# spring boot + rabbitmq!
Để dễ hình dung, mình sẽ dựng một hệ thống xử lý đơn hàng (Order Processing) để xem RabbitMQ hoạt động thực tế ra sao.
# setup dự án
pom.xml, khai báo các dependency cần thiết:
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AMQP — RabbitMQ integration -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- JSON serialization -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Lombok — giảm boilerplate -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>application.yml, cấu hình kết nối:
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
# Timeout khi mở TCP connection tới broker
connection-timeout: 5000
# Listener configuration
listener:
simple:
acknowledge-mode: manual # QUAN TRỌNG: Manual ack cho production
prefetch: 10 # Số message tối đa chưa ack mà broker được đẩy tới mỗi consumer
concurrency: 3 # Min consumer threads
max-concurrency: 10 # Max consumer threads
# Retry của Spring chỉ chạy khi listener ném exception ra ngoài. OrderConsumer bên dưới tự bắt
# mọi exception rồi tự ack/nack, nên với code đó khối retry này không có tác dụng: chọn một trong hai cách.
retry:
enabled: true
initial-interval: 1000 # 1 giây
max-interval: 10000 # 10 giây
multiplier: 2.0 # Exponential backoff
max-attempts: 3
# Publisher confirms
publisher-confirm-type: correlated # Async publisher confirms
publisher-returns: true # Return unroutable messages# rabbitmq config
package com.example.order.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class RabbitMQConfig {
// ==================== Constants ====================
public static final String ORDER_EXCHANGE = "order.exchange";
public static final String ORDER_QUEUE = "order.processing.queue";
public static final String ORDER_ROUTING_KEY = "order.created";
public static final String NOTIFICATION_EXCHANGE = "notification.exchange";
public static final String EMAIL_QUEUE = "notification.email.queue";
public static final String SMS_QUEUE = "notification.sms.queue";
// Dead Letter
public static final String DLX_EXCHANGE = "dlx.exchange";
public static final String DLX_QUEUE = "dlx.queue";
public static final String DLX_ROUTING_KEY = "dlx.routing";
// ==================== Exchanges ====================
@Bean
public TopicExchange orderExchange() {
return ExchangeBuilder
.topicExchange(ORDER_EXCHANGE)
.durable(true)
.build();
}
@Bean
public FanoutExchange notificationExchange() {
return ExchangeBuilder
.fanoutExchange(NOTIFICATION_EXCHANGE)
.durable(true)
.build();
}
@Bean
public DirectExchange deadLetterExchange() {
return ExchangeBuilder
.directExchange(DLX_EXCHANGE)
.durable(true)
.build();
}
// ==================== Queues ====================
@Bean
public Queue orderQueue() {
return QueueBuilder
.durable(ORDER_QUEUE)
.withArgument("x-dead-letter-exchange", DLX_EXCHANGE)
.withArgument("x-dead-letter-routing-key", DLX_ROUTING_KEY)
.withArgument("x-message-ttl", 300000) // 5 phút TTL
.build();
}
@Bean
public Queue emailQueue() {
return QueueBuilder
.durable(EMAIL_QUEUE)
.withArgument("x-dead-letter-exchange", DLX_EXCHANGE)
.withArgument("x-dead-letter-routing-key", DLX_ROUTING_KEY)
.build();
}
@Bean
public Queue smsQueue() {
return QueueBuilder
.durable(SMS_QUEUE)
.withArgument("x-dead-letter-exchange", DLX_EXCHANGE)
.withArgument("x-dead-letter-routing-key", DLX_ROUTING_KEY)
.build();
}
@Bean
public Queue deadLetterQueue() {
return QueueBuilder
.durable(DLX_QUEUE)
.build();
}
// ==================== Bindings ====================
@Bean
public Binding orderBinding() {
return BindingBuilder
.bind(orderQueue())
.to(orderExchange())
.with("order.#"); // Match tất cả order events
}
@Bean
public Binding emailBinding() {
return BindingBuilder
.bind(emailQueue())
.to(notificationExchange());
}
@Bean
public Binding smsBinding() {
return BindingBuilder
.bind(smsQueue())
.to(notificationExchange());
}
@Bean
public Binding deadLetterBinding() {
return BindingBuilder
.bind(deadLetterQueue())
.to(deadLetterExchange())
.with(DLX_ROUTING_KEY);
}
// ==================== Message Converter ====================
@Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
// ==================== RabbitTemplate ====================
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMessageConverter(jsonMessageConverter());
// Publisher Confirms — biết chắc message đã đến Exchange
template.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
log.debug("Message delivered to exchange: {}", correlationData);
} else {
log.error("Message FAILED to deliver to exchange: {}, cause: {}",
correlationData, cause);
// TODO: Retry logic hoặc save to DB
}
});
// Publisher Returns — message không route được đến queue nào
template.setReturnsCallback(returned -> {
log.error("Message returned: exchange={}, routingKey={}, replyCode={}, replyText={}",
returned.getExchange(),
returned.getRoutingKey(),
returned.getReplyCode(),
returned.getReplyText());
});
template.setMandatory(true); // Enable returns cho unroutable messages
return template;
}
// ==================== Listener Container Factory ====================
// Tự khai báo bean tên "rabbitListenerContainerFactory" thì Spring Boot sẽ KHÔNG tạo factory mặc định nữa.
// Dùng configurer để vẫn áp các giá trị spring.rabbitmq.listener.simple.* trong application.yml
// (acknowledge-mode, prefetch, concurrency, retry...), nếu không chúng bị bỏ qua.
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
SimpleRabbitListenerContainerFactoryConfigurer configurer,
ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
configurer.configure(factory, connectionFactory);
factory.setMessageConverter(jsonMessageConverter());
return factory;
}
}Vài điểm đáng để ý trong config này. x-message-ttl 5 phút trên order.processing.queue nghĩa là đơn nào nằm chờ quá 5 phút sẽ bị chuyển sang DLX, nên chọn con số này theo SLA thật chứ đừng copy. setMandatory(true) mới làm cho ReturnsCallback được gọi khi message không route được tới queue nào; thiếu nó thì broker lặng lẽ bỏ message. Còn nếu bạn đã lên Spring Boot 4 / Spring AMQP 4.x (dùng Jackson 3), Jackson2JsonMessageConverter đã bị đánh dấu deprecated, lớp thay thế là JacksonJsonMessageConverter.
# domain models
package com.example.order.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderEvent implements Serializable {
private String orderId;
private String customerId;
private String customerEmail;
private String customerPhone;
private List<OrderItem> items;
private BigDecimal totalAmount;
private String currency;
private OrderStatus status;
private LocalDateTime createdAt;
private String eventType; // "created", "paid", "shipped", "cancelled"
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class OrderItem implements Serializable {
private String productId;
private String productName;
private int quantity;
private BigDecimal price;
}
public enum OrderStatus {
PENDING, CONFIRMED, PAID, SHIPPED, DELIVERED, CANCELLED
}
}# producer: gửi message
package com.example.order.producer;
import com.example.order.config.RabbitMQConfig;
import com.example.order.model.OrderEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderProducer {
private final RabbitTemplate rabbitTemplate;
/**
* Publish order event đến Topic Exchange.
* Routing key format: order.{eventType}
* Ví dụ: order.created, order.paid, order.shipped
*/
public void publishOrderEvent(OrderEvent event) {
String routingKey = "order." + event.getEventType();
String correlationId = UUID.randomUUID().toString();
log.info("Publishing order event: orderId={}, type={}, correlationId={}",
event.getOrderId(), event.getEventType(), correlationId);
CorrelationData correlationData = new CorrelationData(correlationId);
rabbitTemplate.convertAndSend(
RabbitMQConfig.ORDER_EXCHANGE,
routingKey,
event,
message -> {
// Custom message properties
message.getMessageProperties().setMessageId(UUID.randomUUID().toString());
message.getMessageProperties().setCorrelationId(correlationId);
message.getMessageProperties().setHeader("source", "order-service");
message.getMessageProperties().setHeader("version", "1.0");
return message;
},
correlationData
);
log.info("Order event published successfully: orderId={}", event.getOrderId());
}
/**
* Broadcast notification đến tất cả notification consumers.
* Dùng Fanout Exchange — tất cả queue bound sẽ nhận message.
*/
public void broadcastNotification(OrderEvent event) {
log.info("Broadcasting notification for order: {}", event.getOrderId());
rabbitTemplate.convertAndSend(
RabbitMQConfig.NOTIFICATION_EXCHANGE,
"", // Fanout exchange bỏ qua routing key
event
);
}
}# consumer: nhận và xử lý message
package com.example.order.consumer;
import com.example.order.model.OrderEvent;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
public class OrderConsumer {
/**
* Consumer chính xử lý order events.
*
* Manual Ack pattern:
* 1. Nhận message
* 2. Xử lý business logic
* 3. Ack nếu thành công, Nack nếu thất bại
*
* QUAN TRỌNG: mọi nhánh (kể cả nhánh lỗi) đều phải ack/nack/reject, nếu không message bị stuck ở trạng thái unacked
*/
@RabbitListener(
queues = "order.processing.queue",
containerFactory = "rabbitListenerContainerFactory"
)
public void handleOrderEvent(
@Payload OrderEvent event,
@Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,
@Header(AmqpHeaders.REDELIVERED) boolean redelivered,
Channel channel) throws IOException {
log.info("Received order event: orderId={}, type={}, redelivered={}",
event.getOrderId(), event.getEventType(), redelivered);
try {
// ========== Business Logic ==========
switch (event.getEventType()) {
case "created":
processNewOrder(event);
break;
case "paid":
processPayment(event);
break;
case "shipped":
processShipment(event);
break;
case "cancelled":
processCancellation(event);
break;
default:
log.warn("Unknown event type: {}", event.getEventType());
}
// ========== ACK — Xử lý thành công ==========
channel.basicAck(deliveryTag, false);
log.info("Order event processed successfully: orderId={}", event.getOrderId());
} catch (BusinessException e) {
// Business error — không cần retry, gửi đến DLX
log.error("Business error processing order: {}", event.getOrderId(), e);
channel.basicReject(deliveryTag, false); // false = không requeue → đến DLX
} catch (TransientException e) {
// Transient error (DB timeout, network issue) — có thể retry
log.warn("Transient error processing order: {}, redelivered: {}",
event.getOrderId(), redelivered);
if (redelivered) {
// Đã retry rồi mà vẫn fail → gửi đến DLX
channel.basicReject(deliveryTag, false);
log.error("Message sent to DLX after retry: orderId={}", event.getOrderId());
} else {
// Retry lần đầu — requeue
channel.basicNack(deliveryTag, false, true); // true = requeue
}
} catch (Exception e) {
// Unexpected error
log.error("Unexpected error processing order: {}", event.getOrderId(), e);
channel.basicReject(deliveryTag, false); // Gửi đến DLX
}
}
private void processNewOrder(OrderEvent event) {
log.info("Processing new order: {} - Amount: {} {}",
event.getOrderId(), event.getTotalAmount(), event.getCurrency());
// Validate inventory, reserve stock, create order record...
}
private void processPayment(OrderEvent event) {
log.info("Processing payment for order: {}", event.getOrderId());
// Update order status, trigger fulfillment...
}
private void processShipment(OrderEvent event) {
log.info("Processing shipment for order: {}", event.getOrderId());
// Update tracking info, notify customer...
}
private void processCancellation(OrderEvent event) {
log.info("Processing cancellation for order: {}", event.getOrderId());
// Release inventory, process refund...
}
}Cách dùng cờ redelivered ở trên khá thô, bạn nên biết giới hạn của nó. Message bị basicNack(..., true) sẽ quay lại queue và thường được giao lại gần như ngay lập tức, không có khoảng nghỉ nào, nên lỗi tạm thời kiểu DB đang quá tải chưa chắc kịp hồi. Cờ này cũng bật lên khi consumer chết giữa chừng, chứ không chỉ khi bị nack, và nó không cho biết đã giao lại bao nhiêu lần. Muốn retry có độ trễ và đếm số lần, xem mục DLX và delay queue ở dưới.
# notification consumers
package com.example.order.consumer;
import com.example.order.model.OrderEvent;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
public class NotificationConsumer {
/**
* Email notification consumer.
* Nhận từ Fanout Exchange — mọi order event đều trigger email.
*/
@RabbitListener(queues = "notification.email.queue")
public void handleEmailNotification(
@Payload OrderEvent event,
@Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,
Channel channel) throws IOException {
try {
log.info("Sending email notification for order: {} to {}",
event.getOrderId(), event.getCustomerEmail());
// Gửi email logic...
// emailService.sendOrderConfirmation(event);
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
log.error("Failed to send email for order: {}", event.getOrderId(), e);
channel.basicNack(deliveryTag, false, true); // Retry
}
}
/**
* SMS notification consumer.
* Cũng nhận từ Fanout Exchange — cùng message, khác channel.
*/
@RabbitListener(queues = "notification.sms.queue")
public void handleSmsNotification(
@Payload OrderEvent event,
@Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,
Channel channel) throws IOException {
try {
log.info("Sending SMS notification for order: {} to {}",
event.getOrderId(), event.getCustomerPhone());
// Gửi SMS logic...
// smsService.sendOrderSms(event);
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
log.error("Failed to send SMS for order: {}", event.getOrderId(), e);
channel.basicNack(deliveryTag, false, true);
}
}
}Hai consumer này basicNack(..., true) mọi lỗi, nghĩa là một message "độc" (payload hỏng, email sai định dạng) sẽ bị requeue rồi fail mãi, ăn CPU và chặn các message phía sau. Với classic queue thì vòng lặp này không bao giờ tự dừng. Quorum queue từ RabbitMQ 4.0 có delivery-limit mặc định là 20: quá 20 lần giao lại thì message bị bỏ hoặc chuyển sang DLX nếu có cấu hình. Dù vậy, tốt nhất vẫn nên phân loại lỗi như OrderConsumer thay vì requeue vô điều kiện.
# rest controller: trigger point
package com.example.order.controller;
import com.example.order.model.OrderEvent;
import com.example.order.producer.OrderProducer;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderProducer orderProducer;
@PostMapping
public ResponseEntity<Map<String, String>> createOrder(@RequestBody OrderEvent orderRequest) {
// Generate order ID
String orderId = "ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
orderRequest.setOrderId(orderId);
orderRequest.setStatus(OrderEvent.OrderStatus.PENDING);
orderRequest.setCreatedAt(LocalDateTime.now());
orderRequest.setEventType("created");
// Publish to order processing queue
orderProducer.publishOrderEvent(orderRequest);
// Broadcast notification
orderProducer.broadcastNotification(orderRequest);
// Response ngay lập tức — không chờ processing
return ResponseEntity.accepted().body(Map.of(
"orderId", orderId,
"status", "ACCEPTED",
"message", "Order is being processed"
));
}
/**
* Demo endpoint để test các loại event
*/
@PostMapping("/{orderId}/events/{eventType}")
public ResponseEntity<Map<String, String>> publishEvent(
@PathVariable String orderId,
@PathVariable String eventType) {
OrderEvent event = OrderEvent.builder()
.orderId(orderId)
.eventType(eventType)
.createdAt(LocalDateTime.now())
.build();
orderProducer.publishOrderEvent(event);
return ResponseEntity.accepted().body(Map.of(
"orderId", orderId,
"eventType", eventType,
"status", "EVENT_PUBLISHED"
));
}
}# chạy thử
# 1. Start RabbitMQ bằng Docker
docker run -d --name rabbitmq \
-p 5672:5672 \
-p 15672:15672 \
rabbitmq:4-management
# Management UI: http://localhost:15672 (guest/guest)
# 2. Start Spring Boot app
./mvnw spring-boot:run
# 3. Tạo order
curl -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": "CUST-001",
"customerEmail": "user@example.com",
"customerPhone": "+84123456789",
"items": [
{
"productId": "PROD-001",
"productName": "Mechanical Keyboard",
"quantity": 1,
"price": 150.00
}
],
"totalAmount": 150.00,
"currency": "USD"
}'
# Response (ngay lập tức, < 10ms):
# {
# "orderId": "ORD-A1B2C3D4",
# "status": "ACCEPTED",
# "message": "Order is being processed"
# }
# 4. Publish event
curl -X POST http://localhost:8080/api/orders/ORD-A1B2C3D4/events/paid
# 5. Xem logs — bạn sẽ thấy:
# OrderConsumer: Received order event: orderId=ORD-A1B2C3D4, type=created
# NotificationConsumer: Sending email notification for order: ORD-A1B2C3D4
# NotificationConsumer: Sending SMS notification for order: ORD-A1B2C3D4# reliability patterns
Lên Production thật thì không đơn giản như vậy. Mất tin nhắn là mất đơn hàng, khách phàn nàn, doanh nghiệp chịu thiệt. Dưới đây là các kỹ thuật giúp message đi tới nơi an toàn.
# publisher confirms (xác nhận từ phía người gửi)
Khi bật publisher confirms, broker sẽ gửi lại ack cho từng message sau khi đã nhận trách nhiệm với nó: với message persistent vào queue durable, nghĩa là đã ghi xuống đĩa (quorum queue thì đã được đa số node xác nhận). Nếu broker không xử lý được, producer nhận nack.
Có một chỗ hay bị hiểu nhầm: message không route được tới queue nào vẫn được ack, vì broker đã "xử lý xong" nó theo cách là bỏ đi. Vì vậy phải bật thêm mandatory + returns như config ở trên thì mới biết message bị rơi.
+----------+ publish --> +----------+
| Producer | | Exchange |
+----------+ <-- confirm +----------+Confirm chỉ giải quyết được nửa bài toán. Hãy nghĩ tới trường hợp: lưu đơn hàng vào DB thành công rồi app chết trước khi publish, hoặc publish xong thì transaction DB lại rollback. Hai hệ thống (DB và broker) không chung một transaction, nên sớm muộn sẽ lệch nhau. Transactional Outbox Pattern xử lý chuyện này bằng cách ghi event vào một bảng outbox trong cùng transaction với dữ liệu nghiệp vụ, rồi một tiến trình khác đọc bảng đó và publish (yêu cầu publisher-confirm-type: correlated như config ở trên):
@Slf4j
@Service
@RequiredArgsConstructor
public class ReliableProducer {
private final RabbitTemplate rabbitTemplate;
private final OrderOutboxRepository outboxRepository; // Outbox pattern
private final ObjectMapper objectMapper;
/**
* Transactional Outbox Pattern:
* 1. Save business data + bản ghi outbox trong CÙNG một DB transaction
* 2. KHÔNG publish ngay trong transaction: nếu publish xong mà transaction rollback,
* consumer sẽ nhận event của một đơn hàng không hề tồn tại
* 3. Một relay (scheduler bên dưới, hoặc CDC như Debezium) đọc outbox rồi publish
* 4. Chỉ mark SENT khi broker đã confirm
*/
@Transactional
public void saveWithOutbox(OrderEvent event) throws JsonProcessingException {
// orderRepository.save(...); // business data, cùng transaction
OrderOutbox outbox = OrderOutbox.builder()
.messageId(UUID.randomUUID().toString())
.exchange(RabbitMQConfig.ORDER_EXCHANGE)
.routingKey("order." + event.getEventType())
.payload(objectMapper.writeValueAsString(event))
.status(OutboxStatus.PENDING)
.retryCount(0)
.createdAt(LocalDateTime.now())
.build();
outboxRepository.save(outbox);
}
/**
* Relay: publish các bản ghi PENDING, chạy mỗi 5 giây.
* Chạy nhiều instance thì phải khoá bản ghi (SELECT ... FOR UPDATE SKIP LOCKED, ShedLock...)
* để hai instance không cùng gửi một message.
*/
@Scheduled(fixedDelay = 5000)
public void relayPendingMessages() {
List<OrderOutbox> pendingMessages = outboxRepository
.findTop100ByStatusOrderByCreatedAtAsc(OutboxStatus.PENDING);
for (OrderOutbox outbox : pendingMessages) {
CorrelationData correlationData = new CorrelationData(outbox.getMessageId());
try {
rabbitTemplate.convertAndSend(
outbox.getExchange(),
outbox.getRoutingKey(),
objectMapper.readValue(outbox.getPayload(), OrderEvent.class),
message -> {
// Giữ nguyên messageId qua mọi lần gửi lại để consumer khử trùng được
message.getMessageProperties().setMessageId(outbox.getMessageId());
return message;
},
correlationData
);
// Chờ publisher confirm (cần publisher-confirm-type: correlated)
CorrelationData.Confirm confirm = correlationData.getFuture().get(10, TimeUnit.SECONDS);
if (confirm.isAck() && correlationData.getReturned() == null) {
outbox.setStatus(OutboxStatus.SENT);
} else {
// nack từ broker, hoặc message không route được tới queue nào
outbox.setRetryCount(outbox.getRetryCount() + 1);
}
} catch (Exception e) {
log.warn("Publish failed: {}", outbox.getMessageId(), e);
outbox.setRetryCount(outbox.getRetryCount() + 1);
}
if (outbox.getStatus() != OutboxStatus.SENT && outbox.getRetryCount() >= 5) {
outbox.setStatus(OutboxStatus.FAILED);
log.error("Message permanently failed after 5 retries: {}",
outbox.getMessageId());
}
outboxRepository.save(outbox);
}
}
}Để ý là outbox cho bạn at-least-once chứ không phải exactly-once: nếu broker đã nhận message nhưng confirm bị mất (timeout, rớt mạng), relay sẽ gửi lại lần nữa. Đó là lý do messageId được giữ cố định qua các lần gửi, và consumer phải xử lý được message trùng.
# consumer idempotency (tránh xử lý trùng lặp)
Với at-least-once delivery (giao ít nhất một lần), message hoàn toàn có thể đến hai lần: consumer xử lý xong nhưng mất kết nối trước khi kịp ack, producer gửi lại vì không nhận được confirm... Vì vậy consumer phải idempotent, tức là nhận cùng một message nhiều lần thì kết quả cuối cùng vẫn như nhận một lần.
@Slf4j
@Component
@RequiredArgsConstructor
public class IdempotentOrderConsumer {
private final ProcessedMessageRepository processedMessageRepo;
private final OrderService orderService;
@RabbitListener(queues = "order.processing.queue")
public void handleOrderEvent(
@Payload OrderEvent event,
@Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,
@Header(value = AmqpHeaders.MESSAGE_ID, required = false) String messageId,
Channel channel) throws IOException {
// Idempotency check — đã xử lý message này chưa?
if (messageId != null && processedMessageRepo.existsByMessageId(messageId)) {
log.warn("Duplicate message detected, skipping: messageId={}", messageId);
channel.basicAck(deliveryTag, false); // Ack để không nhận lại
return;
}
try {
// Process order
orderService.processOrder(event);
// Mark as processed
if (messageId != null) {
processedMessageRepo.save(new ProcessedMessage(
messageId,
event.getOrderId(),
LocalDateTime.now()
));
}
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
log.error("Error processing order: {}", event.getOrderId(), e);
channel.basicReject(deliveryTag, false);
}
}
}Đoạn code trên dễ hiểu nhưng còn hai kẽ hở. Thứ nhất, "kiểm tra rồi mới làm" không nguyên tử: hai consumer nhận cùng một message gần như đồng thời đều thấy existsByMessageId trả về false và cùng xử lý. Thứ hai, nếu app chết sau processOrder nhưng trước khi lưu ProcessedMessage, lần giao lại sẽ xử lý thêm lần nữa. Cách chắc hơn là đặt unique constraint cho messageId, rồi insert bản ghi ProcessedMessage và cập nhật dữ liệu nghiệp vụ trong cùng một DB transaction: message trùng sẽ vấp lỗi duplicate key, bạn bắt lỗi đó và ack bỏ qua. Ở nhiều nghiệp vụ, có thể làm chính thao tác đó idempotent luôn (vd UPDATE ... SET status = 'PAID' WHERE order_id = ? AND status = 'PENDING').
# tổng hợp reliability flow

# dead letter exchange: xử lý message thất bại
Dead Letter Exchange (DLX) giống "phòng cấp cứu", có khi là "nghĩa trang" của message. Message nào không xử lý được, thay vì biến mất không dấu vết hoặc làm tắc queue chính, sẽ được chuyển sang DLX để lưu lại, phân tích hoặc xử lý tay sau. Về kỹ thuật, DLX chỉ là một exchange bình thường; queue nào khai báo x-dead-letter-exchange thì broker sẽ tự publish các message "chết" của queue đó sang exchange này, kèm header x-death ghi lại lý do.
# khi nào message trở thành "dead letter"?
Có 4 trường hợp khiến một message bị chuyển sang DLX:
- Consumer reject hoặc nack message với tham số
requeue=false. - Hết hạn (TTL expired): message nằm trong queue quá lâu mà không ai consume.
- Queue đầy: queue chạm giới hạn
x-max-lengthhoặcx-max-length-bytes, khi đó (với chế độ overflow mặc địnhdrop-head) message cũ nhất ở đầu queue bị đẩy ra. - Vượt delivery limit (chỉ quorum queue): message bị giao lại quá số lần cho phép (
delivery-limit, mặc định 20 từ RabbitMQ 4.0).
# sơ đồ luồng dlx
+-------------+
| Main Queue |
| (order.q) |
+------+------+
|
Consumer reject
hoac TTL expired
|
v
+-------------+
| DLX Exchange|
+------+------+
|
v
+-------------+
| DLX Queue |--> DLX Consumer
| (parking) | (analyze, retry, alert)
+-------------+# dlx consumer: phân tích và xử lý sự cố
@Slf4j
@Component
@RequiredArgsConstructor
public class DeadLetterConsumer {
// Header tự quản lý để đếm số lần retry (xem giải thích bên dưới)
private static final String RETRY_HEADER = "x-retry-count";
private final RabbitTemplate rabbitTemplate;
private final AlertService alertService;
/**
* Consumer cho Dead Letter Queue.
* Phân tích nguyên nhân fail và quyết định:
* 1. Retry (gửi lại main queue)
* 2. Alert (thông báo team)
* 3. Archive (lưu lại để debug)
*/
@RabbitListener(queues = "dlx.queue")
public void handleDeadLetter(
org.springframework.amqp.core.Message message,
@Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,
Channel channel) throws IOException {
// Lấy thông tin về nguyên nhân dead letter
Map<String, Object> headers = message.getMessageProperties().getHeaders();
List<Map<String, Object>> xDeath = (List<Map<String, Object>>) headers.get("x-death");
if (xDeath != null && !xDeath.isEmpty()) {
Map<String, Object> deathInfo = xDeath.get(0); // Lần dead letter gần nhất
String reason = (String) deathInfo.get("reason"); // "rejected", "expired", "maxlen", "delivery_limit"
String queue = (String) deathInfo.get("queue"); // Queue gốc
String exchange = (String) deathInfo.get("exchange"); // Exchange gốc
// Routing key gốc. Không dùng receivedRoutingKey: nó đã bị thay bằng x-dead-letter-routing-key ("dlx.routing")
List<String> routingKeys = (List<String>) deathInfo.get("routing-keys");
String originalRoutingKey = routingKeys.get(0);
// Từ RabbitMQ 4.0, broker không đọc x-death do client gửi kèm khi publish lại,
// nên "count" trong x-death không tăng qua các vòng retry kiểu này. Tự đếm bằng header riêng.
int retryCount = ((Number) headers.getOrDefault(RETRY_HEADER, 0)).intValue();
log.error("Dead letter received: reason={}, queue={}, retryCount={}, exchange={}",
reason, queue, retryCount, exchange);
// Retry logic dựa trên số lần fail
if (retryCount < 3) {
// Retry — gửi lại queue gốc sau delay
log.info("Retrying message (attempt {}): queue={}", retryCount + 1, queue);
// Delay trước khi retry (exponential backoff).
// Lưu ý: sleep chặn luôn thread của listener, xem cách tốt hơn ở mục delay queue bên dưới.
try {
Thread.sleep(Math.min(1000 * (long) Math.pow(2, retryCount), 30000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
message.getMessageProperties().setHeader(RETRY_HEADER, retryCount + 1);
rabbitTemplate.send(exchange, originalRoutingKey, message);
} else {
// Đã retry quá nhiều — alert và archive
log.error("Message permanently failed after {} retries. Alerting team.", retryCount);
alertService.sendAlert(
"Dead Letter Alert",
String.format("Message failed %d times in queue %s. Manual intervention needed.",
retryCount, queue)
);
// Archive to database for later analysis
// deadLetterRepository.save(...)
}
}
channel.basicAck(deliveryTag, false);
}
}Code này đủ để minh hoạ cách đọc x-death, nhưng có hai chỗ cần sửa nếu mang lên production. Thread.sleep giữ chặt một listener thread trong lúc chờ, vài chục message lỗi cùng lúc là DLQ đứng hình. Và message được ack ở cuối dù rabbitTemplate.send có thất bại hay không, nên một lần gửi lại hỏng là mất message; an toàn hơn là chỉ ack khi đã gửi lại (hoặc đã lưu archive) thành công.
# thử lại với delay queue pattern (tránh nghẽn hàng loạt)
Khi có lỗi tạm thời (ví dụ DB timeout), retry ngay lập tức nhiều khi chỉ làm hệ thống quá tải thêm, kiểu cả đám cùng xông vào một lúc (thundering herd). Cách gọn hơn là để broker tự giữ message một lúc trong một queue trung gian có TTL, hết hạn thì nó tự quay về queue chính, không thread nào phải ngồi chờ:
@Configuration
public class RetryQueueConfig {
// Retry queue với TTL — message tự động quay lại main queue sau delay
@Bean
public Queue retryQueue() {
return QueueBuilder
.durable("order.retry.queue")
.withArgument("x-dead-letter-exchange", RabbitMQConfig.ORDER_EXCHANGE)
.withArgument("x-dead-letter-routing-key", "order.created")
.withArgument("x-message-ttl", 5000) // 5 giây delay
.build();
}
// Binding retry queue đến DLX exchange.
// Queue chính phải dead-letter với đúng routing key này (x-dead-letter-routing-key = "retry.order"),
// nếu vẫn để "dlx.routing" như RabbitMQConfig ở trên thì message rơi vào dlx.queue, không bao giờ tới retry queue.
@Bean
public Binding retryBinding() {
return BindingBuilder
.bind(retryQueue())
.to(new DirectExchange(RabbitMQConfig.DLX_EXCHANGE))
.with("retry.order");
}
}Flow: Main Queue -> reject -> DLX -> Retry Queue (wait 5s) -> TTL expire -> Main Queue
|
v
Consumer retryVài lưu ý khi dùng pattern này. TTL của retry queue là cố định, muốn backoff tăng dần (5s, 30s, 5 phút) thì tạo vài retry queue với TTL khác nhau và chọn queue theo số lần đã thử. Vòng main -> retry -> main sẽ lặp mãi nếu lỗi không bao giờ hết, nên consumer cần đọc x-death (lần này do chính broker dead-letter nên trường count tăng đúng) để biết đã thử bao nhiêu lần và đẩy message sang parking queue khi vượt ngưỡng. Ngoài ra RabbitMQ chỉ hết hạn message ở đầu queue, nên mọi message trong một retry queue cần cùng TTL (đặt ở queue như trên), đừng đặt TTL khác nhau cho từng message. Nếu cần delay linh hoạt từng message, có plugin rabbitmq_delayed_message_exchange, nhưng plugin này có giới hạn riêng về độ bền dữ liệu, nên đọc kỹ README trước khi dùng.
# tối ưu hóa hiệu năng & best practices
# prefetch count (sweet spot)
Prefetch Count quyết định số message tối đa mà RabbitMQ được đẩy xuống một Consumer khi Consumer đó chưa ack. Prefetch thấp thì broker phải chờ ack rồi mới gửi tiếp, consumer hay ngồi không; prefetch cao thì message dồn sẵn ở consumer, nhanh hơn nhưng một consumer chậm sẽ ôm nhiều việc trong khi consumer khác rảnh, và những message đó chỉ được chia lại khi consumer chết.
prefetch = 1: Chậm nhưng phân phối đều (fair distribution)
Mỗi consumer chỉ nhận 1 message tại một thời điểm.
Thích hợp khi thời gian xử lý giữa các message chênh lệch lớn.
prefetch = 10-50: Cân bằng
Throughput khá và vẫn chia tải tương đối đều.
Điểm khởi đầu hợp lý khi mỗi message mất vài ms tới vài trăm ms.
prefetch = 100-300+: High throughput
Consumer dễ bị ngợp nếu gặp message nặng.
Hợp khi xử lý rất nhanh và đều. (250 là mặc định của Spring AMQP.)
# application.yml
spring:
rabbitmq:
listener:
simple:
prefetch: 25 # Sweet spot cho hầu hết use caseCác con số trên chỉ là điểm xuất phát. Cách đúng là đo: tăng dần prefetch tới khi throughput không tăng nữa, rồi để ý RAM của consumer. Lưu ý Spring AMQP mặc định prefetch là 250, nên nếu không cấu hình gì thì bạn đang ở nhóm "high throughput" mà không biết.
# quản lý connection và channel hiệu quả
// SAI — Tạo connection mỗi lần publish
public void badPublish(String message) {
ConnectionFactory factory = new ConnectionFactory();
Connection connection = factory.newConnection(); // Tốn kém!
Channel channel = connection.createChannel(); // Mỗi lần!
channel.basicPublish(...);
channel.close();
connection.close();
}
// ĐÚNG — Spring Boot dùng CachingConnectionFactory
// Mặc định: một connection dùng chung + cache channel, RabbitTemplate tự reuse
@Autowired
private RabbitTemplate rabbitTemplate;
public void goodPublish(OrderEvent event) {
rabbitTemplate.convertAndSend("exchange", "routing.key", event);
// Connection được dùng chung, channel lấy từ cache rồi trả lại
}# message size (kích thước tin nhắn)
Rule of thumb:
- < 128KB: Lý tưởng
- 128KB - 1MB: OK nhưng cân nhắc
- > 1MB: KHÔNG NÊN — dùng Claim Check pattern
Claim Check Pattern:
1. Upload large payload lên S3/MinIO
2. Gửi message chỉ chứa reference (URL/key)
3. Consumer download payload từ S3/MinIO
// Claim Check Pattern
public class ClaimCheckProducer {
private final RabbitTemplate rabbitTemplate;
private final S3Client s3Client;
public void publishLargePayload(byte[] largePayload, String orderId) {
// 1. Upload to S3
String s3Key = "messages/" + orderId + "/" + UUID.randomUUID();
s3Client.putObject(PutObjectRequest.builder()
.bucket("message-payloads")
.key(s3Key)
.build(), RequestBody.fromBytes(largePayload));
// 2. Publish reference only
Map<String, String> claimCheck = Map.of(
"orderId", orderId,
"payloadRef", s3Key,
"payloadSize", String.valueOf(largePayload.length)
);
rabbitTemplate.convertAndSend("order.exchange", "order.created", claimCheck);
}
}Message lớn làm mọi thứ chậm theo: tốn RAM broker, chiếm băng thông giữa các node trong cluster, và một message to có thể chặn các message nhỏ đi sau trên cùng connection. Broker cũng có trần cứng: max_message_size mặc định là 16 MiB từ RabbitMQ 4.0 (trước đó 128 MiB), vượt quá là channel bị đóng.
# batch publishing (gửi tin nhắn hàng loạt)
// Batch publish cho high-throughput scenarios
public void batchPublish(List<OrderEvent> events) {
rabbitTemplate.invoke(operations -> {
for (OrderEvent event : events) {
operations.convertAndSend(
RabbitMQConfig.ORDER_EXCHANGE,
"order." + event.getEventType(),
event
);
}
// Tất cả message được gửi trong cùng một channel operation
// Giảm overhead so với gửi từng message
return null;
});
}invoke không gom message thành một gói trên đường truyền; nó chỉ giữ một channel cho cả vòng lặp thay vì mượn và trả channel cho từng lần gửi. Lợi ích lớn nhất là bạn có thể chờ confirm một lần cho cả lô bằng operations.waitForConfirmsOrDie(timeout) ở cuối lambda (cần publisher-confirm-type: simple). Nếu thật sự muốn gộp nhiều message thành một message AMQP thì Spring AMQP có BatchingRabbitTemplate, đổi lại consumer phải biết tách batch.
# giám sát chỉ số (monitoring metrics)
# application.yml — Enable RabbitMQ metrics với Micrometer
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
tags:
application: order-service
# Metrics quan trọng cần monitor:
# - rabbitmq.consumed: Số message consumed
# - rabbitmq.published: Số message published
# - rabbitmq.acknowledged: Số message acked
# - rabbitmq.rejected: Số message rejected
# - rabbitmq.channels: Số channels active
# - spring.rabbitmq.listener: Listener container metricsCác metric rabbitmq.* ở trên là số liệu phía client (app của bạn), cần thêm spring-boot-starter-actuator và micrometer-registry-prometheus thì endpoint /actuator/prometheus mới có. Số liệu phía broker như độ dài queue, số message unacked, RAM/đĩa của từng node thì lấy từ plugin rabbitmq_prometheus có sẵn trong RabbitMQ (mặc định ở port 15692), rồi dựng dashboard Grafana từ đó.
# tóm tắt best practices checklist
Message Design:
✅ Dùng JSON serialization (Jackson2JsonMessageConverter)
✅ Luôn set messageId cho idempotency
✅ Luôn set correlationId cho tracing
✅ Giữ message size < 128KB
✅ Version message schema (header "version": "1.0")
Queue Design:
✅ Durable queues cho production
✅ Persistent messages (delivery_mode = 2)
✅ Set TTL để tránh queue grow vô hạn
✅ Set max-length để protect memory
✅ Luôn configure DLX
Consumer Design:
✅ Manual acknowledgment
✅ Idempotent processing
✅ Proper error handling (business vs transient errors)
✅ Prefetch count phù hợp
✅ Graceful shutdown
Infrastructure:
✅ Cluster mode cho HA (ít nhất 3 nodes)
✅ Quorum queues cho dữ liệu cần HA (classic mirrored queues đã bị gỡ bỏ từ RabbitMQ 4.0)
✅ Monitoring với Prometheus + Grafana
✅ Alerting cho queue depth, consumer count, memory usage
# rabbitmq vs kafka: khi nào nên dùng gì?
Câu hỏi kinh điển mà dev nào cũng từng đặt ra. Ngắn gọn thì hai hệ thống này sinh ra cho những bài toán khác nhau.
# khác biệt về mặt kiến trúc

# so sánh chi tiết các tiêu chí
| Tiêu chí | RabbitMQ | Kafka |
|---|---|---|
| Model | Message Queue (push) | Event Log (pull) |
| Routing | Flexible (exchange types) | Topic + partition |
| Message sau consume | Xóa khỏi queue | Giữ lại (retention) |
| Replay | Không với queue thường | Có (reset offset) |
| Ordering | Per-queue guarantee | Per-partition guarantee |
| Throughput | Hàng chục nghìn msg/s/queue | Hàng trăm nghìn tới triệu msg/s |
| Latency | Rất thấp (< 1ms) | Thấp (< 10ms) |
| Consumer Groups | Competing consumers | Consumer groups native |
| Protocol | AMQP, MQTT, STOMP | Custom binary protocol |
| Use case chính | Task queue, RPC, routing | Event streaming, log aggregation |
Con số throughput và latency trong bảng chỉ là bậc độ lớn để so sánh, thực tế thay đổi rất nhiều theo phần cứng, kích thước message và cấu hình durability. "Ordering per-queue" cũng chỉ đúng khi một queue có một consumer; nhiều consumer hoặc có requeue thì thứ tự xử lý không còn được đảm bảo. Ngoài ra từ bản 3.9, RabbitMQ có thêm Streams: một kiểu queue dạng log append-only, giữ message sau khi đọc và cho phép đọc lại từ một offset, khá giống Kafka. Nếu bạn đã chạy RabbitMQ và chỉ cần replay ở mức vừa phải thì Streams đáng cân nhắc trước khi dựng thêm Kafka.
# khi nào nên chọn rabbitmq?
✅ Task queue — distribute work giữa workers
✅ RPC pattern — request/reply
✅ Complex routing — route message dựa trên nhiều criteria
✅ Priority queue — xử lý message quan trọng trước
✅ Message TTL — message tự expire
✅ Khi cần flexible acknowledgment
✅ Khi message không cần replay
✅ Khi team quen với traditional messaging
# khi nào nên chọn kafka?
✅ Event sourcing — lưu trữ toàn bộ event history
✅ Stream processing — xử lý data real-time
✅ Log aggregation — collect logs từ nhiều service
✅ Khi cần replay messages
✅ Khi throughput cực cao (> 100K msg/s)
✅ Khi nhiều consumer cần đọc cùng data
✅ CDC (Change Data Capture)
✅ Khi cần long-term message retention
# có thể kết hợp cả hai không?
Hoàn toàn được. Nhiều hệ thống lớn dùng cả hai: Kafka làm "xương sống" chở luồng sự kiện, RabbitMQ lo phân việc và định tuyến cho các worker.

# production readiness checklist
Trước khi đưa hệ thống lên Production, rà lại các mục sau:
# cấu hình hạ tầng (infrastructure)
□ Cluster với ít nhất 3 nodes (số lẻ để quorum queue bầu được leader)
□ Quorum queues (classic mirrored queues đã bị gỡ từ 4.0)
□ Separate disk cho RabbitMQ data directory
□ Memory alarm threshold: vm_memory_high_watermark (mặc định 0.6 ở các bản hiện tại)
□ Disk alarm threshold: nâng disk_free_limit lên ít nhất bằng dung lượng RAM (mặc định chỉ 50MB)
□ TLS/SSL cho connections
□ Firewall rules: chỉ mở port cần thiết
- 5672: AMQP
- 5671: AMQPS (TLS)
- 15672: Management UI (restrict access)
- 25672: Inter-node communication
- 4369: epmd (node discovery khi cluster)
- 15692: Prometheus metrics (restrict access)
□ Separate vhost cho mỗi application/environment
□ Non-default credentials (KHÔNG dùng guest/guest)
# cấu hình ứng dụng (application)
□ Connection recovery (Spring AMQP tự kết nối lại, có sẵn mặc định)
□ Publisher confirms enabled
□ Manual acknowledgment
□ Idempotent consumers
□ Dead Letter Exchange configured
□ Proper error handling (business vs transient)
□ Message TTL set
□ Queue max-length set
□ Graceful shutdown handling
□ Health check endpoint include RabbitMQ
# giám sát & cảnh báo (monitoring & alerting)
□ Queue depth monitoring (alert khi > threshold)
□ Consumer count monitoring (alert khi = 0)
□ Memory usage monitoring
□ Disk usage monitoring
□ Connection count monitoring
□ Channel count monitoring
□ Message rate monitoring (publish/consume)
□ Unacked message count monitoring
□ Node health monitoring
□ Grafana dashboard cho RabbitMQ metrics
# spring boot health check
# application.yml
management:
endpoint:
health:
show-details: always # tiện khi dev; production nên dùng when-authorized
health:
rabbit:
enabled: true # Auto-configured khi spring-boot-starter-amqp có mặt
# Response:
# {
# "status": "UP",
# "components": {
# "rabbit": {
# "status": "UP",
# "details": {
# "version": "4.1.0"
# }
# }
# }
# }# lời kết
RabbitMQ không phải "viên đạn bạc", nhưng là một trong những món đáng tin nhất trong hộp đồ nghề của backend engineer khi làm hệ thống phân tán.
Mấy điểm mình muốn bạn nhớ sau bài này:
- Hiểu kiến trúc trước khi code: nắm Exchange, Queue, Binding và cách Channel hoạt động sẽ giúp debug hoặc thiết kế hệ thống tự tin hơn nhiều.
- Reliability không tự nhiên mà có: Publisher confirms, outbox, manual ack, idempotent consumers, DLX... mỗi thứ chặn một kiểu mất hoặc trùng message khác nhau. Trên Production, thường bạn sẽ cần gần như tất cả.
- Chọn đúng công cụ cho đúng bài toán: dùng RabbitMQ khi cần định tuyến linh hoạt, phân phối tác vụ (task queue) và xử lý request/response bất đồng bộ. Dùng Kafka khi cần lưu lịch sử sự kiện (event sourcing), stream dữ liệu thời gian thực hoặc cần replay message.
- Luôn giám sát (Monitor): đừng đợi hệ thống sập mới đi tìm nguyên nhân. Thiết lập cảnh báo cho độ dài hàng đợi (queue depth), số lượng consumer online và mức dùng RAM/Disk của broker để phát hiện lỗi sớm.
Tài liệu tham khảo:
- RabbitMQ Official Documentation
- Spring AMQP Reference
- AMQP 0-9-1 Protocol Reference
- RabbitMQ in Depth, Gavin M. Roy (Manning)
- Enterprise Integration Patterns, Hohpe & Woolf
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.