[Backend Masterclass] Giới thiệu Message Queue: "Bưu điện" giải cứu hệ thống khỏi cảnh ôm nhau cùng sập
Chào anh em! Ở bài trước, chúng ta đã đưa Elasticsearch vào để làm tính năng tìm kiếm siêu tốc. Nhưng ngay sau đó, một vấn đề cực lớn nảy sinh: Khi khách hàng mua xong một lọ kem chống nắng, kho bị trừ đi 1. Hệ thống của bạn phải làm 3 việc:
- Update số lượng trong DB (MySQL).
- Xóa Cache (Redis).
- Gọi API sang Elasticsearch để update lại Inverted Index.
Nếu bạn viết code chạy tuần tự (Đồng bộ - Synchronous): Làm xong việc 1 -> làm việc 2 -> làm việc 3. Thảm họa xảy ra: Lỡ server Elasticsearch hôm đó bị lag, nó ngâm cái request mất 10 giây. Thế là App của bạn cũng bắt khách hàng nhìn cái vòng xoay xoay Loading... trọn vẹn 10 giây đó, dù tiền thì đã trừ rồi! Tệ hơn, nếu gọi API sang Elasticsearch bị lỗi (Timeout), toàn bộ giao dịch bị Rollback. Khách hàng chửi rủa, công ty mất tiền.
Để giải quyết bài toán giao tiếp giữa các Component/Microservices này, chúng ta cần rước về một "Kẻ điều phối" vĩ đại: Message Queue (Hàng đợi thông điệp).
1. Message Queue là cái quái gì?
Đừng nghĩ nó phức tạp. Hãy tưởng tượng mô hình Quán Phở:
- Không có Queue: Khách (User) vào order phở. Thu ngân nhận tiền, rồi tự chạy vào bếp nấu. Khách đứng chờ thu ngân nấu xong, bưng ra mới được đi tìm bàn ngồi. Rất ngớ ngẩn và chậm chạp!
- Có Queue: Khách order -> Thu ngân nhận tiền, viết một cái Bill ném vào Kẹp Order (Queue) -> Thu ngân bảo khách: "Dạ anh ra bàn ngồi chờ chút ạ" (Trạng thái Success). -> Trong bếp, các Đầu bếp (Worker) cứ tuần tự rút từng cái Bill trên kẹp xuống để nấu. Nấu xong thì bưng ra.
Trong kĩ thuật, thuật ngữ của nó như sau:
- Producer (Người sản xuất): Thu ngân - Kẻ tạo ra thông điệp (Message) và ném vào hàng đợi.
- Queue (Hàng đợi): Cái kẹp order - Nơi lưu trữ thông điệp tạm thời theo nguyên tắc FIFO (Vào trước ra trước).
- Consumer/Worker (Người tiêu thụ): Đầu bếp - Kẻ túc trực ở Queue, thấy có thông điệp thì lôi ra xử lý.
Các công cụ phổ biến nhất hiện nay là RabbitMQ và Apache Kafka.
2. 3 Quyền năng vô song của Message Queue
Tại sao các hệ thống lớn (chạy Laravel, Golang hay Node.js) không thể sống thiếu Queue?
Quyền năng 1: Bất đồng bộ (Asynchronous) - Tăng tốc độ phản hồi Quay lại ví dụ mua kem chống nắng ở đầu bài. Với Queue, logic sẽ đổi thành:
- Update DB (Mất 50ms).
- Ném một message:
{"action": "update_inventory", "product_id": 123}vào Queue (Mất 10ms). - Trả về
200 OKcho khách hàng ngay lập tức! (Tổng cộng mất 60ms).
Phía sau cánh gà, một con Worker (có thể viết bằng Golang cho nhanh) sẽ âm thầm tóm lấy cái message kia và đi dọn dẹp phần còn lại: xóa Redis, update Elasticsearch. Khách hàng không bao giờ phải chờ đợi những tác vụ râu ria này.
Quyền năng 2: Chống sốc tải (Load Leveling / Buffering) Ngày 11/11, công ty chạy Flash Sale. Bình thường có 100 đơn/phút, nay vọt lên 10,000 đơn/phút. Nếu chọc thẳng vào Database, DB sẽ giãy đành đạch và lăn ra chết vì quá tải Connection. Nhưng nếu có Queue: 10,000 cái order đó sẽ được đẩy hết vào hàng đợi. Nó đóng vai trò như một cái "hồ chứa". Đội Worker phía sau (ví dụ có 20 con) cứ từ từ mà nhẩn nha xử lý với tốc độ ổn định là 500 đơn/phút. App không bao giờ sập, chỉ là người dùng sẽ nhận được email xác nhận đơn hàng chậm hơn vài phút thôi.
Quyền năng 3: Tách rời hệ thống (Decoupling) Hãy tưởng tượng Service Thanh Toán (viết bằng Java) cần gửi Email hóa đơn. Thay vì Service Thanh Toán phải import thư viện gửi mail, phải biết thông tin SMTP các kiểu... Nó chỉ việc hét lên một câu (ném vào Queue): "Ê, thằng Hiếu vừa mua hàng xong đấy!". Bên kia, Service Gửi Mail (viết bằng Node.js) đang lắng nghe ở Queue, thấy có tin báo liền tự động lấy thông tin đi gửi mail. Lợi ích: Service Gửi Mail có bị sập cả ngày thì Service Thanh Toán vẫn hoạt động bình thường, khách vẫn mua được hàng. Message cứ nằm chờ trong Queue, bao giờ Service Mail sống lại thì gửi bù!
3. Demo sương sương kiến trúc "Hét - Nghe" (Pub/Sub)
Đây là kịch bản giả mã (Pseudo-code) cực kì kinh điển của RabbitMQ sử dụng mô hình Fanout Exchange (Một người hét, nhiều người nghe):
Phía App Thanh toán (Producer):
// Khi user thanh toán thành công
$orderData = ['order_id' => 888, 'user_email' => 'hieu@example.com'];
// Ném vào một cái Loa phát thanh (Exchange) có tên là 'order.completed'
RabbitMQ::publish('order.completed', json_encode($orderData));
return response()->json(['message' => 'Thanh toán thành công!']);
Phía Worker Gửi Mail (Consumer 1 - Có thể là một app Node.js khác):
// Lắng nghe trên Queue được nối với loa 'order.completed'
RabbitMQ.consume('queue_send_email', (message) => {
let order = JSON.parse(message);
EmailService.send(order.user_email, "Cảm ơn bạn đã mua hàng!");
});
Phía Worker Elasticsearch (Consumer 2 - Có thể là một app Golang):
// Cùng lắng nghe cái loa đó, chả liên quan gì đến thằng gửi mail
RabbitMQ.Consume("queue_update_elastic", func(message []byte) {
order := parse(message)
ElasticSearch.UpdateInventory(order.ID)
})
Anh em thấy sự kì diệu chưa? 1 tác vụ sinh ra, ném vào Queue, và N các hệ thống khác nhau (khác cả ngôn ngữ lập trình) có thể tự động bâu vào xử lý mà app chính không hề hay biết!
Tổng kết & Vấn đề nhức nhối tiếp theo
Đưa Queue vào kiến trúc là một bước ngoặt biến bạn từ một người làm "Web nhỏ" sang tư duy làm "Hệ thống lớn". Nó giải quyết bài toán hiệu năng cực kì hoàn hảo.
Nhưng (lại nhưng)... Ở đời không có viên đạn bạc (Silver bullet). Đưa Queue vào nghĩa là bạn chấp nhận hệ thống có độ trễ (Eventual Consistency). Và đau đầu nhất là: Chuyện gì xảy ra nếu con Worker đang xử lý message thì bị crash (Out of memory)? Thông điệp đó sẽ biến mất luôn hay được xử lý lại? Nếu xử lý lại mà bị lỗi tiếp thì nó cứ kẹt mãi ở đó làm tắc nghẽn toàn bộ hàng đợi sao?
Nếu anh em từng làm việc với Queue chắc chắn đã từng bị "cháy máy" vì tin nhắn lỗi kẹt cứng trong hệ thống. Để giải quyết, chúng ta có một khái niệm cực ngầu gọi là DLQ (Dead Letter Queue) - Nghĩa trang của những thông điệp chết.
Mời anh em đón đọc ở bài sau: 👉 Bài tiếp theo: Dead Letter Queue (DLQ) và chiến lược Retry không làm sập hệ thống.
Nhớ Upvote và Clip bài viết nếu thấy hợp gu nhé anh em!
All rights reserved