Xử lý Race Condition khi trừ tồn kho trong Flash Sale bằng Redis Lua Script
Thảm họa bán vượt số lượng Overselling trong Flash Sale Hãy tưởng tượng bạn có một sản phẩm chỉ còn 5 chiếc trong kho, nhưng vào đúng 00:00 có tới 10.000 người dùng cùng bấm nút "Đặt mua" trong cùng …
☰ Mục lục bài viết (5 phần) Mở / Đóng mục lục
Thảm họa bán vượt số lượng (Overselling) trong Flash Sale#
Hãy tưởng tượng bạn có một sản phẩm chỉ còn 5 chiếc trong kho, nhưng vào đúng 00:00 có tới 10.000 người dùng cùng bấm nút "Đặt mua" trong cùng một giây.
Nếu bạn viết code theo cách thông thường:
// LỖI KINH ĐIỂN GÂY RACE CONDITION:
const stock = await db.query('SELECT stock FROM products WHERE id = $1', [productId]);
if (stock > 0) {
await db.query('UPDATE products SET stock = stock - 1 WHERE id = $1', [productId]);
// Tạo đơn hàng...
}
Hàng trăm tiến trình chạy đồng thời sẽ cùng đọc được stock = 5 trước khi kịp trừ, dẫn tới kết quả là 50 đơn hàng được tạo thành công dù trong kho chỉ có 5 món hàng!
Giải pháp 1: Khóa bi quan ở tầng Database (Pessimistic Locking)#
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
- Cách này giải quyết được đúng tính đúng đắn, nhưng sẽ làm khóa chặt hàng (row lock) và làm sập connection pool của database khi hàng ngàn request đổ dồn vào một dòng duy nhất.
Giải pháp 2: Nguyên tử hóa (Atomicity) bằng Redis Lua Script#
Redis là môi trường đơn luồng (Single-threaded). Khi bạn thực thi một script Lua, Redis đảm bảo đoạn script đó chạy từ đầu đến cuối một cách nguyên tử (atomic), không có bất kỳ lệnh nào khác xen ngang.
-- Script Lua trừ tồn kho an toàn trong Redis (deduct_stock.lua)
local stock_key = KEYS[1]
local quantity_to_buy = tonumber(ARGV[1])
local current_stock = tonumber(redis.call('get', stock_key) or "0")
if current_stock >= quantity_to_buy then
redis.call('decrby', stock_key, quantity_to_buy)
return 1 -- Thành công
else
return 0 -- Hết hàng
end
Tích hợp vào Node.js backend:
import Redis from 'ioredis';
import fs from 'node:fs';
const redis = new Redis();
const luaScript = fs.readFileSync('deduct_stock.lua', 'utf-8');
export async function buyFlashSaleProduct(productId: string, quantity: number) {
const stockKey = `product:stock:${productId}`;
// Gọi script Lua nguyên tử
const result = await redis.eval(luaScript, 1, stockKey, quantity);
if (result === 1) {
// Đẩy message vào Kafka / RabbitMQ để ghi database bất đồng bộ
await messageQueue.publish('order_created', { productId, quantity });
return { success: true, message: 'Đặt hàng thành công!' };
} else {
return { success: false, message: 'Sản phẩm đã hết hàng trong kho.' };
}
}
Kết quả kiểm thử tải (Load Test 10.000 RPS)#
- Không có bất kỳ đơn hàng nào bị bán vượt quá số lượng (Zero Overselling).
- Database chính hoàn toàn không bị áp lực vì chỉ nhận ghi đơn bất đồng bộ từ Message Queue.
- Latency trung bình của mỗi thao tác trừ kho qua Redis chỉ mất 1.2ms.
Góc thảo luận#
Anh em xử lý bài toán Flash Sale trong dự án thực tế bằng cách nào? Có ai dùng Optimistic Locking với cột version trong database không?
Xem thêm nội dung liên quan:
Thảo luận
1 phản hồi
Tham gia thảo luận
Bài liên quan trong #Backend & Microservices
-
Áp dụng Model Context Protocol (MCP) của Anthropic để kết nối AI với Postgres và Docker
Hoàng Nam (Tech Lead) · 💬 1 -
So sánh mô hình Embedding đa ngữ: OpenAI text-embedding-3 vs BGE-M3 vs Cohere
Minh Trí (AI Engineer) · 💬 1 -
Xây dựng Voice AI Agent thời gian thực với WebRTC và Whisper Live: Độ trễ dưới 500ms
Lan Anh (Senior Frontend) · 💬 1 -
Cấu hình Rate Limiting ở tầng Nginx và API Gateway: Chống DDOS Layer 7 hiệu quả
Đức Huy (DevOps Specialist) · 💬 1 -
Tokenization tiếng Việt của các LLM: Tại sao chi phí tiếng Việt đắt gấp 3 lần tiếng Anh?
Minh Trí (AI Engineer) · 💬 1