Quay lại
Công Nghệ

What is Message Queue?

13 phút đọc8 thg 7, 2026
H

hoanggg2110

Tác giả

What is Message Queue?

Nếu bạn từng build một hệ thống mà có một tác vụ chạy chậm (gửi email, resize ảnh, tính toán nặng) làm cho cả API bị treo theo, thì gần như chắc chắn bạn sẽ được giới thiệu tới message queue như một "cây đũa thần". Nhưng queue không miễn phí — nó đánh đổi độ phức tạp lấy khả năng chịu tải, và phần đánh đổi đó chính là nơi hầu hết các bài blog "giới thiệu message queue" bỏ qua.

Bài này đi thẳng vào 5 câu hỏi mà bất kỳ ai dùng message queue trong production cũng sẽ đụng phải, từ cơ chế hoạt động bên dưới cho tới những failure mode kinh điển.


1. Message Queue là gì? Vì sao cần nó? Hoạt động ra sao?

Vấn đề trước khi có queue

Giả sử bạn có một API POST /orders. Khi user đặt hàng, server cần:

  1. Ghi đơn hàng vào database

  2. Gửi email xác nhận

  3. Trừ kho

  4. Bắn event cho hệ thống thống kê

Nếu làm tất cả đồng bộ (synchronous) trong cùng một request, thì:

Message Queue giải quyết gì?

Message queue là một lớp trung gian giúp tách rời (decouple) người gửi (producer) và người xử lý (consumer) theo thời gian và theo tốc độ. Producer chỉ cần "ném" message vào queue rồi trả response ngay cho user; consumer sẽ lấy message ra và xử lý bất cứ khi nào nó sẵn sàng.

Ba lợi ích cốt lõi:

Cơ chế hoạt động (dưới lớp vỏ)

Về bản chất, một message queue là một cấu trúc dữ liệu kiểu FIFO (First In First Out) được host trên một service riêng, với ba thành phần: Producer → Queue/Broker → Consumer.

What is Message Queue?

Bên trong broker, mỗi message thường đi qua các trạng thái:

  1. Enqueue: producer gửi message, broker ghi message vào một log hoặc storage bền vững (không chỉ giữ trong RAM).

  2. Ready: message nằm trong hàng đợi, sẵn sàng được lấy.

  3. In-flight / Unacked: một consumer đã "lấy" message này ra để xử lý, nhưng broker chưa xoá nó — nó chỉ đánh dấu là "đang được ai đó xử lý" kèm theo một cái đồng hồ đếm ngược (visibility timeout).

  4. Acked / Deleted: consumer báo "tôi xử lý xong rồi" (ack), broker mới thực sự xoá message khỏi queue.

  5. Nacked / Requeued: consumer báo lỗi, hoặc hết giờ mà không ack → message quay lại trạng thái Ready để consumer khác lấy lại.

Điểm quan trọng nhất cần nhớ: message chỉ biến mất khỏi queue khi được ack tường minh. Đây chính là nền tảng cho "at-least-once delivery" — broker thà gửi message hai lần còn hơn làm mất nó.

Về mặt lưu trữ, hai mô hình phổ biến:


2. Race Condition: Nhiều consumer cùng tranh nhau một message

Đây là bug kinh điển khi bạn scale consumer lên nhiều instance để tăng throughput.

Kịch bản lỗi

Giả sử bạn tự làm một hàng đợi job đơn giản trên một bảng dữ liệu, với mỗi record có một trạng thái (pending, done...). Nếu có 2 consumer cùng chạy gần như đồng thời:

  1. Consumer A đọc record → thấy job #1 đang ở trạng thái pending.

  2. Consumer B cũng đọc → thấy job #1 vẫn đang pending (vì A chưa kịp cập nhật trạng thái).

  3. Cả A và B đều xử lý job #1 → gửi email trùng, trừ tiền hai lần, hoặc double-charge thẻ khách hàng.

Đây chính là race condition kiểu "double delivery do thiếu khóa" — không liên quan gì đến network, chỉ đơn thuần là hai luồng đọc cùng một dữ liệu trước khi nó kịp bị khoá.

What is Message Queue?

Cách các message queue thật xử lý

a. Visibility timeout / lock ẩn (SQS, RabbitMQ)

Khi một consumer lấy message ra, broker ẩn message đó khỏi các consumer khác trong một khoảng thời gian (ví dụ 30 giây). Nếu consumer không ack trong thời gian đó (crash, treo, network lỗi), broker coi như xử lý thất bại và đưa message trở lại hàng đợi cho consumer khác lấy.

Vấn đề: nếu bạn set visibility timeout quá ngắn so với thời gian xử lý thực tế, message sẽ bị "phát lại" trong khi consumer đầu tiên vẫn đang chạy — dẫn đến xử lý trùng. Ngược lại nếu bạn không tuning timeout theo tải thực tế thì risk vẫn còn.

