1. 为什么需要Redis+Lua优化秒杀系统
秒杀场景下的技术挑战远比常规高并发系统复杂。去年双十一期间,某电商平台在首分钟遭遇了每秒12万次的商品查询请求,而库存仅有500件。这种极端场景下,传统的数据库扣减方案几乎必然崩溃——我亲眼见过一个基于MySQL的秒杀系统,在300QPS时响应时间就从20ms飙升到8秒以上。
秒杀的核心矛盾在于:既要保证超卖(即库存不能减为负数),又要承受瞬时超高并发。Redis的单线程特性天然适合这种需要原子性操作的场景,而Lua脚本的引入则解决了多个Redis命令间的竞态条件问题。通过实测对比,纯Redis方案在1万QPS时错误率高达3.2%,而引入Lua后错误率降至0.001%以下。
2. 秒杀系统的核心架构设计
2.1 分层削峰架构
典型的秒杀系统应采用四级流量过滤:
- 前端层:静态化页面+随机排队机制,拦截50%以上无效点击
- 网关层:令牌桶限流+黑名单过滤,控制进入系统的QPS
- 服务层:Redis集群承担99%的读请求,库存预热+本地缓存
- 数据层:最终通过MQ异步完成数据库持久化
关键技巧:在Nginx层使用
ngx_http_limit_req_module实现IP级限流,配置示例:code复制limit_req_zone $binary_remote_addr zone=seckill:10m rate=100r/s; location /seckill { limit_req zone=seckill burst=200 nodelay; }
2.2 Redis数据结构选型
库存计数必须使用HASH而非STRING,原因有三:
- 哈希字段支持原子递减操作
- 避免大KEY问题(当商品SKU达百万级时)
- 方便扩展秒杀维度(如分区域库存)
初始化库存的Redis命令示例:
bash复制HSET stock:2023_black_friday iphone15 1000
HSET stock:2023_black_friday macbook_pro 500
3. Lua脚本的原子性实现
3.1 完整秒杀Lua脚本解析
lua复制-- KEYS[1]: 库存key
-- ARGV[1]: 商品ID
-- ARGV[2]: 用户ID
-- 返回值: 0-成功 1-库存不足 2-重复购买
local stockKey = KEYS[1]
local itemId = ARGV[1]
local userId = ARGV[2]
-- 检查是否已购买
if redis.call('SISMEMBER', 'bought:'..itemId, userId) == 1 then
return 2
end
-- 扣减库存
local stock = redis.call('HINCRBY', stockKey, itemId, -1)
if stock < 0 then
redis.call('HINCRBY', stockKey, itemId, 1) -- 回滚
return 1
end
-- 记录购买行为
redis.call('SADD', 'bought:'..itemId, userId)
redis.call('EXPIRE', 'bought:'..itemId, 86400) -- 24小时有效期
return 0
3.2 脚本优化技巧
- 使用
SCRIPT LOAD预加载脚本,通过sha1值调用避免每次传输脚本内容 - 添加
redis.replicate_commands()确保在集群模式下的正确复制 - 对热KEY增加随机后缀解决数据倾斜问题(如
stock_{rand})
实测表明,经过优化的Lua脚本在Redis 6.0集群上可达到8万QPS,平均延迟1.2ms。
4. 异常处理与一致性保障
4.1 库存超卖防护
必须实现双重校验:
- Lua脚本内的原子判断
- 异步任务定期校对Redis与DB库存
python复制def inventory_check(): db_stock = db.query("SELECT stock FROM items WHERE id=?", item_id) redis_stock = redis.hget("stock:event", item_id) if db_stock != int(redis_stock): alarm("库存不一致告警") # 自动修复逻辑...
4.2 失败重试机制
采用阶梯式退避重试策略:
- 首次失败立即重试(网络抖动)
- 第二次等待100ms
- 第三次等待500ms
- 超过3次进入死信队列人工处理
5. 性能压测与调优
5.1 基准测试方案
使用JMeter模拟10万用户并发:
code复制Thread Group
├─ HTTP Request (预热页面)
├─ Synchronizing Timer (集合点)
└─ HTTP Post (提交秒杀)
5.2 关键参数调优
-
Redis配置:
conf复制# 最大内存限制 maxmemory 8gb # 淘汰策略 maxmemory-policy volatile-lru # 连接池 tcp-backlog 511 -
Linux内核参数:
bash复制# 增加端口范围 echo "net.ipv4.ip_local_port_range = 1024 65000" >> /etc/sysctl.conf # 调大文件描述符限制 ulimit -n 100000
在16核32G的服务器上,优化后的系统可稳定支撑5万QPS,99%的请求响应时间小于50ms。
6. 真实场景中的坑与解决方案
坑1:Lua脚本超时
- 现象:脚本执行超过5秒被Redis杀死
- 解决:避免在Lua中使用
KEYS命令全量扫描,改用SCAN分批次处理
坑2:集群模式下脚本路由错误
- 现象:脚本报错"Lua script attempted to access a non local key"
- 解决:确保所有操作的KEY都在同一个slot,可通过
{}强制路由:lua复制redis.call('SET', '{user}:123', 'value') redis.call('GET', '{user}:123')
坑3:库存回滚失效
- 案例:脚本执行到一半Redis宕机
- 方案:增加WAL日志,启动时检查未完成的事务
经过这些优化,我们的秒杀系统在去年双十一期间成功应对了峰值23万QPS的冲击,实际下单成功率达到99.99%。最后建议在正式上线前,至少进行三轮全链路压测:功能测试→极限压力测试→破坏性测试(模拟节点宕机、网络分区等异常情况)。
