Trong bài viết Thiết kế Hệ thống Flash Sale, chúng ta đã nhắc đến viễn cảnh: 1.000 chiếc iPhone giá sốc và 2 triệu người dùng cùng lúc bấm "Mua ngay". Để hệ thống không sập, chúng ta chuyển "chiến trường" từ Database truyền thống sang Redis In-Memory Cache.
Tuy nhiên, giữ cho hệ thống không sập chỉ là một nửa bài toán. Nửa còn lại là: Làm sao để chắc chắn bạn không bán ra 1.001 chiếc iPhone khi trong kho chỉ có 1.000?.
Lỗi bán vượt số lượng (Overselling) không chỉ là một lỗi kỹ thuật, nó là "thảm họa" kinh doanh. Nếu bán vượt 1.000 đơn vị, bạn sẽ đối mặt với:
Financial Loss: Phí xử lý giao dịch cổng thanh toán (Payment Gateway fees) không được hoàn lại (thường là 1.5% - 3% cho 1.000 đơn hàng hỏng).
Operation Cost: Đội ngũ CSKH phải gọi điện xin lỗi và xử lý Refund thủ công cho 1.000 khách hàng.
Brand Damage: Tỷ lệ đánh giá 1-sao trên App Store tăng đột biến từ sự phẫn nộ của người dùng, kéo Rating từ 4.8 xuống 3.2 chỉ sau 1 đêm.
Bài viết này sẽ đi sâu vào "Race Condition" và cách sử dụng các Atomic Operations (Thao tác nguyên tử) để giải quyết bài toán này.
Bản Chất Của Sự Cố: Race Condition Trong Chu Trình Read-Modify-Write
Anti-Pattern: Giải pháp ngây ngô của Newbie
Newbie thường đề xuất: "Khi tồn kho về 0, báo Frontend làm mờ (disable) nút Mua Hàng là xong."
Thực tế: Trong Flash Sale, hàng chục ngàn API requests được bắn trực tiếp (bypass Frontend hoàn toàn) thông qua bot hoặc cURL. Frontend không có giá trị bảo vệ dữ liệu.
Hãy tưởng tượng logic trừ tồn kho cơ bản nhất dưới Backend:
[User A] và [User B] cùng gửi request mua sản phẩm lúc 00:00:01.
User A đọc DB: Tồn kho = 1.
User B đọc DB: Tồn kho = 1.
User A tính toán: 1 - 1 = 0. Ghi vào DB: Tồn kho = 0.
User B tính toán: 1 - 1 = 0. Ghi vào DB: Tồn kho = 0.
Kết quả: Cả A và B đều nhận thông báo mua thành công, nhưng tồn kho chỉ trừ đi 1. Hệ thống đã Oversell!.
Đây gọi là Race Condition (Điều kiện tương tranh). Để giải quyết, chúng ta cần đảm bảo chu trình Read-Modify-Write này là một Atomic Operation – một khối thống nhất, không thể bị chia cắt hay chen ngang bởi các request khác.
4 Cấp Độ Giải Quyết Bài Toán Overselling
Cấp Độ 1: Pessimistic Locking (Khóa bi quan tại RDBMS)
Đây là phương pháp truyền thống nhất. RDBMS (như MySQL/PostgreSQL) cung cấp tính năng Row-level Lock (Khóa cấp độ hàng).
Bằng lệnh SELECT ... FOR UPDATE, hệ thống sẽ "khóa" dòng dữ liệu tồn kho lại.
Các request đến sau bắt buộc phải xếp hàng chờ request trước hoàn thành giao dịch (Commit/Rollback) thì mới được chạm vào dữ liệu.
Trade-off: Đổi lấy độ an toàn tuyệt đối là một hiệu năng thảm họa. Trong Flash Sale, hàng triệu request xếp hàng chờ đợi sẽ làm cạn kiệt Connection Pool, đẩy Latency lên mức Timeout và gây ra hiệu ứng Domino làm sập toàn bộ hệ thống (Cascading Failure).
Cấp Độ 2: Optimistic Locking (Khóa lạc quan bằng Versioning)
Thay vì khóa Database từ đầu, ta thêm một cột version vào bảng tồn kho.
Logic cập nhật sẽ trở thành: UPDATE inventory SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = [version_đã_đọc].
Nếu User A và User B cùng đọc version = 5, User A update thành công (version thành 6). Khi User B thực thi lệnh UPDATE, điều kiện version = 5 không còn đúng, thao tác sẽ thất bại (reject) và User B phải thử lại.
Trade-off: Không gây nghẽn Database vì không có khóa chặn. Tuy nhiên, trong Flash Sale với hàng trăm ngàn lượt truy cập đồng thời, tỷ lệ reject sẽ cực kỳ khổng lồ. Điều này gây lãng phí tài nguyên CPU khổng lồ chỉ để xử lý việc tính toán và retry liên tục các transaction thất bại.
Cấp Độ 3: Redis DECR (Bộ đệm In-Memory)
Đây là cách các hệ thống lớn giảm tải cho Database: Đưa tồn kho lên Redis. Lệnh DECR (Decrement) của Redis có bản chất là đơn luồng (single-threaded) và nguyên tử. Tại một thời điểm, chỉ có 1 request được phép trừ đi 1 đơn vị.
Lỗ hổng chết người:Lệnh DECR thuần túy rất nhanh, nhưng nếu nó trả về -1, bạn đã vô tình trừ lạm vào kho (số âm). Bạn phải gọi ngay lệnh INCR để cộng hoàn lại. Việc có thêm một lượt round-trip qua mạng lưới (gọi lại Redis) sẽ phát sinh rủi ro: Nếu mạng chập chờn hoặc Application crash ngay trước khi gọi INCR, kho của bạn sẽ bị sai lệch mãi mãi.
Cấp Độ 4: Chân Ái Của High-Concurrency – Redis + Lua Script
Để giải quyết triệt để điểm yếu của DECR, chúng ta đẩy logiccheck tồn kho -> nếu đủ -> thì trừ kho xuống chạy trực tiếp bên trong Redis server thông qua Lua Script.
Lua Script cho phép bạn gói gọn toàn bộ logic nhiều bước thành một atomic unit duy nhất, chạy trên Redis server và không thể bị interrupt.
Bằng cách này, chúng ta không cần lệnh INCR để hoàn số, không mất thời gian round-trip network nhiều lần, và loại bỏ hoàn toàn Oversell ở tầng Application.
Phân Tích Rủi Ro Với PEUF Framework
Để thiết kế hệ thống Flash Sale thực sự vững chãi, PM/TPM cần nhìn vượt ra khỏi "Race condition" thuần túy và áp dụng PEUF Framework:
P (Permission - Quyền hạn): Bot net có thể bypass hàng đợi (Queue) để gọi thẳng vào API Redis Lua không? *Cách xử lý:* Yêu cầu một Cryptographic Token một lần (One-time use) được cấp bởi hệ thống Queue trước khi gọi API thanh toán.
E (Empty/Extreme - Khắc nghiệt):Điều gì xảy ra khi kho vừa đúng chạm 0, nhưng có 100,000 requests đồng loạt ập vào Node Redis đó? Cách xử lý: Redis có thể xử lý ~100k ops/sec, nhưng để an toàn, dùng Local Cache ở tầng Application (ví dụ Guava cache) để chặn ngay lập tức toàn bộ request khi nhận được tín hiệu "Sold Out" đầu tiên từ Redis.
U (Unavailability - Bất khả dụng): Node Redis chứa kho hàng bị sập vật lý (Crash) ngay giữa phiên Flash Sale. Cách xử lý: Redis Cluster setup với Master-Slave replication. Tuy nhiên, lưu ý rủi ro mất dữ liệu nhỏ do replication của Redis là bất đồng bộ (Asynchronous).
F (Fraud - Gian lận):1 User dùng tool gửi 50 requests đồng thời bằng các tài khoản clone. Redis Lua Script xử lý trừ kho cực tốt, nhưng làm sao ngăn 1 người ôm hết hàng? Cách xử lý: Tích hợp logic SETNX (Set if Not eXists) khóa User ID vào chính Lua Script để đảm bảo mỗi người dùng chỉ mua được 1 sản phẩm.
Bảng So Sánh Chiến Lược Nâng Cấp
Chiến lược
Môi trường
Nguy cơ Oversell
Bottleneck & Hiệu năng
Use-case Thực Tế Điển Hình
Pessimistic Lock
Database
0%
Rất thấp (Dễ treo DB)
Sản phẩm B2B giới hạn số lượng cấp phát, giao dịch nhà đất/ô tô (Traffic thấp, cần chính xác tuyệt đối).
Optimistic Lock
Database
0%
Trung bình (Tốn CPU cho việc Retry)
Nền tảng bán vé sự kiện nhỏ/Workshop, khóa học online. Tần suất tranh chấp thấp.
Redis DECR
Cache
Có (Nếu Rollback INCR thất bại)
Rất cao
Lượt xem Video, số lượt Like, giới hạn API Rate Limiting cơ bản (Chấp nhận sai số nhỏ).
Redis + Lua Script
Cache
0%
Tuyệt đối (Rất cao)
Flash Sale iPhone, vé xem concert BlackPink, săn mã giảm giá giờ vàng của Shopee/Lazada.
Tổng Kết
Trong System Design, không bao giờ có giải pháp hoàn hảo tuyệt đối, chỉ có giải pháp với Trade-off phù hợp với mục tiêu kinh doanh. Nếu bạn xây dựng một nền tảng bán vé nhỏ, Optimistic Locking ở cấp độ DB là quá đủ. Nhưng nếu bạn là một TPM chịu trách nhiệm cho chiến dịch Mega Campaign với hàng triệu đô la GMV đối mặt nguy cơ bốc hơi vì Refund, sự kết hợp giữa Redis Cache và Lua Script là tiêu chuẩn kỹ thuật bắt buộc phải nằm lòng.
System ConceptApr 20, 2026
Database Architecture 4: Sharding: Phá Vỡ Giới Hạn Vật Lý & Nỗi Đau Vận Hành
Khi hệ thống chạm ngưỡng giới hạn Ghi (Write limit), Replication trở nên vô dụng. Khám phá chiến lược chọn Shard Key, cách né thảm họa Hotspot và những mặt tối đắt đỏ của Sharding.