← Vibe coding với Next.js

Bài 13 · Nâng cao · 28 phút

Hàng đợi BullMQ & khoá số dư

Biên soạn bởi Nguyễn Anh Tuấn

Xử lý việc nặng và nhạy cảm đúng cách: hàng đợi (queue) với BullMQ (trên nền Redis) để chạy nền - gửi email giao dịch (biến động số dư, đặt lại mật khẩu), cập nhật đơn - không bắt khách chờ và tự thử lại khi lỗi. Và bài toán số dư tài khoản khách: dùng khoá lạc quan (optimistic lock) hay khoá bi quan (pessimistic lock) để tránh trừ lố khi nhiều yêu cầu chạy cùng lúc (race condition).

Tiền đã vào ví an toàn ở bài trước. Còn hai chỗ chưa ổn: (1) gửi email biến động số dư mà bắt khách đứng chờ thì tệ; (2) lúc khách mua hàng, nếu nhiều yêu cầu trừ tiền cùng lúc, số dư dễ bị trừ lố. Bài này lo cả hai: hàng đợi cho việc (1), khoá số dư cho việc (2).

Gửi email có thể mất một, hai giây (hoặc lỗi rồi phải thử lại). Nếu làm ngay trong lúc khách bấm nút, khách phải đợi cả khoảng đó. Cách đúng: bỏ việc vào hàng đợi rồi trả lời khách ngay; một worker chạy nền lấy ra làm sau:

RequestApptrả ngay⚡ Trả lời khách ngay(không phải chờ)bỏ jobHàng đợi(Redis)Worker(chạy nền)📧 Email/ cập đơn
Việc nặng (gửi email, cập nhật đơn) bỏ vào hàng đợi rồi trả lời khách ngay; worker chạy nền xử lý sau, tự thử lại nếu lỗi.

Khoá dùng BullMQ - một thư viện hàng đợi chạy trên nền Redis (chính Redis mèo con đã thêm để cache). Vậy Redis vừa làm cache vừa làm nền cho hàng đợi.

  • App bỏ job vào queue (vd "gửi email biến động cho khách X"), trả lời khách ngay.
  • Worker chạy nền lấy job ra xử lý - không nằm trong đường đi của request.
  • Job lỗi được THỬ LẠI tự động; quá số lần thì vào dead-letter để xem lại.

Dùng cho gì

Hàng đợi hợp với: email biến động số dư, email đặt lại mật khẩu (từ bài đăng nhập), cập nhật đơn, và mọi việc chậm hoặc có thể lỗi mà khách không cần đứng chờ kết quả.

Sang việc thứ hai, khó hơn. Giả sử ví khách còn 100, và khách (hoặc hệ thống) gửi hai yêu cầu mua 80 gần như cùng lúc. Đáng lẽ lần hai phải bị từ chối vì không đủ tiền. Nhưng nếu cả hai cùng đọc số dư trước khi ai kịp ghi:

thời gian →A: đọc 100đủ → trừ 80ghi số dư = 20B: đọc 100đủ → trừ 80ghi số dư = 20Cả hai đọc 100 → cùng cho mua → số dư chỉ trừ một lần (shop mất 80đ)
Hai yêu cầu cùng đọc số dư 100, cùng thấy 'đủ', cùng trừ 80 rồi cùng ghi 20 - số dư chỉ bị trừ một lần dù khách nhận hàng hai lần.

Đây là race condition: nhiều yêu cầu đọc-rồi-ghi cùng một dữ liệu, dẫm lên nhau. Lỗi này khó bắt vì chỉ xảy ra khi đông và đúng nhịp - nhưng ở chỗ tiền thì một lần cũng đủ đau.

Cách nhẹ: không khoá trước. Cứ đọc số dư, nhưng lúc ghi thì kèm điều kiện "số dư vẫn đúng như lúc tôi đọc". Nếu ai đó đã đổi nó giữa chừng, lệnh ghi không khớp và bị huỷ - app thử lại từ đầu với số dư mới.

Khoá lạc quan - mã giả (chỉ ghi nếu số dư chưa đổi)

# đọc số dư hiện tại
so_du = doc()                       # vd 100
# CHỈ trừ nếu số dư vẫn đúng 100 lúc ghi
ok = ghi_neu_con_dung(so_du, so_du - 80)
if not ok:
    thu_lai()                       # ai đó đã đổi -> làm lại từ đầu

Hợp khi ít đụng độ: phần lớn lần ghi trót lọt, thỉnh thoảng mới phải thử lại.

Cách chắc: khoá dòng số dư ngay khi bắt đầu xử lý. Yêu cầu khác đụng tới ví đó phải chờ tới lượt, nên không thể cùng đọc số dư cũ. Xử lý xong, nhả khoá, yêu cầu kế mới vào - và nó thấy số dư đã cập nhật.

  • Optimistic: không khoá, kiểm lúc ghi, thử lại nếu lệch - nhẹ, hợp ít đụng độ.
  • Pessimistic: khoá dòng khi xử lý, request khác chờ - chắc, hợp chỗ hay đụng và rất nhạy cảm.
  • Số dư ví là tiền: ưu tiên chắc. Pessimistic lock (hoặc trừ tiền trong một transaction có khoá) là lựa chọn an toàn.

