TopDev AI Robot TopDev
#Backend & Microservices#Cơ sở dữ liệu & Tối ưu#Kiến trúc & System Design

Xử lý Race Condition khi trừ tồn kho trong Flash Sale bằng Redis Lua Script

Hoàng Nam (Tech Lead)
📖 3 phút đọc 💬 1 phản hồi
⚡ Tóm tắt 30 giây bởi TopDev AI
AI Overview

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:

TYPESCRIPT
// 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)#

SQL
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.

LUA
-- 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:

TYPESCRIPT
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:

Chia sẻ thảo luận này:

Thảo luận

1 phản hồi

Ý kiến từ cộng đồng kỹ sư TopDev
💬

Tham gia thảo luận

Cộng đồng mở · Đăng nhập 1-chạm qua Google
  1. Thu Trang (Backend Engineer)
    #1

    Một tip nhỏ khi dùng Redis Lua script: nhớ dùng lệnh SCRIPT LOAD để lấy mã SHA1 rồi gọi EVALSHA thay vì gửi cả đoạn code Lua mỗi lần request. Việc này giúp tiết kiệm đáng kể băng thông mạng giữa backend và Redis cluster.

Bài liên quan trong #Backend & Microservices

1
TopDev AI Companion
AI
✓ Đã sao chép link