b. Khoá độc quyền + bỏ qua dòng đã khoá (tự build queue trên database)

Nếu bạn build queue thủ công trên một hệ quản trị dữ liệu quan hệ, pattern chuẩn là: khi một consumer chọn một record để xử lý, nó khoá độc quyền record đó trong phạm vi transaction — không consumer nào khác đọc/ghi được cho tới khi transaction commit. Đồng thời, nếu một consumer khác đang tìm record để lấy mà gặp record đang bị khoá, nó sẽ tự động bỏ qua và tìm record tiếp theo, thay vì đứng đợi.

Kết quả: nhiều consumer chạy song song, mỗi consumer luôn lấy được một job khác nhau, không ai giẫm chân ai, và không ai phải đợi ai.

c. Idempotency ở tầng consumer — lớp phòng thủ cuối cùng

Dù broker có cơ chế lock tốt tới đâu, at-least-once delivery vẫn có thể khiến một message được xử lý nhiều hơn 1 lần (ví dụ consumer xử lý xong, chuẩn bị ack thì bị kill). Vì vậy, thiết kế consumer handler nên idempotent: xử lý cùng một message N lần cho ra kết quả giống hệt như xử lý 1 lần. Cách làm phổ biến: dùng message_id hoặc idempotency_key để kiểm tra "đã xử lý chưa" trước khi làm, thường lưu trong một bộ nhớ đệm nhanh như Redis hoặc một bảng ghi nhận riêng.

Nguyên tắc: broker giúp giảm khả năng trùng lặp, nhưng chỉ có idempotency ở consumer mới triệt để loại bỏ hậu quả của việc trùng lặp.


3. Dead Letter Queue (DLQ)

Vấn đề: message "độc" làm nghẽn cả hệ thống

Tưởng tượng một message bị lỗi dữ liệu (ví dụ payload thiếu field bắt buộc) khiến consumer luôn luôn crash hoặc throw exception khi xử lý nó. Nếu broker cứ requeue message này mỗi khi consumer fail, bạn sẽ có một vòng lặp vô hạn:

lấy message → xử lý fail → requeue → lấy lại → xử lý fail → requeue → ...

Message này gọi là poison message. Nó không chỉ tự nó không bao giờ xử lý được, mà còn:

DLQ giải quyết như thế nào

Dead Letter Queue là một queue phụ, nơi message được chuyển tới sau khi:

What is Message Queue?

Lợi ích:

Một best practice đi kèm: cấu hình alert khi có message rơi vào DLQ, vì đây thường là dấu hiệu sớm của một lỗi hệ thống (schema mismatch, dependency down, bug logic) — không nên để DLQ âm thầm phình to mà không ai biết.


4. Bài toán Throughput: Producer sản xuất nhanh hơn Consumer tiêu thụ

Vấn đề

Queue giúp buffer traffic tăng đột biến — nhưng buffer không phải là vô hạn. Nếu tốc độ sản xuất trung bình của producer liên tục lớn hơn tốc độ tiêu thụ trung bình của consumer trong một thời gian dài (không chỉ là spike ngắn hạn), độ dài queue sẽ tăng dần vô hạn.

Hậu quả:

What is Message Queue?

Các chiến lược xử lý

a. Scale consumer theo chiều ngang (horizontal scaling)

Cách đơn giản nhất: tăng số lượng consumer instance để tăng tổng throughput tiêu thụ. Nhiều hệ thống dùng auto-scaling dựa trên độ dài queue (ví dụ: queue vượt một ngưỡng nhất định → tự động tăng thêm consumer).

Giới hạn: consumer scale không vô hạn được — nếu downstream (database, API bên thứ ba) chỉ chịu được một mức tải nhất định, việc thêm consumer chỉ đẩy nghẽn từ queue sang downstream.

b. Backpressure — báo ngược cho producer "chậm lại"

Backpressure là cơ chế để hệ thống phía sau (consumer/queue) báo hiệu ngược lên cho phía trước (producer) biết "tôi đang quá tải, đừng gửi nữa (hoặc gửi chậm lại)", thay vì âm thầm để hàng đợi phình to không kiểm soát.

Vài cách áp dụng thực tế:

c. Batching

Thay vì consumer xử lý từng message một, gom nhiều message thành một batch rồi xử lý cùng lúc (ví dụ ghi một lần cho cả trăm message thay vì ghi riêng lẻ từng cái). Batching giảm overhead trên mỗi message, tăng throughput hiệu dụng đáng kể — đặc biệt hiệu quả với các thao tác I/O như ghi dữ liệu, gọi API.

d. Priority queue / phân luồng theo mức độ quan trọng

Nếu không thể xử lý hết mọi message kịp thời, ít nhất đảm bảo message quan trọng (ví dụ giao dịch thanh toán) được xử lý trước message ít quan trọng (ví dụ log analytics), bằng cách tách thành nhiều queue với độ ưu tiên khác nhau.