Trung thực

Đừng để Claude "tự nhiên" trừ số dư bằng đọc-rồi-ghi thường. Hãy nói rõ ràng buộc: "trừ số dư phải an toàn khi nhiều yêu cầu cùng lúc, dùng khoá để không trừ lố". Ghi luôn vào CLAUDE.md để mọi phiên đều bám.

Hai mảnh ghép lại: hàng đợi để việc nặng không bắt khách chờ, và khoá số dư để tiền không bị trừ lố. Cả hai đều ở chỗ nhạy cảm, nên trước khi mở hãy cho subagent bảo mật rà phần tiền và xử lý đồng thời.

Bước tiếp theo

Sản phẩm đã chạy được cho khách. Giờ mèo con cần một chỗ để quản lý hàng, kho, giao dịch và số dư - và phân quyền để admin tách khỏi khách thường: Trang quản trị & phân quyền Casbin.

Câu hỏi thường gặp

Là một danh sách công việc chờ xử lý. Thay vì làm ngay trong lúc khách đứng đợi, app bỏ việc (job) vào hàng đợi rồi trả lời khách ngay; một tiến trình riêng (worker) lấy job ra làm sau. Hợp với việc nặng hoặc chậm như gửi email, cập nhật đơn.

BullMQ là một thư viện hàng đợi cho Node, chạy TRÊN NỀN Redis - chính Redis mèo con đã thêm ở bài tăng tốc. Redis giữ danh sách job; BullMQ lo bỏ job vào, lấy job ra, thử lại khi lỗi. Vậy là Redis vừa làm cache vừa làm nền cho hàng đợi.

BullMQ tự THỬ LẠI (retry) job lỗi vài lần với khoảng nghỉ tăng dần - tiện cho email khi nhà cung cấp chập chờn. Quá số lần vẫn lỗi thì job bị đánh dấu thất bại (vào "dead-letter") để mèo con xem lại, thay vì mất âm thầm.

Là khi nhiều yêu cầu chạy gần như đồng thời cùng đọc-rồi-ghi một dữ liệu, dẫm lên nhau. Với số dư ví: hai lần mua cùng lúc đều đọc số dư cũ, đều thấy "đủ tiền", đều trừ - nên số dư bị trừ lố (sai). Đây là lỗi khó tái hiện vì chỉ xảy ra khi đông và đúng nhịp.

Khoá lạc quan (optimistic): không khoá trước, lúc ghi mới kiểm "số dư có còn như lúc đọc không"; khác thì huỷ và thử lại. Khoá bi quan (pessimistic): khoá dòng số dư trong lúc xử lý, yêu cầu khác phải chờ. Với tiền bạc dễ đụng độ, pessimistic chắc ăn hơn; optimistic nhẹ hơn khi ít đụng. Cứ mô tả ràng buộc cho Claude và cho subagent rà.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Vì sao gửi email biến động số dư nên đẩy vào hàng đợi?

Bài tập về nhà

  1. 1

    Cài hàng đợi BullMQ

    Nhờ Claude thêm BullMQ (trên nền Redis đã có) và tạo một hàng đợi "email" kèm một worker chạy nền.

    ✅ Hoàn thành khi: Có một queue "email" và một worker; nhờ Claude xác nhận worker đang chạy và lắng nghe job.

  2. 2

    Gửi email biến động qua queue

    Khi cộng/trừ số dư, đẩy job gửi email vào hàng đợi thay vì gửi đồng bộ trong request.

    ✅ Hoàn thành khi: Thao tác số dư trả về NGAY cho khách; email do worker gửi sau (thấy trong log của worker).

  3. 3

    Thử retry khi lỗi

    Cố ý làm job email lỗi một lần (vd cấu hình email sai tạm), quan sát BullMQ tự thử lại.

    ✅ Hoàn thành khi: Job được thử lại và cuối cùng thành công, hoặc vào dead-letter sau khi hết số lần thử - không mất âm thầm.

  4. 4

    Tạo cảnh trừ lố (chưa khoá)

    Nhờ Claude mô phỏng hai yêu cầu mua gần như đồng thời trên cùng một ví khi CHƯA có khoá.

    ✅ Hoàn thành khi: Quan sát thấy số dư bị trừ sai (trừ lố) - đúng như sơ đồ race condition trong bài.

  5. 5

    Thêm khoá số dư

    Nhờ Claude thêm khoá (optimistic hoặc pessimistic) cho thao tác trừ tiền, rồi chạy lại cảnh trên.

    ✅ Hoàn thành khi: Với khoá, một trong hai yêu cầu bị từ chối hoặc phải chờ; số dư cuối cùng đúng, không trừ lố.

  6. 6

    Cho subagent bảo mật rà

    Gọi subagent rà bảo mật (OWASP) kiểm phần trừ tiền và xử lý đồng thời.

    ✅ Hoàn thành khi: Có danh sách phát hiện; mỗi mục được sửa hoặc mèo con xác nhận lý do an toàn.