Redis + Lua 解决库存超卖问题 —— Python 实现完整教程

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 的简洁语法,可快速构建高并发、不超卖的秒杀系统核心模块。生产环境建议配合限流、降级、监控、异步化等手段,保障系统稳定性。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值