Về bản chất, backpressure là cách hệ thống nói "chậm lại" một cách tường minh và có kiểm soát, thay vì để hệ thống tự sập một cách âm thầm và mất kiểm soát khi vượt ngưỡng.


5. Khi Message Queue crash hoặc hỏng — làm sao không mất message?

Đây là câu hỏi về durability (độ bền) — queue tồn tại để đảm bảo message không bị mất, vậy chính bản thân nó bị chết thì sao?

Vấn đề

Nếu broker chỉ giữ message trong RAM (in-memory queue) mà không ghi xuống đĩa, thì khi:

Toàn bộ message đang nằm trong queue sẽ biến mất vĩnh viễn, kể cả những message đã được producer gửi thành công (broker đã trả ACK cho producer) nhưng chưa kịp được consumer xử lý.

What is Message Queue?

Các lớp phòng thủ

a. Persistence — ghi message xuống disk

Broker nghiêm túc (RabbitMQ, Kafka, SQS) đều ghi message xuống disk (hoặc write-ahead log) trước khi báo "nhận thành công" cho producer, không chỉ giữ trong RAM.

Trade-off: ghi disk chậm hơn RAM → giảm throughput để đổi lấy durability. Đây là lý do các broker thường cho phép cấu hình mức độ persistence (ví dụ RabbitMQ có durable queue + persistent message; nếu không bật, message chỉ sống trong RAM và mất khi restart).

b. Replication — nhân bản dữ liệu ra nhiều node

Chỉ ghi disk trên 1 node vẫn chưa đủ, vì disk đó có thể hỏng, hoặc cả server đó chết.

Giải pháp: replicate message ra nhiều broker node (thường 3 node là tiêu chuẩn phổ biến).

Nếu leader node chết, một trong các replica sẽ được bầu làm leader mới, và message vẫn còn nguyên vì đã được nhân bản trước đó.

c. Producer-side: chờ ACK trước khi coi là "đã gửi thành công"

Producer không nên coi message là "an toàn" ngay khi gọi hàm gửi — mà phải đợi broker ACK tường minh rằng message đã được ghi bền vững (persist + đủ replica). Nếu không nhận được ACK (timeout, network lỗi), producer cần retry gửi lại. Đây chính là điểm mà idempotency ở consumer (đã nói ở mục 2) lại phát huy tác dụng — vì retry ở producer có thể dẫn tới gửi trùng.

d. Consumer-side: không mất message đang xử lý dở

Kết hợp với cơ chế visibility timeout / ack ở mục 2: nếu consumer đang xử lý message mà crash giữa chừng (chưa kịp ack), broker sẽ tự động đưa message đó trở lại hàng đợi sau khi visibility timeout hết hạn, để consumer khác xử lý lại. Message không bị mất — nó chỉ bị trễ, và nhờ idempotency, việc xử lý lại không gây tác dụng phụ.

e. Backup / cross-region disaster recovery

Với hệ thống cực kỳ quan trọng (ví dụ giao dịch tài chính), ngoài replication trong cùng cluster, có thể cần backup snapshot định kỳ hoặc mirror sang region khác, để chống lại kịch bản cả cluster/datacenter chính bị sập hoàn toàn.

Tóm gọn nguyên tắc durability: persist trước khi ACK, replicate trước khi coi là an toàn, và luôn thiết kế consumer idempotent để retry không phải là nỗi sợ.


Tổng kết

Message queue nhìn bề ngoài đơn giản — chỉ là một hàng đợi FIFO. Nhưng để dùng nó đúng trong production, bạn cần hiểu và xử lý được 4 lớp vấn đề:

Hiểu rõ 4 lớp này không chỉ giúp bạn debug nhanh hơn khi có sự cố, mà còn giúp bạn chọn đúng công cụ (RabbitMQ, Kafka, SQS, hay tự build) tuỳ theo yêu cầu thực tế của hệ thống, thay vì chọn theo trend.

Thích bài viết này?

Nội dung trên Vết Mực luôn được chia sẻ miễn phí. Nếu bài viết mang lại giá trị cho bạn, hãy cân nhắc ủng hộ để chúng mình có thể duy trì máy chủ, phát triển thêm tính năng mới và tiếp tục xây dựng một không gian dành cho những người yêu viết lách. ✨

Các cách ủng hộ:

  • Viết và đăng bài trên Vết Mực
  • Chia sẻ bài viết với bạn bè
  • Góp ý để chúng mình cải thiện sản phẩm qua email: nsikhoa@gmail.com

Dù bạn chọn ủng hộ hay chỉ đơn giản là tiếp tục đọc và chia sẻ bài viết, đó đều là nguồn động lực rất lớn với chúng mình. ❤️

Bình luận

Đăng nhập để để lại bình luận.