[System Design] Bóc trần câu hỏi phỏng vấn ngàn đô: Xử lý bài toán Flash Sale hàng triệu User không sập DB
Chào anh em, lại là mình đây.
Lướt mạng thấy cái ảnh chế câu hỏi phỏng vấn này quen quá, chắc hẳn anh em làm backend nào cũng từng giật mình thon thót khi nghĩ đến cảnh hệ thống của mình phải gánh một đợt Flash Sale.
"Vào ngày siêu sale, có hàng triệu user cùng truy cập vào một món hàng. Làm sao hệ thống có thể hiển thị chính xác trạng thái: 'Chỉ còn 2 sản phẩm trong kho' theo thời gian thực (Real-time) mà không làm sập Database?"
Thực ra bài toán này không chỉ áp dụng cho mảng e-commerce bán đồ điện tử hay mỹ phẩm, mà anh em nào đang dựng mấy hệ thống đặt vé xem phim (tranh nhau book một cái ghế trống) thì logic cũng y chang. Hôm nay, chúng ta cùng mổ xẻ kiến trúc để giải quyết bài toán "nhức nách" này nhé.
1. Tại sao Database truyền thống lại "hẹo"?
Tưởng tượng 12h đêm mùng 9/9, 1 triệu anh em cùng F5 cháy máy để săn con iPhone 15 giá 1K.
Nếu anh em code theo kiểu ngây thơ:
Nếu anh em code theo kiểu ngây thơ:
SELECT stock FROM products WHERE id = 1 để lấy số lượng, và UPDATE products SET stock = stock - 1 khi có người mua.
Điều gì sẽ xảy ra?
- Quá tải Connection: MySQL/PostgreSQL chỉ chịu được vài ngàn connection đồng thời là kịch kim. 1 triệu request ập vào là bung bét pool connection.
- Row-level Lock: Khi một transaction đang trừ đi số lượng của con iPhone đó, nó sẽ khóa (lock) cái dòng (row) đó lại. 999.999 request còn lại phải xếp hàng chờ. Kết quả là Deadlock, CPU 100%, RAM cạn kiệt, và server lăn ra chết.
2. Giải pháp Tầng 1: Đưa Redis ra làm "Khiên đỡ đạn"
Nguyên tắc vàng trong System Design: Đừng bao giờ để lượng traffic khổng lồ chạm trực tiếp vào Database vật lý.
Chúng ta phải đưa dữ liệu tồn kho (stock) lên bộ nhớ đệm (Cache) in-memory, và Redis chính là chân ái. Redis chạy trên RAM, lại xử lý đơn luồng cực kỳ tối ưu, nó có thể gánh hàng trăm ngàn đến cả triệu phép toán đọc/ghi mỗi giây
Cách hoạt động:
- Trước giờ G (giờ bắt đầu sale), hệ thống sẽ load số lượng tồn kho từ MySQL/PostgreSQL lên Redis. Ví dụ: SET
iphone_stock_1 100. - Khi có người ấn nút "Mua ngay", thay vì chọc xuống DB, chúng ta dùng lệnh
DECR(Decrement) của Redis:DECR iphone_stock_1. - Vì
DECRlà một lệnh Atomic (nguyên tử) trong Redis, nên dù có 10.000 người cùng ấn mua trong một phần nghìn giây, Redis vẫn trừ tuần tự một cách chính xác tuyệt đối. Ai trừ xuống bị âm (< 0) thì hệ thống báo "Hết hàng", đá văng ra ngoài. Không bao giờ lo bị bán lố (Overselling).
3. Giải pháp Tầng 2: Bơm Real-time về Client như thế nào?
Ok, Redis đã xử lý xong vụ đếm số. Nhưng làm sao để 1 triệu cái điện thoại đang mở app tự nhiên nhảy số từ "Còn 3 sản phẩm" xuống "Chỉ còn 2 sản phẩm" mà user không cần vuốt màn hình load lại?
Nhiều anh em xài Short Polling (cứ 1 giây cho Frontend gọi API lên hỏi server 1 lần). Cách này cực kỳ ngu ngốc vì nó tạo ra cả tỷ request rác, giết chết server web.
Cách giải quyết chuẩn chỉ:
- Sử dụng WebSockets hoặc SSE (Server-Sent Events).
- Khi giá trị
iphone_stock_1trong Redis bị thay đổi (từ 3 xuống 2), Backend sẽ bắt sự kiện này và broadcast (phát thanh) một thông điệp nhỏ giọt qua kênh WebSocket tới tất cả những user đang Subscribe (theo dõi) cái sản phẩm đó. - Nhờ đó, Frontend nhận được tín hiệu và tự động update con số trên màn hình ngay lập tức. Cảm giác rất "phép thuật" và real-time.
4. Giải pháp Tầng 3: Đồng bộ về Database (Backstage)
Đến đây, kho trên Redis đã trừ xong, user đã thấy thông báo hết hàng. Nhưng DB vật lý (PostgreSQL/MySQL) của anh em vẫn đang ghi tồn kho là 100. Redis lỡ bị cúp điện sập nguồn là bay sạch dữ liệu. Làm sao để ghi xuống DB mà không làm nó chết ngợp?
Đây là lúc chúng ta mang Message Queue (như Apache Kafka) vào cuộc.
- Khi Redis xác nhận đơn hàng thành công (số lượng > 0), Backend sẽ không chọc xuống DB ngay, mà gói cái thông tin đơn hàng đó ném vào một Topic của Kafka.
- Trả response về cho User ngay lập tức: "Chúc mừng bạn đã đặt hàng thành công". (User vui vẻ đi ngủ).
- Ở dưới nền (background), chúng ta sẽ có một cụm Worker (có thể viết bằng Golang cho tốc độ xử lý nhanh, hoặc dùng hàng đợi của Laravel). Các worker này sẽ từ từ "nhẩn nha" bốc từng đơn hàng từ Kafka ra, và thực hiện câu lệnh UPDATE hoặc INSERT xuống DB vật lý.
- Kafka đóng vai trò như một hồ chứa, giảm xóc cực tốt. Cho dù có 100.000 đơn hàng đổ về cùng lúc, DB vẫn chỉ ung dung xử lý với tốc độ an toàn của nó (ví dụ 1000 đơn/giây). Cái này gọi là cơ chế Eventual Consistency (Nhất quán cuối).
Chốt hạ
Tóm lại, để trả lời ăn điểm tuyệt đối cho câu hỏi trên, anh em cứ vạch ra luồng này cho người phỏng vấn:
- Tránh ghi/đọc trực tiếp vào RDBMS, dùng Redis lưu tồn kho và xài lệnh DECR để chống over-sell.
- Đẩy data real-time về client thông qua WebSockets/SSE.
- Dùng Kafka (hoặc RabbitMQ) làm Message Queue để hứng event mua hàng, giải phóng request cho user nhanh nhất có thể.
- Worker xử lý bất đồng bộ từ Queue xuống Database để đảm bảo an toàn cho hệ thống.
Kiến trúc này là xương sống của mọi hệ thống chịu tải cao, từ săn sale e-commerce cho đến book vé xem phim rạp. Nắm vững cái này thì anh em tự tin deal lương nghìn đô nhé.
Anh em có cách nào tối ưu hơn hay từng dính "phốt" sập DB ngày sale chưa? Comment chia sẻ bên dưới nhé! Thấy hay thì cho xin 1 upvote lấy động lực gõ phím nha!
All rights reserved