Đặc thù bài này: tải không đều — bình thường vài trăm request/giây, đúng giờ mở bán vọt lên hàng chục nghìn, và tồn kho là tài nguyên hữu hạn không được bán vượt.
Bước 1 — chặn tải trước khi vào lõi. Trang chi tiết sự kiện là tĩnh, đẩy hết lên CDN. Đặt hàng đợi ảo (waiting room) hoặc token vào phiên mua trước cổng API, phần còn lại nhận 429 kèm Retry-After thay vì để chúng dồn xuống DB.
Bước 2 — giữ chỗ, không bán ngay. Luồng ba trạng thái: hold (TTL 5-10 phút) -> paid -> ticketed. Trừ tồn kho ngay lúc hold để không có hai người cùng thanh toán một ghế.
-- Redis script: atomic decrement, no oversell
local left = tonumber(redis.call('GET', KEYS[1]))
if left and left >= tonumber(ARGV[1]) then
return redis.call('DECRBY', KEYS[1], ARGV[1])
end
return -1Redis giữ bộ đếm tồn kho, script Lua chạy nguyên tử nên không có race. DB vẫn là nguồn sự thật cuối: worker ghi holds xuống Postgres và job dọn hold hết hạn trả lại tồn kho.
Ghế có sơ đồ chỗ ngồi thì bộ đếm không đủ — cần khoá theo từng ghế (SET seat:{id} NX EX 600) hoặc SELECT ... FOR UPDATE SKIP LOCKED khi cấp ghế theo lô.
Điểm nghẽn:
- Hot key: một sự kiện = một key Redis. Nếu quá nóng, chia tồn kho thành N bucket (stock:{event}:{0..9}), client chọn ngẫu nhiên bucket; hết bucket mới dò bucket khác.
- Cổng thanh toán chậm hơn hệ thống của mình nhiều lần → thanh toán bất đồng bộ, khách nhận kết quả qua webhook/polling; đừng giữ HTTP request chờ.
Đánh đổi: trừ tồn kho lúc hold có thể "giam" vé của người bỏ giữa chừng trong vài phút (bán ít hơn khả năng), nhưng đổi lại không bao giờ oversell — với vé sự kiện, oversell là chi phí pháp lý, còn giam vé chỉ là chi phí cơ hội.