Core Characteristics of Seckill Systems
-
Extreme Concurrent Load: Seckill events generate massive instantaneous traffic, often reaching hundreds of thousands or millions of QPS. Traditional databases typically support only thousands of concurrent connections, whereas a single Redis node can handle tens of thousands. This makes Redis suitable for processing the bulk of requests, with only a small subset proceeding to the database layer.
-
Read-Intensive, Write-Sparse: The scenario involves a vast number of users competing for a limited inventory. Consequently, read operations (checking product details and stock) far outnumber successful write operations (placing orders). Queries are typically simple key-value lookups based on product IDs, which Redis handles efficiently.
Managing Enventory Synchronization
Product inventory is stored entirely in Redis. Stock levels are read directly from Redis. During the order placement phase, inventory deduction occurs directly in Redis. The deduction event is then published to a message queue for asynchronous synchronization to the backend database, thereby preventing instantaneous pressure on the database.
Key-Value Structure for Inventory
- Key: Product ID.
- Value: A string or hash, e.g.,
{"total": 1000, "reserved": 50}.totalrepresents the total available stock, andreservedindicates the quantity already claimed.
Ensuring Atomic Inventory Operations
Using Lua Scripts
Inventory verification and deduction are combined into a single, atomic Lua script executed on the Redis server.
-- Retrieve current inventory data
local stockData = redis.call("HMGET", KEYS[1], "total", "reserved");
local totalStock = tonumber(stockData[1])
local reservedStock = tonumber(stockData[2])
local requestQty = tonumber(ARGV[1])
-- Check if requested quantity is available
if reservedStock + requestQty <= totalStock then
-- Increment the reserved count
redis.call("HINCRBY", KEYS[1], "reserved", requestQty)
return requestQty -- Success
end
return 0 -- Failure, insufficient stock
The client checks the script's return value: a positive number indicates success, while 0 indicates failure.
Using Distributed Locks
A Redis-based lock can also serialize access to the inventory check-and-update block.
import redis
redis_client = redis.Redis()
product_key = "item_123"
client_id = "unique_client_identifier"
requested_qty = 1
lock_timeout = 10 # seconds
def acquire_lock(key, value, timeout):
return redis_client.set(key, value, nx=True, ex=timeout)
def release_lock(key, value):
# Use Lua to ensure only the lock owner releases it
script = """
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
"""
redis_client.eval(script, 1, key, value)
# Attempt to acquire the lock
if acquire_lock(product_key, client_id, lock_timeout):
try:
# Perform inventory check and deduction under the lock
current_reserved = redis_client.hget(product_key, "reserved")
total = redis_client.hget(product_key, "total")
if int(current_reserved) + requested_qty <= int(total):
redis_client.hincrby(product_key, "reserved", requested_qty)
# Proceed with order processing...
else:
# Handle insufficient stock
pass
finally:
release_lock(product_key, client_id)
else:
# Failed to acquire lock, handle retry or failure
pass
Consistency of Inventory Deduction
The design prioritizes Redis deduction, followed by asynchronous DB sync via message queue, achieving eventual consistency. However, edge cases must be handled:
-
Redis Deduction Succeeds, Message Queue Fails:
- Option A: Roll back the Redis deduction and report order failure.
- Option B: Treat the operation as a failure, potentially resulting in slightly fewer sales (under-selling).
-
System Crash After Successful Deduction & Message Send:
- The calling service can invoke a "restore stock" API. This API should use a unique trensaction ID (passed during both deduction and restoration attempts) to determine if restoration is necessary, preventing double-counting.
Restoring Inventory
The restore operation should first update the database to ensure inventory accuracy, then synchronize the change back to Redis. Since restorations are rare in seckill scenarios, direct DB writes are acceptable. If the sync to Redis fails later, it only causes minor under-selling, which is tolerable.
A race condition can occur where a restore request arrives before its corresponding deduction request (which is still in the async queue). The system should temporarily store such restore requests and retry them periodically. If a restore cannot be finalized after a significant period (e.g., one hour), an alert should trigger for manual intervention.
Inventory Monitoring
After a seckill event concludes, compare the actual number of successful orders with the total inventory deducted in Redis. This monitoring helps quickly identify inconsistencies, especially critical over-selling issues.