Async hay vướng không phải “dùng Rabbit hay Kafka”, mà channel đang là kiểu gì — và recovery bạn chọn có khớp kiểu đó không. Cùng “fail rồi xử lý lại” trên queue work và trên topic Kafka là hai thế giới.
Bài này là mental model queue channel vs streaming channel. Series tiếp: write / read / shared, resend có an toàn?, playbook recovery.
Demo: Consume xóa message vs chỉ trượt cursor.
Queue — lấy xong (thường) là hết
Hình dung hộp thư chung: message nằm trong queue, consumer nhận / claim, xử lý, ack → broker bỏ message (destructive consume). Nhiều worker cùng queue = competitive consumers: mỗi message chỉ một worker trong “nhóm” đó nhận — scale thêm worker là chia load, gần như không trần về số consumer (hạ tầng chịu nổi thì scale).
- RabbitMQ classic queue, SQS, nhiều “job queue” (Bull/BullMQ phía app + Redis list…) cùng họ ý.
- Order: broker có thể FIFO, nhưng scale nhiều consumer → xử lý thực tế hay out of order. Muốn order chặt thường phải 1 consumer (hy sinh parallel) hoặc order theo key ở tầng app.
- Redelivery: nack / timeout visibility / lease → message về queue (hoặc delayed). Không còn “lịch sử stream” để app khác đọc lại từ đầu.
publish → [m1 m2 m3]
consumer A ack m1 → m1 biến mất khỏi queue
consumer B lấy m2 → …
// không có “app Z đọc lại m1 từ channel” trừ khi bạn log/outbox riêng
Stream / topic — consume không xóa
Kafka-style (và Rabbit stream, v.v.): message append vào log, dọn theo retention. Consumer chỉ commit vị trí (offset) trong partition — “tôi đã xử lý tới đây”.
- Partition: lane con; cùng partition key → cùng lane → order trong lane.
- 1 active consumer / partition (trong một consumer group): scale consumer ≤ số partition; thừa consumer thì idle.
- Consumer group: nhiều instance chia partition; mỗi message ~ một lần / group. Tạo group khác = đọc lại cùng stream độc lập.
topic partitions: P0 [e0 e1 e2…] P1 [e0 e1…]
group billing: offset P0=2, P1=1 // “đã đọc”
group analytics: offset P0=0, P1=0 // đọc từ đầu, không xóa của billing
Fail recovery trên stream hay là không commit / rewind offset / seek — message vẫn nằm đó. “Resend vào topic” là thêm bản ghi mới (có thể duplicate cho mọi group) — xem resend & shared.
Bảng một nhìn
- Sau xử lý OK: queue → xóa (hoặc equivalent); stream → commit offset.
- Nhiều app đọc cùng nguồn: queue classic ≈ một nhóm; stream ≈ nhiều group tự nhiên.
- Scale workers: queue competitive tự do hơn; stream kẹt số partition.
- Order: stream per-key/partition rõ; queue scale ra thường mất global order.
- Replay lịch sử: stream (trong retention); queue thì phải tự lưu (event store, outbox log…).
Blur: Rabbit stream, Kafka “queue”…
Platform đang lai: Rabbit có stream; Kafka có thảo luận queue-like semantics. Vì vậy đừng gắn chết “Rabbit = queue, Kafka = stream” — hỏi:
1) Consume có xóa message khỏi channel không?
2) Nhiều consumer group / app đọc độc lập được không?
3) Order unit là gì (global / partition / key)?
4) Scale consumer bị chặn bởi gì?
Liên hệ series cũ
- BullMQ / lease — thế giới queue/job: claim, ack, redelivery.
- DLQ — poison trên queue hay “park” offset/error topic trên stream đều cần purpose (bài B).
- Idempotency — bắt buộc mọi kiểu channel (at-least-once + redelivery / resend).
Một câu để nhớ
Queue: message thường biến mất sau ack. Stream: message còn, consumer chỉ nhớ offset — và nhiều group nhớ offset khác nhau.
Chọn recovery (release, resend, ignore, DLQ…) chỉ an toàn khi bạn biết đang đứng trên kiểu channel nào. Tiếp: channel phục vụ write hay read?.