Bài toán: đăng ký mà không gửi email trực tiếp
// File: sync.js
// Đăng ký xong, gửi email NGAY trong request
app.post("/api/auth/register", async (req, res) => {
const user = await createUser(req.body);
await mailer.sendWelcome(user.email); // chậm 2-5 giây!
await stats.increment(); // thêm nữa
res.status(201).json(user);
});
// Email service sập -> cả đăng ký cũng lỗi theo
// File: async.js
app.post("/api/auth/register", async (req, res) => {
const user = await createUser(req.body);
await queue.publish("user.registered", { userId: user.id });
res.status(201).json(user); // trả về ngay
});
// Worker riêng lắng nghe queue và làm việc phụ
queue.consume("user.registered", async ({ userId }) => {
const user = await getUser(userId);
await mailer.sendWelcome(user.email);
await stats.increment();
});
Việc phụ (side effect) chậm, có thể thất bại, không cần trả về tức thì: email, push notification, xử lý ảnh, tính toán thống kê. Nếu chỉ là “gọi hàm thêm” nhanh, đừng thêm độ phức tạp.
RabbitMQ vs Kafka
| Đặc điểm | RabbitMQ | Kafka |
|---|---|---|
| Mô hình | Queue, routing phức tạp | Log phân tán, replay được |
| Cách tiêu thụ | Một consumer nhận mỗi message | Nhiều consumer group độc lập |
| Lưu trữ | Xoá sau khi xử lý | Giữ lại, replay được |
| Phù hợp | Task/job, gửi email, công việc nền | Event streaming, log, data pipeline |
| Độ phức tạp | Dễ hơn | Khó hơn, nặng hơn |
At-least-once và idempotency
Broker thường gửi “ít nhất một lần” — message có thể bị xử lý trùng. Nên consumer phải idempotent: xử lý trùng không gây hậu quả (dùng unique key, so trạng thái trước khi ghi).
Dead letter queue
Message xử lý thất bại nhiều lần sẽ bị đẩy sang dead letter queue (DLQ) — nơi để con người xem xét. Đừng để message hỏng chặn cả hàng đợi.
❓ Worker xử lý message gửi email. Nhận tin "user.registered" lần hai vì broker gửi lại. Nên làm gì?
- Nhận biết việc phụ nên tách bất đồng bộ
- Chọn RabbitMQ hay Kafka theo bài toán
- Viết consumer idempotent
- Cấu hình dead letter queue