Redis + Lua 解决库存超卖问题 —— Python 实现完整教程
适用场景:秒杀、抢购、优惠券领取等高并发库存扣减场景
技术栈:Redis + Lua 脚本 + Python(redis-py)
目标:防止超卖、保证库存一致性、高性能处理并发请求
一、什么是“超卖”问题?
在高并发场景中,多个用户同时抢购同一商品,若不加控制,可能出现以下情况:
用户A:查询库存=1 → 扣减库存 → 库存=-1 ❌
用户B:查询库存=1 → 扣减库存 → 库存=-1 ❌
虽然初始库存为 1,但两个用户都成功下单,导致库存为负,即“超卖”。
二、为什么用 Redis + Lua 能解决超卖?
1. Redis 的优势
- 内存操作,性能极高(10万+ QPS)
- 支持原子操作
- 适合作为热点数据(如库存)的缓存/存储
2. Lua 脚本的原子性
- Redis 执行 Lua 脚本时是单线程原子执行的
- 脚本内的多个命令不会被其他客户端请求打断
- 实现“查库存 + 判断 + 扣减”一体化,杜绝竞态条件
✅ 结论:Redis + Lua 是解决超卖问题的高性能、简洁可靠方案
三、完整实现方案(Python 版)
1. 环境准备
安装依赖
pip install redis
启动 Redis
确保本地或服务器已运行 Redis 服务(默认端口 6379)。
2. Lua 脚本:原子扣减库存
创建文件 decr_stock.lua:
-- KEYS[1]: 库存 key,如 "stock:1001"
-- ARGV[1]: 要扣减的数量(通常为 1)
-- 返回值:
-- 1: 扣减成功
-- 0: 库存不足
-- -1: 商品不存在
local key = KEYS[1]
local count = tonumber(ARGV[1])
-- 检查库存是否存在
local stock = redis.call('GET', key)
if not stock then
return -1
end
stock = tonumber(stock)
-- 判断库存是否足够
if stock < count then
return 0
end
-- 原子性扣减
redis.call('DECRBY', key, count)
return 1
✅ 脚本逻辑:先查后减,全程原子执行,避免超卖。
3. Python 实现:库存初始化
# init_stock.py
import redis
def init_stock(product_id: str, stock: int):
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
key = f"stock:{product_id}"
r.set(key, stock)
print(f"✅ 商品 {product_id} 库存初始化完成:{stock}")
if __name__ == "__main__":
init_stock("1001", 100)
运行:
python init_stock.py
4. Python 实现:执行 Lua 扣减库存
# deduct_stock.py
import redis
import sys
class StockDeductor:
def __init__(self, host='localhost', port=6379, db=0):
self.redis_client = redis.Redis(host=host, port=port, db=db, decode_responses=True)
self.script = self._load_script("decr_stock.lua")
# 预加载脚本,获取 SHA1,后续可用 EVALSHA 提升性能
self.sha = self.redis_client.script_load(self.script)
def _load_script(self, filepath: str) -> str:
"""读取 Lua 脚本内容"""
with open(filepath, 'r', encoding='utf-8') as f:
return f.read()
def deduct(self, product_id: str, quantity: int = 1) -> int:
"""
扣减库存
返回值:
1: 成功
0: 库存不足
-1: 商品不存在
"""
key = f"stock:{product_id}"
try:
# 使用 EVALSHA(若脚本已加载),否则 fallback 到 EVAL
result = self.redis_client.evalsha(self.sha, 1, key, quantity)
return int(result)
except redis.exceptions.NoScriptError:
# 脚本未缓存,使用 EVAL
result = self.redis_client.eval(self.script, 1, key, quantity)
return int(result)
# 示例调用
if __name__ == "__main__":
deductor = StockDeductor()
product_id = "1001"
result = deductor.deduct(product_id, 1)
if result == 1:
print("✅ 扣减成功!下单成功!")
# 此处可异步创建订单
elif result == 0:
print("❌ 库存不足,抢购失败!")
elif result == -1:
print("❌ 商品不存在!")
else:
print("❌ 扣减失败!")
5. 高并发压力测试(模拟 1000 用户抢购)
# stress_test.py
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from deduct_stock import StockDeductor
def worker(deductor, product_id, worker_id):
result = deductor.deduct(product_id, 1)
if result == 1:
print(f"线程 {worker_id}: 抢购成功 ✅")
return True
else:
print(f"线程 {worker_id}: 抢购失败 ❌")
return False
def stress_test(product_id: str, total_users: int = 1000):
deductor = StockDeductor()
start_time = time.time()
success_count = 0
# 使用线程池模拟并发
with ThreadPoolExecutor(max_workers=100) as executor:
futures = [
executor.submit(worker, deductor, product_id, i)
for i in range(total_users)
]
for future in futures:
if future.result():
success_count += 1
end_time = time.time()
print(f"\n🎉 压力测试完成")
print(f"总请求: {total_users}")
print(f"成功扣减: {success_count}")
print(f"耗时: {end_time - start_time:.2f} 秒")
print(f"QPS: {total_users / (end_time - start_time):.2f}")
if __name__ == "__main__":
stress_test("1001", 1000)
运行测试:
python stress_test.py
✅ 预期结果:成功扣减次数 ≤ 初始库存(如 100),且库存不会为负。
四、进阶优化建议
1. 使用 EVALSHA 提升性能
- 首次用
SCRIPT LOAD加载脚本,获取 SHA 值 - 后续调用使用
EVALSHA,减少网络传输 Lua 脚本内容
已在 StockDeductor 类中实现。
2. 库存归还(退款/取消订单)
-- incr_stock.lua
-- KEYS[1]: 库存 key
-- ARGV[1]: 归还数量
redis.call('INCRBY', KEYS[1], ARGV[1])
return 1
Python 调用:
def refund_stock(self, product_id: str, quantity: int):
key = f"stock:{product_id}"
self.redis_client.incrby(key, quantity)
3. 防止重复下单(用户维度去重)
import uuid
# 生成用户唯一标识(或使用 user_id)
user_token = f"user_token:{user_id}:{product_id}"
# 使用 SET 实现防重(有效期略长于抢购时间)
if r.set(user_token, '1', nx=True, ex=60):
# 可以下单
pass
else:
print("❌ 请勿重复抢购")
4. 异步下单 + 消息队列(推荐)
Lua 脚本只负责扣库存,成功后发送消息到队列(如 RabbitMQ/Kafka),由消费者创建订单,避免阻塞。
if result == 1:
order_queue.publish({
'user_id': user_id,
'product_id': product_id,
'quantity': 1
})
5. 库存分片(应对超大并发)
将库存拆分为多个分片,如:
stock:1001:01 -> 20
stock:1001:02 -> 20
...
抢购时随机选择一个分片扣减,提升并发能力。
五、总结:Redis + Lua 方案优势
| 特性 | 说明 |
|---|---|
| ✅ 防止超卖 | Lua 脚本原子执行,杜绝竞态 |
| ✅ 高性能 | Redis 内存操作,支持高并发 |
| ✅ 实现简单 | 脚本短小,易于维护 |
| ✅ 低延迟 | 减少网络往返(相比先 GET 再 DECR) |
| ⚠️ 注意点 | Lua 脚本不宜过长,避免阻塞 Redis 主线程 |
六、附录:完整项目结构
redis-lua-stock/
├── init_stock.py # 初始化库存
├── deduct_stock.py # 扣减库存类
├── stress_test.py # 压测脚本
├── decr_stock.lua # Lua 扣减脚本
├── incr_stock.lua # Lua 归还脚本
└── requirements.txt # redis
🎯 结语:
Redis + Lua 是解决库存超卖问题的黄金组合。结合 Python 的简洁语法,可快速构建高并发、不超卖的秒杀系统核心模块。生产环境建议配合限流、降级、监控、异步化等手段,保障系统稳定性。
943

被折叠的 条评论
为什么被折叠?



