---
id: case-rabbitmq
title: "RabbitMQ: queue, retry và DLQ"
shortTitle: "RabbitMQ & DLQ"
track: cases
trackTitle: "Tình huống vận hành"
order: 11
level: core
status: published
estimatedMinutes: 45
prerequisites:
  - case-distributed
next: case-kafka
reviewedAgainst: 2026-10-12
sources:
  - title: "RabbitMQ Consumer Acknowledgements"
    url: https://www.rabbitmq.com/docs/confirms
  - title: "RabbitMQ Dead Letter Exchanges"
    url: https://www.rabbitmq.com/docs/dlx
  - title: "RabbitMQ Reliability Guide"
    url: https://www.rabbitmq.com/docs/reliability
---

# RabbitMQ: queue, retry và DLQ

RabbitMQ là message broker, không phải serverless. Cloud có thể vận hành broker giúp bạn, nhưng producer, consumer, acknowledgement và chuyện message bị trùng vẫn là việc của ứng dụng.

## Để làm gì

Bạn sẽ thiết kế một consumer không làm mất message khi crash, không retry vô hạn một message độc, và xử lý message trùng bằng idempotency.

## Gồm những gì

- Exchange, queue, binding, producer, consumer.
- Publisher confirm và consumer acknowledgement.
- Retry queue và dead-letter exchange (DLX).

Bài không dựng RabbitMQ cluster cho production; lab chỉ cần một container.

## Dùng khi nào

Gửi email, tạo thumbnail, xử lý đơn hàng ở nền, hoặc nối hai service mà request HTTP không cần đợi công việc làm xong.

Đừng dùng queue để che một call đồng bộ bắt buộc. Checkout cần biết payment đã được chấp nhận hay chưa thì phải nhận được một trạng thái rõ ràng, chứ không phải ném message vào queue rồi trả "thành công" trước khi payment xử lý xong.

## Kiến thức cốt lõi

Producer publish message vào exchange. Exchange route message qua binding vào queue. Consumer nhận message từ queue.

Có hai loại xác nhận khác nhau:

- Publisher confirm: broker báo cho producer là đã nhận được message.
- Consumer acknowledgement: consumer báo cho broker là đã xử lý xong message.

Nếu consumer ACK trước khi commit database rồi crash, message đã mất trong khi công việc chưa xong. Vì vậy phải ACK sau khi commit. Ngược lại, nếu commit xong mà crash trước khi kịp ACK, broker sẽ giao lại message, và công việc có thể chạy hai lần. Khi bạn muốn tránh mất message, RabbitMQ trên thực tế là at-least-once, nên consumer bắt buộc phải idempotent.

Cách làm idempotency phổ biến: mỗi message có `messageId` hoặc `orderId`, và consumer ghi lại id đã xử lý trong cùng transaction với thay đổi nghiệp vụ. Khi nhận lại một id cũ, consumer chỉ ACK mà không làm lại.

Gọi `nack(requeue=true)` ngay lập tức với một message độc sẽ tạo ra vòng lặp nóng: consumer nhận, fail, trả lại, rồi nhận lại, vừa ăn CPU vừa đầy log. Retry nên đi qua một queue có TTL, để message dead-letter quay về queue chính sau một khoảng chờ. Quá số lần cho phép thì đưa message vào DLQ, để người vận hành đọc và quyết định có replay hay không.

DLQ không phải nghĩa địa không ai ghé thăm. Bạn cần alert theo số message trong DLQ, dashboard theo tuổi của message cũ nhất, và runbook replay sau khi đã sửa code.

Prefetch giới hạn số message chưa ACK mà mỗi consumer được giữ. Nếu không giới hạn, một consumer sẽ kéo về quá nhiều message, giữ hết trong RAM, trong khi các consumer khác ngồi không.

Khi queue có sự cố, đọc các con số theo thứ tự: số message trong queue và tốc độ ACK trước, rồi tới số message trong DLQ, rồi mới tới log của consumer. Hai con số đầu cho biết consumer đang chậm hay đang fail; log chỉ có ích khi bạn đã biết mình cần tìm loại lỗi nào.

## Lệnh và code mẫu

Lab local:

```bash
docker run -d --name lab-rabbit -p 5672:5672 -p 15672:15672 rabbitmq:4-management
docker logs lab-rabbit
```

UI nằm ở `http://127.0.0.1:15672`. Credential mặc định `guest/guest` chỉ dùng được từ localhost. Không mở port 15672 ra Internet.

Pseudo-code consumer:

```js
channel.prefetch(10);
channel.consume("orders", async (msg) => {
  const event = JSON.parse(msg.content.toString());
  try {
    await db.transaction(async (tx) => {
      if (await tx.processed.exists(event.messageId)) return;
      await applyOrder(tx, event);
      await tx.processed.insert(event.messageId);
    });
    channel.ack(msg);
  } catch (error) {
    channel.nack(msg, false, false); // DLX, không requeue nóng
  }
});
```

Tham số `false` cuối cùng giả định queue đã được cấu hình DLX. Nếu queue không có DLX, message sẽ bị bỏ luôn. Vì vậy cấu hình hạ tầng của queue và code consumer phải được review cùng nhau.

## Thực hành

1. Vẽ luồng producer → exchange → queue → consumer.
2. Cho consumer sleep 10 giây rồi kill nó trước khi ACK. Message phải quay lại queue.
3. Gửi hai message có cùng `messageId`. Database chỉ được thay đổi một lần.
4. Đưa một message sai schema vào DLQ, không retry vô hạn.
5. Xóa container lab.

## Kết quả mong đợi

Crash trước khi ACK không làm mất message. Message trùng không tạo ra hai đơn hàng. Message độc đi vào DLQ sau số lần retry giới hạn.

## Checklist

- [ ] Dùng durable queue và persistent message khi dữ liệu phải sống qua lần broker restart.
- [ ] ACK sau khi commit.
- [ ] Consumer idempotent theo message id nghiệp vụ.
- [ ] Retry có delay và có giới hạn; quá giới hạn thì vào DLQ.
- [ ] Có alert theo backlog, tuổi message và số message trong DLQ.

## Lỗi thường gặp

**ACK ngay khi nhận cho queue trông ngắn.** Consumer crash là mất message. Queue nhìn thì đẹp, còn dữ liệu thì sai.

**Requeue vô hạn.** Một message sai JSON chiếm hết consumer. Hãy dead-letter sau một số lần cố định.

**Scale consumer không giới hạn khi database đang chậm.** Queue giảm nhưng database chết. Concurrency của consumer phải tính theo sức chịu của downstream.

## Nguồn

1. [Consumer acknowledgements and publisher confirms](https://www.rabbitmq.com/docs/confirms) — hai loại xác nhận.
2. [Dead Letter Exchanges](https://www.rabbitmq.com/docs/dlx) — route message bị reject hoặc hết TTL.
3. [Reliability Guide](https://www.rabbitmq.com/docs/reliability) — kết nối, duplicate và phục hồi.

## Tiếp theo

[Kafka: partition, offset và consumer lag](./12-kafka.md)
