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) là cơn ác mộng lớn nhất về mặt vận hành và chăm sóc khách hàng. 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ử) từ Database đến Cache để 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
Hãy tưởng tượng logic trừ tồn kho cơ bản nhất mà một Junior Developer thường viết:
[User A] & [User B] cùng mua 1 sản phẩm lúc 00:00:01
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 mua thành công, nhưng tồn kho chỉ trừ đi 1. Hệ thống đã Oversell!
Đây được 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)
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: Độ an toàn tuyệt đối, nhưng đánh đổi bằng hiệu năng thảm họa. Trong Flash Sale, hàng triệu request chờ đợi nhau sẽ làm cạn kiệt Connection Pool, đẩy latency lên cao 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, 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. Tuy nhiên, trong sự kiện 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ồ, gây lãng phí tài nguyên CPU cho việc tính toán và retry liên tục.
Cấp độ 3: Redis DECR (Bộ đệm In-Memory)
Đây là cách Shopee hay Amazon giảm tải cho Database. Tồn kho được đưa 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ử. Nghĩa là tại một thời điểm, chỉ có 1 request được phép trừ đi 1 đơn vị.
Vấn đề: 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 để hoàn lại. Việc có thêm một lượt round-trip (gọi lại Redis) sẽ phát sinh rủi ro: Nếu mạng chập chờn hoặc App crash 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 logic check tồn kho -> nếu đủ -> thì trừ kho xuống chạy trực tiếp bên trong Redis bằng Lua Script.
Lua Script cho phép bạn gói gọn toàn bộ logic phức tạp thành một atomic unit duy nhất chạy trên Redis server, không thể bị interrupt.
Flow của Lua Script (Thực thi hoàn toàn trên Redis)
↓ Request chạm Redis
[Lua Script bắt đầu]
IF (GET stock > 0):
DECR stock
RETURN "SUCCESS"
ELSE:
RETURN "FAILED"
[Lua Script kết thúc]
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.
Bảng So Sánh Chiến Lược
Chiến lược
Môi trường thực thi
Nguy cơ Oversell
Hiệu năng / Concurrency
Khi nào nên dùng?
Pessimistic Lock
Database
0%
Rất thấp (Dễ treo DB)
Sản phẩm B2B giá trị cực cao, traffic thấp.
Optimistic Lock
Database
0%
Trung bình (Nhiều Retry)
Traffic vừa phải, tần suất tranh chấp thấp.
Redis DECR
Cache
Có (Nếu xử lý rollback lỗi)
Rất cao
Hệ thống đơn giản, chấp nhận sai số nhỏ.
Redis Lua Script
Cache
0%
Rất cao
Flash Sale, Mega Campaign traffic khổng lồ.
Tổng kết
Trong System Design, không có giải pháp hoàn hảo, chỉ có giải pháp phù hợp với bài toán. Nếu bạn xây dựng một nền tảng bán vé sự kiện nhỏ, Optimistic Locking là đủ. Nhưng nếu bạn là một TPM đối mặt với bài toán Flash Sale Mega Campaign, sự kết hợp giữa Redis Cache và Lua Script là tiêu chuẩn ngành bắt buộc phải nắm vững.
Case StudyApr 11, 2026
Bài Toán "Chấm Xanh" Của Slack: Cơn Ác Mộng Kiến Trúc Thời Gian Thực
Việc hiển thị trạng thái online tưởng chừng cơ bản nhưng lại là bài toán hạ tầng khổng lồ ở quy mô lớn. Phân tích cách Slack cấu trúc lại mô hình Pub/Sub để cân bằng giữa độ chính xác realtime và chi phí máy chủ.