Redis Lua原子操作 + 电商秒杀超卖超卖完整解决方案
Redis Lua原子操作 + 电商秒杀超卖超卖完整解决方案
说明:全文极简、重实战、无废话,包含 Redis Lua基础用法 + 原子特性原理 + 高并发秒杀防超卖完整落地方案,可直接用于生产环境。
一、Redis Lua 核心知识点(必懂)
1. 为什么Redis需要Lua脚本
作用:将多条Redis命令打包为单个原子指令执行
- 普通多条命令:存在并发时差、线程安全问题
- Lua脚本:执行不插针、不中断、原子串行
- 解决:并发竞争、超卖、库存错乱、分布式锁竞争问题
2. Redis Lua 执行特性(硬核规则)
- 单个Lua脚本:要么全部执行成功,要么全部失败
- 脚本执行期间,其他客户端命令全部阻塞等待
- 不支持事务回滚,但天然具备事务原子性
- 性能极高:一次网络IO完成多条命令,减少网络开销
3. Redis Lua 标准语法模板
作用:固定传参、取值、返回格式,生产统一规范
-- KEYS[1]、KEYS[2] 传入key
-- ARGV[1]、ARGV[2] 传入参数值
return redis.call("命令名", KEYS[1], ARGV[1])4. 常用Redis Lua命令
-- 取值
redis.call("GET", KEYS[1])
-- 设值
redis.call("SET", KEYS[1], ARGV[1])
-- 自增/自减
redis.call("INCR", KEYS[1])
redis.call("DECR", KEYS[1])
-- 判断存在
redis.call("EXISTS", KEYS[1])5. Java/Python 调用Redis Lua通用格式
Python示例
import redis
r = redis.Redis(decode_responses=True)
# 加载Lua脚本
lua_script = """
return redis.call('GET', KEYS[1])
"""
# 执行脚本:keys列表、args列表
res = r.eval(lua_script, 1, "goods_stock")
print(res)二、秒杀超卖问题根源
场景:库存10件,1000人同时抢购
普通代码bug流程:
- 线程A查询库存 = 10
- 线程B查询库存 = 10(未扣减,并发同时读到充足库存)
- 线程A、B同时扣减 → 库存变为8,卖出12件,超卖
本质原因:查库存、判库存、扣库存 非原子操作
三、Lua原子防超卖核心思路
一条Lua脚本完成三件原子操作(不可分割)
- 查询当前商品库存
- 判断库存是否大于0
- 满足则扣减库存,不满足直接返回失败
全程单线程执行,杜绝并发同时判定通过
四、完整可上线:秒杀防超卖Lua脚本
1. 脚本功能说明
- 支持单商品限购1件(防止单人多抢)
- 原子判定库存 + 原子扣减
- 记录已抢购用户,防止重复下单
- 返回状态码,前端/后端可直接判断结果
2. 最终生产级Lua脚本
-- KEYS[1]:商品库存key
-- KEYS[2]:用户抢购记录key
-- ARGV[1]:用户ID
-- ARGV[2]:本次抢购数量(固定1)
-- 1. 判断用户是否已抢购过(防重复下单)
local has_buy = redis.call("sismember", KEYS[2], ARGV[1])
if has_buy == 1 then
return 0 -- 0:重复抢购,失败
end
-- 2. 获取当前库存
local stock = tonumber(redis.call("get", KEYS[1]) or 0)
-- 3. 库存不足直接返回
if stock < 1 then
return -1 -- -1:库存不足,秒杀结束
end
-- 4. 原子扣减库存
redis.call("decr", KEYS[1])
-- 5. 记录用户抢购记录
redis.call("sadd", KEYS[2], ARGV[1])
-- 6. 秒杀成功
return 13. 返回码统一规范
- 1:秒杀成功
- 0:用户重复抢购
- -1:库存不足、超卖拦截
五、高并发秒杀完整业务流程(无漏洞)
1. 秒杀预热(提前初始化库存)
# 初始化商品库存100件
SET seckill:goods:1001:stock 100
# 用户抢购记录集合
SADD seckill:goods:1001:user2. 完整执行链路(层层拦截,抗百万并发)
前端拦截:按钮置灰、防抖、倒计时
网关拦截:限流、防刷、IP限制
Redis Lua原子拦截:防超卖、防重复抢(核心)
异步落库:秒杀成功后,MQ异步写入数据库(扛高并发)
六、Python 完整可运行秒杀Demo
功能:模拟100并发抢10库存,绝对无超卖
import redis
import threading
# 连接Redis
r = redis.Redis(decode_responses=True)
# 秒杀Lua脚本
SECKILL_LUA = """
local has_buy = redis.call("sismember", KEYS[2], ARGV[1])
if has_buy == 1 then
return 0
end
local stock = tonumber(redis.call("get", KEYS[1]) or 0)
if stock < 1 then
return -1
end
redis.call("decr", KEYS[1])
redis.call("sadd", KEYS[2], ARGV[1])
return 1
"""
# 初始化库存10
r.set("seckill:stock", 10)
def seckill(user_id):
"""单个用户抢购"""
res = r.eval(SECKILL_LUA, 2, "seckill:stock", "seckill:user", user_id)
if res == 1:
print(f"用户{user_id} 秒杀成功")
elif res == 0:
print(f"用户{user_id} 重复抢购")
else:
print(f"用户{user_id} 秒杀失败,库存不足")
# 模拟100个并发线程
if __name__ == "__main__":
thread_list = []
for i in range(1,101):
t = threading.Thread(target=seckill, args=(f"user{i}",))
thread_list.append(t)
t.start()
for t in thread_list:
t.join()
# 最终库存校验
final_stock = r.get("seckill:stock")
print("最终剩余库存:", final_stock)
print("成功抢购人数:", r.scard("seckill:user"))
七、PHP 完整可运行秒杀Demo(生产可用+高并发模拟)
功能:基于PHP Redis扩展 + Lua原子脚本,模拟高并发秒杀,彻底防超卖、防重复抢购,适配PHP框架(ThinkPHP/Laravel)原生使用
环境要求:安装PHP Redis扩展 pecl install redis
<?php
// 环境要求:安装PHP Redis扩展、开启pcntl多进程扩展(模拟高并发)
// 生产适配:兼容Laravel/ThinkPHP/Swoole所有PHP项目
// 1. 连接Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 解决Redis超时、并发连接问题
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
// 2. 生产级秒杀Lua脚本(原子防超卖、防重复抢购)
$luaScript = <<<LUA
local has_buy = redis.call("sismember", KEYS[2], ARGV[1])
if has_buy == 1 then
return 0
end
local stock = tonumber(redis.call("get", KEYS[1]) or 0)
if stock < 1 then
return -1
end
redis.call("decr", KEYS[1])
redis.call("sadd", KEYS[2], ARGV[1])
return 1
LUA;
// 3. 初始化秒杀库存(仅首次执行,可后台预热执行)
$redis->del('seckill:stock');
$redis->del('seckill:user');
$redis->set('seckill:stock', 10);
/**
* 秒杀核心原子方法
* @param string $userId 唯一用户ID
* @return int 1=成功 0=重复抢购 -1=库存不足
*/
function seckill(string $userId): int
{
global $redis, $luaScript;
// PHP eval固定参数:脚本内容、[KEYS+ARGV数组]、KEYS参数数量
return $redis->eval($luaScript, [
'seckill:stock',
'seckill:user',
$userId
], 2);
}
/**
* 多进程模拟高并发秒杀(100用户并发抢10库存)
*/
function batchSeckill()
{
$processNum = 100;
for ($i = 1; $i <= $processNum; $i++) {
$pid = pcntl_fork();
if ($pid == 0) {
// 子进程执行秒杀
$userId = 'user_' . $i;
$res = seckill($userId);
switch ($res) {
case 1:
echo "【成功】{$userId} 秒杀下单完成\n";
break;
case 0:
echo "【拦截】{$userId} 禁止重复抢购\n";
break;
case -1:
echo "【售罄】{$userId} 库存不足,秒杀失败\n";
break;
}
// 必须退出子进程,避免循环派生
exit(0);
}
}
// 等待所有子进程执行完毕
while (pcntl_waitpid(0, $status) != -1);
}
// 执行高并发秒杀模拟
batchSeckill();
// 4. 最终数据校验(杜绝超卖核心校验)
global $redis;
$finalStock = $redis->get('seckill:stock');
$successNum = $redis->sCard('seckill:user');
echo "\n========== 秒杀最终统计结果 ==========\n";
echo "剩余库存数量:{$finalStock}\n";
echo "成功下单人数:{$successNum}\n";
echo "校验结果:" . ($finalStock + $successNum == 10 ? "✅ 无超卖、无漏卖,数据正常" : "❌ 数据异常") . "\n";
$redis->close();
?>一、PHP版本核心特性说明
- 原子性互通:Lua脚本与Python版本完全一致,底层原子锁库存逻辑通用,跨语言无差异
- PHP独有规则:Redis eval方法参数顺序固定,必须严格区分KEYS数量,否则脚本执行报错
- 框架无缝适配:可直接移植 Laravel/ThinkPHP/Swoole 项目,仅需替换框架Redis实例
- 高并发适配性:基于多进程模拟真实秒杀流量,兼容PHP-FPM、Swoole协程环境,分布式集群完全生效
- 数据强一致性:最终库存+成功人数恒等于初始库存,彻底杜绝超卖、少卖、漏卖问题
二、PHP生产部署注意事项
- 线上关闭pcntl多进程代码,仅用业务接口接收请求,多进程仅用于本地压测验证
- Lua脚本建议使用
script load预加载缓存,通过sha1值调用,减少网络传输开销 - 秒杀Redis Key设置过期时间,自动清理过期秒杀数据,避免内存堆积
- 接口层增加限流、防刷、IP黑白名单,拦截恶意黄牛请求
- 脚本预加载优化:生产环境禁止每次请求传递完整Lua脚本,采用script\_load预加载+sha1哈希调用,大幅减少网络IO、提升并发吞吐量
三、PHP Lua脚本缓存(SHA1)生产优化代码
优化作用:首次加载脚本生成唯一SHA1哈希值,后续所有秒杀请求直接通过哈希值调用脚本,无需重复传输大段脚本内容,高并发场景性能提升30%+
<?php
// 连接Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
// 秒杀Lua脚本
$luaScript = <<<LUA
local has_buy = redis.call("sismember", KEYS[2], ARGV[1])
if has_buy == 1 then
return 0
end
local stock = tonumber(redis.call("get", KEYS[1]) or 0)
if stock < 1 then
return -1
end
redis.call("decr", KEYS[1])
redis.call("sadd", KEYS[2], ARGV[1])
return 1
LUA;
// 核心:预加载脚本,获取SHA1哈希值(项目初始化执行一次即可)
$sha1Hash = $redis->script('load', $luaScript);
echo "脚本缓存SHA1值:" . $sha1Hash . "\n";
/**
* 缓存版秒杀方法(生产正式使用)
* 无需传递完整脚本,仅通过SHA1调用,性能更高
*/
function fastSeckill(string $userId, string $sha1Hash): int
{
global $redis;
// evalsha 哈希调用脚本
return $redis->evalsha($sha1Hash, [
'seckill:stock',
'seckill:user',
$userId
], 2);
}
// 初始化库存
$redis->del('seckill:stock', 'seckill:user');
$redis->set('seckill:stock', 10);
// 测试缓存版秒杀
$res = fastSeckill('user_001', $sha1Hash);
var_dump($res);
// 脚本状态校验(判断脚本是否已缓存)
$isExists = $redis->script('exists', $sha1Hash);
echo "脚本是否缓存成功:" . ($isExists[0] ? "是" : "否") . "\n";
$redis->close();
?>四、脚本缓存生产高可用方案(自动降级容错)
核心解决问题:Redis重启、缓存脚本丢失、evalsha执行失败等线上突发异常,无感知自动降级,保障秒杀服务不宕机、不报错
<?php
// 连接Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1);
// 原始秒杀Lua脚本
$luaScript = <<<LUA
local has_buy = redis.call("sismember", KEYS[2], ARGV[1])
if has_buy == 1 then
return 0
end
local stock = tonumber(redis.call("get", KEYS[1]) or 0)
if stock < 1 then
return -1
end
redis.call("decr", KEYS[1])
redis.call("sadd", KEYS[2], ARGV[1])
return 1
LUA;
// 项目初始化预加载脚本,全局缓存SHA1值
$sha1Hash = $redis->script('load', $luaScript);
/**
* 高可用秒杀方法(缓存优先 + 自动降级)
* 优先evalsha高性能调用,失败自动降级eval原生调用
* @param string $userId 用户唯一ID
* @return int 1成功 0重复 -1库存不足
*/
function safeSeckill(string $userId): int
{
global $redis, $sha1Hash, $luaScript;
$keys = ['seckill:stock', 'seckill:user', $userId];
$keyNum = 2;
try {
// 优先使用缓存脚本(高性能)
return $redis->evalsha($sha1Hash, $keys, $keyNum);
} catch (RedisException $e) {
// 捕获脚本不存在异常,自动降级原生执行
if (str_contains($e->getMessage(), 'NOSCRIPT')) {
// 重新加载脚本缓存,修复异常
$GLOBALS['sha1Hash'] = $redis->script('load', $luaScript);
// 降级执行完整脚本
return $redis->eval($luaScript, $keys, $keyNum);
}
// 其他异常直接抛出
throw $e;
}
}
// 初始化秒杀库存
$redis->del('seckill:stock', 'seckill:user');
$redis->set('seckill:stock', 10);
// 模拟多次秒杀调用,验证容错性
for ($i = 1; $i <= 5; $i++) {
$res = safeSeckill('user_' . $i);
echo "用户user_{$i} 结果:" . ($res === 1 ? "秒杀成功" : "失败") . "\n";
}
// 最终数据校验
$finalStock = $redis->get('seckill:stock');
$successNum = $redis->sCard('seckill:user');
echo "\n========== 高可用秒杀结果 ==========\n";
echo "剩余库存:{$finalStock}\n";
echo "成功人数:{$successNum}\n";
$redis->close();
?>五、高可用方案核心特性
- 智能优先策略:正常场景使用
evalsha缓存调用,性能拉满 - 无感自动降级:捕捉
NOSCRIPT脚本丢失异常,自动重新加载脚本并降级原生执行 - 自愈修复能力:异常后自动更新全局SHA1缓存,后续请求恢复高性能模式
- 业务零影响:整个容错过程无报错、无中断,用户完全感知不到
六、脚本缓存生产使用规范
- 加载时机:项目启动、框架初始化时执行一次
script load,常驻Redis缓存,无需重复加载 - 缓存有效期:Redis不主动清理缓存脚本,重启Redis后依赖自动降级逻辑自愈
- 统一复用:全局统一SHA1常量,所有秒杀接口共用缓存脚本,避免重复加载
- 异常监控:线上可捕获降级日志,用于监控Redis脚本缓存异常频次,及时排查问题
- 加载时机:项目启动、框架初始化时执行一次
script load,常驻Redis缓存,无需重复加载 - 缓存有效期:Redis不主动清理缓存脚本,重启Redis后需重新加载
- 降级方案:若evalsha报错(脚本丢失),自动降级为eval完整脚本加载,保障服务不中断
- 统一复用:全局统一SHA1常量,所有秒杀接口共用缓存脚本,避免重复加载
八、彻底解决超卖超买的核心原理总结
1. 为什么普通代码会超卖
查库存、判库存、扣库存 三步非原子,并发间隙导致多人同时抢到资格
2. 为什么Lua能彻底杜绝超卖
Redis单线程执行Lua脚本,整个秒杀逻辑串行原子执行,无间隙、无并发穿插,从底层锁死库存竞争
九、生产环境额外优化(高阶)
- 脚本缓存:使用 script load + sha1 缓存脚本,不用每次传完整脚本
- 过期时间:秒杀key设置过期时间,自动释放内存
- 限流熔断:接口层增加QPS限流,避免打垮Redis
- 异步落库:Redis只做预扣减,MQ异步更新MySQL,提升吞吐量
- 防黄牛:绑定用户、IP、设备号,限制单用户限购次数
十、常见误区避坑
❌ 误区:Java加锁(synchronized/lock)防超卖
✅ 问题:分布式集群部署,单机锁完全失效
❌ 误区:Redis多条普通命令判库存
✅ 问题:命令之间存在并发空隙,必然超卖
✅ 唯一稳妥方案:Redis Lua原子脚本(行业通用秒杀最终方案)