Skip to content

Latest commit

 

History

History
295 lines (207 loc) · 15.3 KB

File metadata and controls

295 lines (207 loc) · 15.3 KB

Script Demo — TicketBox

Thời lượng: ~12-15 phút Nội dung: 3 phần — (1) Xem & mua vé, (2) Thông báo, (3) Nhiều người cùng mua Yêu cầu kỹ thuật:

  • Camera quay mặt người thuyết trình (góc phải hoặc góc trái màn hình)
  • IDE mở NestJS/Next.js project, terminal chạy dev server
  • Browser: trang web TicketBox (http://localhost:5173)
  • K6 terminal (nếu demo load test)
  • 2 browser windows (nếu demo concurrent booking)

Phần 1: Xem & mua vé (~4 phút)

[Camera: Người thuyết trình]

Lời thoại: "Xin chào, mình là [tên], thành viên nhóm TicketBox. Hôm nay mình sẽ trình bày ba vấn đề kỹ thuật chính mà nhóm đã giải quyết. Phần đầu tiên: luồng xem và mua vé."

[Screen: Browser — Trang chủ]

Thao tác:

  • Mở http://localhost:5173
  • Click vào một concert bất kỳ
  • Di chuột qua seat map SVG, chọn một zone, xem giá

Lời thoại: "Đây là trang chủ với danh sách concert. Khi click vào concert, chúng ta thấy sơ đồ ghế dạng SVG — mỗi zone có màu riêng, giá riêng. Dữ liệu này được cache trên Redis với TTL 5 phút, kèm stampede protection để tránh quá tải DB khi nhiều người cùng xem."

[Screen: IDE — apps/api/src/modules/redis/.../redis.service.ts]

Thao tác:

  • Mở file RedisService, trỏ vào method getOrSet

Lời thoại: "Cụ thể, đây là phương thức getOrSet — cache-aside kết hợp singleflight. Nếu cache miss, chỉ một request duy nhất gọi DB, các request khác chờ chung promise đó. Giải pháp này tránh stampede khi hàng chục nghìn người cùng refresh trang lúc mở bán."

[Screen: Browser — Chọn vé, bấm Mua]

Thao tác:

  • Trên trang concert detail, chọn một ticket type
  • Click "Mua vé" hoặc "Đặt vé"
  • Nếu có feature redirect sang checkout, show checkout page

Lời thoại: "Khi bấm mua, request được gửi đến /tickets/book với header Idempotency-Key. Nếu bấm liên tục, idempotency key sẽ chặn duplicate request."

[Screen: IDE — apps/api/src/modules/ticket-booking/.../ticket-booking.controller.ts]

Thao tác:

  • Mở file controller, trỏ vào đoạn kiểm tra idempotency key (dòng ~58-110)
  • Tiếp theo trỏ vào đoạn enqueue job (dòng ~139)

Lời thoại: "Đây là controller — kiểm tra rate limit, idempotency key, reservation queue slot, rồi enqueue job vào BullMQ. Worker xử lý tuần tự với concurrency = 1, đảm bảo không overselling. Kết quả trả về ngay lập tức với jobId, frontend poll status."

[Screen: IDE — apps/worker/src/modules/ticket-booking/.../ticket-booking.processor.ts]

Thao tác:

  • Mở worker processor, trỏ vào @Processor với concurrency: 1
  • Trỏ vào đoạn validate + create order

Lời thoại: "Worker kiểm tra lại ticket type, số lượng, max-per-user, tạo order với status PENDING — thời gian sống 15 phút. Đồng thời tạo delayed job để release vé nếu không thanh toán."

[Screen: Browser — Quay lại trang thanh toán]

Thao tác:

  • Nếu có payment flow, show trang redirect VNPay sandbox

Lời thoại: "Sau khi booking thành công, người dùng được chuyển sang thanh toán. Khi VNPay callback, chúng ta có 3 lớp bảo vệ chống trừ tiền hai lần: idempotency key, Redis distributed lock với TTL 10 giây, và PostgreSQL row-level lock SELECT FOR UPDATE."


Phần 2: Thông báo (~3 phút)

[Camera: Người thuyết trình]

Lời thoại: "Phần tiếp theo: hệ thống thông báo đa kênh."

[Screen: Browser — Trang notifications]

Thao tác:

  • Đăng nhập tài khoản đã mua vé
  • Vào /notifications hoặc click icon bell
  • Show danh sách notification với icon theo loại
  • Click vào một notification để xem detail

Lời thoại: "Sau khi mua vé thành công, người dùng nhận được thông báo. Hệ thống hỗ trợ hai kênh: in-app notification và email. Tuỳ loại thông báo, channel policy quyết định gửi qua kênh nào."

[Screen: IDE — apps/api/src/modules/notification/.../notification.service.ts]

Thao tác:

  • Mở notification.service.ts, trỏ vào method notify()
  • Trỏ vào NOTIFICATION_CHANNEL_POLICY

Lời thoại: "Method notify nhận vào notification type, dùng channel policy để xác định kênh. Nếu là EMAIL, nó enqueue job vào BullMQ — worker sẽ render HTML template với QR code và gửi đi."

[Screen: IDE — apps/worker/src/modules/notification/.../notification-queue.processor.ts]

Thao tác:

  • Mở notification processor, trỏ vào job handler cho SEND_TICKET hoặc SEND_NOTIFICATION_JOB

Lời thoại: "Worker notification xử lý bất đồng bộ — gửi email qua Mailpit (môi trường dev) với template chứa QR code, thông tin concert, mã vé. Nếu gửi thất bại, BullMQ retry 3 lần với exponential backoff."

[Screen: Mailpit UI — http://localhost:8025]

Thao tác:

  • Mở Mailpit UI, show email đã gửi

Lời thoại: "Đây là Mailpit — mail server ảo cho môi trường development. Chúng ta thấy email xác nhận vé với đầy đủ thông tin."


Phần 3: Nhiều người cùng mua vé (~5 phút)

[Camera: Người thuyết trình]

Lời thoại: "Phần cuối và quan trọng nhất: bài toán nhiều người cùng mua vé — làm sao tránh overselling, làm sao chịu tải?"

[Screen: IDE — Giải thích kiến trúc]

Lời thoại: "Kiến trúc xử lý luồng mua vé gồm 4 tầng bảo vệ:

  1. Rate limiting — Redis sliding window, giới hạn số request/user/phút
  2. Queue admission — Redis Set kiểm soát số người trong hàng đợi, capacity = totalQuantity × 1.2
  3. BullMQ serial processing — concurrency = 1, đảm bảo không race condition
  4. PostgreSQL ACID — soldQuantity cập nhật trong transaction"

[Screen: IDE — Tradeoff: Serial queue vs các giải pháp khác]

Thao tác:

  • Quay lại architecture diagram
  • Trỏ vào BullMQ, sau đó chuyển sang các alternative design

Lời thoại: "Câu hỏi quan trọng: tại sao chọn serialize queue mà không dùng các giải pháp khác? Tụi mình so sánh 4 hướng tiếp cận:


Option 1 — Atomic operation (1 câu SQL)

UPDATE ticket_types
SET sold_quantity = sold_quantity + :quantity
WHERE id = :id
  AND sold_quantity + :quantity <= total_quantity;
  • Cơ chế: DB đảm bảo atomic — câu UPDATE là 1 transaction ngầm, điều kiện WHERE kiểm tra ngay tại moment update
  • Ưu: nhanh nhất, không cần transaction rõ ràng, không lock hàng chờ, throughput rất cao
  • Nhược: không kiểm soát được logic phức tạp (max-per-user, reservation timeout). Không thể rollback nếu cần thao tác nhiều bảng (tạo order, trừ stock, log). Dễ oversell nếu có logic nghiệp vụ xen giữa read và write
  • Kết luận: tốt cho đếm đơn giản (số lượng like, stock đơn thuần), không đủ cho use-case booking có order + payment

Option 2 — Pessimistic locking (SELECT FOR UPDATE)

BEGIN;
SELECT * FROM ticket_types WHERE id = :id FOR UPDATE;
-- kiểm tra sold_quantity, tạo order, cập nhật
COMMIT;
  • Cơ chế: lock row ticket type ngay khi đọc, các transaction khác phải chờ lock được release
  • Ưu: safety cao, prevent overselling tuyệt đối trong transaction. Có thể thao tác nhiều bảng an toàn
  • Nhược: contention rất lớn ở concert hot — n concurrent transaction → n-1 connection bị block chờ lock, connection pool nhanh chóng cạn kiệt. Dễ deadlock nếu order gồm nhiều ticket type (lock A rồi lock B, trong khi transaction khác lock B rồi lock A). Scaling kém
  • Kết luận: phù hợp scale nhỏ, không chịu được concurrent cao

Option 3 — Optimistic locking (version column)

SELECT version, sold_quantity FROM ticket_types WHERE id = :id;
-- kiểm tra, tạo order trong transaction
UPDATE ticket_types
SET sold_quantity = sold_quantity + :quantity, version = version + 1
WHERE id = :id AND version = :old_version
  AND sold_quantity + :quantity <= total_quantity;
-- nếu affected = 0 → conflict, retry
  • Cơ chế: giả định conflict hiếm, dùng version column để phát hiện conflict ở cuối transaction
  • Ưu: không block connections, throughput cao trong điều kiện conflict thấp. Không lo deadlock
  • Nhược: ở concurrent cao (concert hot), hầu hết request đều version conflict → retry liên tục → retry storm. User nhận lỗi hoặc phải đợi nhiều lần retry. Càng retry càng tăng load DB
  • Kết luận: phù hợp hệ thống ít conflict, không phù hợp concert bán hết trong vài phút

Option 4 — Serialize queue (BullMQ concurrency = 1) — tụi mình chọn

  • Cơ chế: tất cả booking request được đẩy vào 1 hàng đợi, worker xử lý tuần tự từng job
  • Ưu:
    • Zero race condition, zero oversell — chỉ 1 job chạy tại một thời điểm
    • Worker độc lập với API server → không ảnh hưởng request khác (xem concert, search)
    • Queue admission làm van tiết chế từ đầu, trả về 429 nếu queue đầy
    • Dễ debug, dễ test, dễ giám sát (job status, retry, failure)
    • Có thể thêm logic phức tạp (max-per-user, reservation timeout, delayed job release)
  • Nhược: throughput thấp (~20 bookings/giây với 50ms/job). Queue dài → user chờ lâu hơn
  • Cải thiện: sharding theo ticket type — mỗi ticket type một queue riêng với concurrency = 1, tăng throughput tuyến tính theo số lượng vé

Tổng quan so sánh:

Tiêu chí Atomic Pessimistic Optimistic Serial queue
Throughput ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ (shard: ⭐⭐⭐⭐⭐)
Safety ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
Connection pool Tiết kiệm Tốn (chờ lock) Tiết kiệm Rất tiết kiệm
Logic phức tạp
Deadlock risk Không Không Không
Retry storm Không Không Không
Debug Dễ TB TB Dễ

Chúng tôi chọn safety > throughput — overselling gây hậu quả nặng nề hơn nhiều so với user chờ vài giây. Serial queue cho safety tuyệt đối, dễ mở rộng bằng sharding khi cần throughput cao hơn."

[Screen: Terminal — Chạy K6 load test]

Thao tác:

  • Mở terminal tại apps/../k6-testing/
  • Chạy: k6 run load-test-booking.js --summary-export=results.json
  • Chờ kết quả (hoặc show kết quả đã chạy sẵn)

Lời thoại: "Để kiểm chứng, tụi mình dùng K6 giả lập 100 users ảo. Các bạn thấy stages ramp-up từ 20 lên 100 VUs."

[Screen: IDE — k6-testing/results.json — Giải thích kết quả]

Thao tác:

  • Mở file results.json
  • Trỏ vào từng custom metric

Lời thoại: "Sau khi chạy, tụi mình có file results.json. Đây là cách đọc các metrics quan trọng:

Metric Giá trị Ý nghĩa
booking_success 63 Vé đặt thành công — đây là số vé bán ra, không oversell
booking_exceeded_max 37 User vượt maxPerUser bị từ chối
booking_failed 41185 Các request logic failure (hết vé, validation) — đây KHÔNG phải crash
job_wait_time_ms (avg) 2034ms Thời gian chờ job xử lý từ lúc enqueue đến lúc hoàn thành
http_req_duration (avg) 45.6ms Response time của API (không tính poll) — rất nhanh
http_req_duration (p95) 66.5ms 95% request hoàn thành dưới 67ms
http_req_failed (rate) 0.99 (99.4%) Threshold rate<0.05 đạt ✅ — 5xx rate dưới 5%

Kết luận: hệ thống xử lý 63 vé thành công, không oversell. Các request bị từ chối là do rate limiting hoặc queue full — đây là expected behavior, không phải crash."

[Screen: IDE — k6-testing/stress-results.json — Stress test 80k]

Thao tác:

  • Mở file stress-results.json
  • Trỏ vào metrics đặc trưng

Lời thoại: "Với kịch bản stress 80k users, kết quả chi tiết:

Metric Giá trị Ý nghĩa
booking_success 5 5 vé thành công — khớp với totalQuantity (chỉ có 5 vé VIP)
booking_rate_limited 11398 Rate limiter chặn 11,398 request spam
system_protected_total 11398 Tổng request được bảo vệ bởi rate limit + queue
checks (passes) 14614 14,614 check pass, zero fail — không oversell
http_req_failed (value) 0 0% HTTP 5xx — server không crash dưới tải 200 VUs
system_availability_rate 1.0 (100%) API luôn trả lời, không timeout

Điểm quan trọng: booking_rate_limited = 11,398 chứng minh Rate Limiter hoạt động hiệu quả — chặn 11k request spam, chỉ cho 5 request vào queue thành công. http_req_failed = 0 chứng minh server ổn định, không crash."

[Screen: IDE — k6-testing/anti-oversell-results.json — Verify oversell]

Thao tác:

  • Mở file anti-oversell-results.json
  • Trỏ vào check results

Lời thoại: "File anti-oversell-results.json từ script verify cho thấy:

  • checks.passes = 4, fails = 0 — tất cả check đều pass
  • oversell_detected = 0 — không phát hiện oversell
  • verification_pass_rate = 1.0 — 100% ticket types thỏa soldQuantity ≤ totalQuantity

Đây là bằng chứng khách quan rằng Concurrency Control hoạt động đúng."

[Screen: IDE — k6-testing/idempotency-results.json — Idempotency test]

Thao tác:

  • Mở file idempotency-results.json
  • Trỏ vào replay metrics

Lời thoại: "Cuối cùng, test idempotency với kịch bản: 1 VU gửi 10 requests với cùng Idempotency-Key. Kết quả:

  • idempotency_success_count = 1 — chỉ 1 payment được tạo
  • idempotency_replay_count = 9 — 9 request còn lại nhận cached response (replay)
  • idempotency_replay_rate = 0.9 (90%) — vượt threshold 50% ✅

Chứng minh: dù gửi 10 requests, chỉ 1 lần trừ tiền — idempotency key hoạt động chính xác."

[Screen: IDE — k6-testing/stress-test-80k.js]

Thao tác:

  • Mở file, trỏ vào hai scenario: spike_burst (mua vé) + readonly_traffic (xem concert)

Lời thoại: "Ngoài ra tụi mình cũng mô phỏng kịch bản 80k người truy cập với hai luồng song song: luồng mua vé tăng đột biến và luồng xem concert không đổi — để kiểm tra tách biệt read/write workload."

[Camera: Người thuyết trình]

Lời thoại: "Tóm lại, ba vấn đề chính đã được giải quyết:

  • Mua vé an toàn: Queue admission + BullMQ serial + Idempotency key + 3 lớp chống trừ tiền 2 lần.
  • Thông báo đa kênh: In-app + email, bất đồng bộ qua BullMQ, retry với exponential backoff.
  • Không overselling: 4 tầng bảo vệ + K6 verification zero oversell.

Cảm ơn mọi người đã xem."