Kafka, RabbitMQ hay Redis Streams? Chọn message queue cho backend
Hồi mới xây service xử lý đơn hàng, mình gặp cảnh quen thuộc: order vừa tạo xong, server phải gọi liên tiếp email, push notification, cập nhật kho, cộng điểm... Chỉ một service chậm thôi là cả chuỗi nghẽn lại, user chờ mòn mỏi. Lúc đó mình mới thấm cái câu "đừng gọi API đồng bộ trong request path" — nghe dễ mà làm mới thấy đau.
Giải pháp quen thuộc là nhét một message queue vô giữa: viết việc vào queue rồi trả response liền, phía sau worker nào rảnh thì xử lý. Nhưng mở docs ra thì có ba cái tên chờ sẵn — Kafka, RabbitMQ, Redis Streams — và câu hỏi muôn thuở: chọn cái nào?
Ảnh: Brett Sayles — Pexels
RabbitMQ — ông bưu điện thông minh
RabbitMQ sinh ra để làm "bưu điện": nó hiểu routing (exchange, binding, routing key), hỗ trợ đủ pattern từ work queue, pub/sub, priority queue tới delayed message. Message được đẩy tới consumer ngay khi có — ai rảnh thì nhận. Phù hợp với workload dạng task: gửi email, resize ảnh, xử lý file...
Điểm yếu: throughput không cao (vài chục nghìn msg/s là đuối so với Kafka), và không replay được lịch sử — message biến mất sau khi được ack.
Kafka — cái log phân tán
Kafka thật ra không phải message queue theo nghĩa truyền thống, mà là append-only log. Message ghi vào partition, giữ lại theo retention (mặc định 7 ngày), ai muốn đọc lại cứ đọc. Nhờ vậy nó cho replay, cho nhiều consumer group đọc cùng lúc, throughput hàng trăm nghìn msg/s.
Đổi lại, vận hành nặng: cluster, partition, offset, rebalance... và pull-based nên latency cao hơn, không "real-time" bằng RabbitMQ.
Redis Streams — nhẹ mà đủ dùng
Đây là lựa chọn mình thích nhất cho hệ thống cỡ vừa: khỏi thêm infra (dùng chung Redis đang chạy cache), API đơn giản, có sẵn consumer group, pending list để retry, dead letter tự làm được. Một node Redis xử lý ~100k msg/s — quá đủ cho đại đa số hệ thống thực tế.
Nhanh gọn thế này:
| Tiêu chí | RabbitMQ | Kafka | Redis Streams |
|---|---|---|---|
| Kiểu hoạt động | Push | Pull / log | Pull / log |
| Replay lịch sử | Không | Có | Có (theo maxlen) |
| Throughput | Trung bình | Rất cao | Cao (1 node) |
| Vận hành | Dễ | Nặng | Rất nhẹ |
| Phù hợp nhất | Task / job | Event streaming | Startup, scale vừa |
Code mẫu: consumer với Redis Streams (Go)
Producer chỉ cần một lệnh XADD:
client.XAdd(ctx, &redis.XAddArgs{
Stream: "orders",
Values: map[string]any{
"order_id": orderID,
"amount": amount,
"user_id": userID,
},
})
Consumer thì đọc theo group, xử lý xong mới ack:
client.XGroupCreateMkStream(ctx, "orders", "payments", "0")
for {
res, _ := client.XReadGroup(ctx, &redis.XReadGroupArgs{
Group: "payments",
Consumer: "worker-1",
Streams: []string{"orders", ">"},
Count: 10,
Block: 5 * time.Second,
})
for _, stream := range res {
for _, msg := range stream.Messages {
if err := processPayment(ctx, msg); err != nil {
moveToDLQ(ctx, msg) // ghi vào stream orders:dlq
continue
}
client.XAck(ctx, "orders", "payments", msg.ID)
}
}
}
Ảnh: Alicia Christin Gerald — Pexels
3 bài học trả giá bằng production
-
At-least-once là mặc định — consumer phải idempotent. Message có thể bị xử lý hai lần: consumer crash giữa chừng, retry, rebalance... Dùng order_id làm idempotency key, kiểm tra đã xử lý chưa trước khi ghi — không thì user bị trừ tiền hai lần lúc nào không hay.
-
Ack SAU khi xử lý xong, không bao giờ ack trước. Ack sớm mà crash là message mất vĩnh viễn. Ack trễ thì message nằm trong pending list — đó chính là cơ chế retry tự nhiên. Consumer chết thì XAUTOCLAIM (Redis 6.2+) nhận lại message pending của nó.
-
DLQ phải có alert. Đừng để dead letter nằm im trong góc. Mình từng mất cả buổi tối truy vết một batch order "mất tích" — hóa ra nó nằm trong DLQ từ ba ngày trước mà không ai để ý. Theo dõi độ dài DLQ, vượt ngưỡng là có bug.
Ảnh: panumas nikhomkhai — Pexels
Kết
Quy tắc của mình giờ đơn giản: đã có Redis thì bắt đầu bằng Redis Streams; cần routing phức tạp hay delayed task thì nhảy qua RabbitMQ; cần replay, throughput khủng, nhiều consumer group thì mới lên Kafka. Đừng chọn Kafka vì "nghe sang" — vận hành Kafka là một công việc toàn thời gian, không phải thứ mình muốn gánh khi hệ thống mới chỉ vài chục message mỗi giây.
📋 Phụ lục thuật ngữ
- Message queue — hàng đợi tin nhắn, nơi producer ghi việc cần làm và consumer đọc ra xử lý bất đồng bộ
- Ack (acknowledge) — xác nhận consumer đã xử lý xong message
- Consumer group — nhóm consumer cùng đọc một stream, mỗi message chỉ một thành viên xử lý
- DLQ (Dead Letter Queue) — hàng đợi chứa message xử lý thất bại nhiều lần
- Idempotent — thao tác chạy nhiều lần vẫn cho kết quả giống chạy một lần
- Retention — thời gian Kafka giữ message trước khi xoá
- Pending list — danh sách message đã giao cho consumer nhưng chưa được ack