Redis + Lua 实现秒杀系统(限流、库存控制、防超卖)详解
秒杀系统是典型的 高并发、瞬时流量洪峰 场景,要求系统具备 高性能、强一致性、防超卖、限流控制 等能力。Redis 凭借其 内存高速读写、原子操作、Lua 脚本支持,成为构建秒杀系统的核心组件。
本文将深入讲解如何使用 Redis + Lua 脚本 实现一个安全、高效、防超卖的秒杀系统,涵盖 库存扣减、用户限购、限流控制、防刷机制 等关键功能。
一、秒杀系统的挑战
| 挑战 | 说明 |
|---|---|
| 🔥 高并发 | 瞬间数万请求涌入,数据库无法承受 |
| 🚫 超卖 | 库存被超额扣减(如库存 100,卖出 105) |
| 🧩 数据一致性 | 库存、订单、用户状态需一致 |
| 🛑 重复下单 | 同一用户多次抢购 |
| 🤖 机器人刷单 | 恶意脚本高频请求 |
| 📉 服务雪崩 | 流量过大导致系统崩溃 |
二、Redis + Lua 的优势
| 技术 | 优势 |
|---|---|
| Redis | 内存操作,QPS 可达 10万+,支持原子命令 |
| Lua 脚本 | 在 Redis 服务端执行,原子性、事务性、避免网络往返 |
| 组合使用 | 将“判断库存 → 扣减库存 → 记录用户 → 限流”封装为一个原子操作 |
✅ 核心价值:
Lua 脚本 = Redis 的“存储过程”,保证多步操作的原子性,彻底解决超卖问题。
三、数据结构设计
1. 库存(String)
SECKILL:STOCK:{itemId} → 100
- 使用 String 存储剩余库存,支持
DECR原子操作
2. 用户限购(Set)
SECKILL:BOUGHT:{itemId} → {uid1, uid2, ...}
- 记录已购买该商品的用户 ID,防止重复抢购
3. 接口限流(String + TTL)
RATE_LIMIT:USER:{uid} → 1 (有效期 1 小时)
- 限制用户单位时间内的请求频率
4. 订单预处理(List 或 Stream)
SECKILL:ORDERS → {uid:1001,itemId:2001,ts:1712345678}
- 异步下单队列,减轻数据库压力
四、Lua 脚本实现核心逻辑
🎯 目标:一个原子操作完成
- 检查用户是否已购买
- 检查库存是否充足
- 扣减库存
- 记录用户已购买
- 返回成功/失败
✅ Lua 脚本(seckill.lua)
-- KEYS[1] = 库存 key (SECKILL:STOCK:{itemId})
-- KEYS[2] = 用户购买记录 key (SECKILL:BOUGHT:{itemId})
-- ARGV[1] = 用户 ID
-- ARGV[2] = 商品 ID
-- 1. 检查用户是否已购买
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then
return 0 -- 已购买,失败
end
-- 2. 获取当前库存
local stock = tonumber(redis.call('GET', KEYS[1]))
if not stock then
return -1 -- 商品不存在
end
if stock <= 0 then
return 0 -- 无库存
end
-- 3. 扣减库存(原子操作)
local newStock = redis.call('DECR', KEYS[1])
-- 4. 记录用户购买(仅当库存 > 0 时)
if newStock >= 0 then
redis.call('SADD', KEYS[2], ARGV[1])
return 1 -- 成功
else
-- 库存不足,回滚(实际不会发生,因为 DECR 前已判断)
redis.call('INCR', KEYS[1])
return 0
end
💡 注意:
DECR是原子的,即使多个请求同时执行,也不会超卖。
五、Java 代码调用示例(Spring Data Redis)
@Service
public class SeckillService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 加载 Lua 脚本
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setScriptText(
"if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then return 0 end " +
"local stock = tonumber(redis.call('GET', KEYS[1])) " +
"if not stock or stock <= 0 then return 0 end " +
"local newStock = redis.call('DECR', KEYS[1]) " +
"if newStock >= 0 then " +
" redis.call('SADD', KEYS[2], ARGV[1]) " +
" return 1 " +
"else " +
" redis.call('INCR', KEYS[1]) " +
" return 0 " +
"end"
);
SECKILL_SCRIPT.setResultType(Long.class);
}
public boolean seckill(long userId, long itemId) {
String stockKey = "SECKILL:STOCK:" + itemId;
String boughtKey = "SECKILL:BOUGHT:" + itemId;
Long result = redisTemplate.execute(
SECKILL_SCRIPT,
Arrays.asList(stockKey, boughtKey),
String.valueOf(userId),
String.valueOf(itemId)
);
return result != null && result == 1;
}
}
六、限流与防刷机制
1. 用户级限流(防止高频请求)
public boolean rateLimitCheck(long userId) {
String key = "RATE_LIMIT:USER:" + userId;
Long count = redisTemplate.opsForValue().increment(key, 1);
if (count == 1) {
redisTemplate.expire(key, 1, TimeUnit.HOURS); // 1小时内最多10次
}
return count <= 10;
}
2. 接口级限流(控制总请求量)
使用 Redis + Lua 实现令牌桶或滑动窗口:
-- 滑动窗口限流(每秒最多 100 次)
local key = "RATE_LIMIT:GLOBAL"
local now = tonumber(ARGV[1])
local window = 1 -- 1秒
local limit = 100
-- 移除过期时间戳
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- 添加当前时间戳
redis.call('ZADD', key, now, now .. '-' .. ARGV[2]) -- 加随机后缀防冲突
-- 获取当前请求数
local count = redis.call('ZCARD', key)
if count > limit then
return 0
else
return 1
end
七、防超卖的终极保障
虽然 Lua 脚本已防超卖,但为保险起见,可增加:
1. 库存预扣(提前加载)
- 秒杀开始前,将库存从数据库加载到 Redis
SET SECKILL:STOCK:1001 100
2. 异步落库
- Lua 成功后,将订单写入消息队列(如 Kafka、Redis Stream)
- 消费者异步写入数据库,避免 DB 成为瓶颈
if (seckill(userId, itemId)) {
// 发送到订单队列
redisTemplate.opsForList().rightPush("SECKILL:ORDERS",
JSON.toJSONString(new Order(userId, itemId)));
return "抢购成功,订单处理中...";
}
3. 库存回补(失败时)
- 用户未支付 → 异步回补库存
INCR SECKILL:STOCK:{itemId}
八、系统架构设计
+------------------+
| Nginx + Lua | ← 限流、防刷
+------------------+
↓
+------------------+
| API Gateway | ← 鉴权、日志
+------------------+
↓
+------------------+
| Redis (Lua) | ← 核心:库存扣减、限购
+------------------+
↓
+------------------+
| Kafka / Stream | ← 异步订单队列
+------------------+
↓
+------------------+
| Order Service | ← 写入数据库
+------------------+
九、最佳实践与优化建议
| 项目 | 建议 |
|---|---|
| 🔐 安全 | 使用 HTTPS、Token 鉴权、验证码防机器人 |
| 🚀 性能 | 预热 Redis、使用连接池、禁用持久化(秒杀期间) |
| 📊 监控 | 监控库存、QPS、Lua 执行时间 |
| 🧩 降级 | Redis 故障时,降级为数据库 + 限流 |
| 🧹 清理 | 秒杀结束后清理临时 key(如库存、购买记录) |
| 📈 压测 | 上线前进行全链路压测 |
十、总结:Redis + Lua 秒杀系统核心要点
| 功能 | 实现方式 |
|---|---|
| 防超卖 | Lua 脚本原子扣减库存 + 用户标记 |
| 防重复购买 | Set 记录已购用户 |
| 限流控制 | 滑动窗口 + 用户级限流 |
| 高并发 | Redis 内存操作,QPS 10万+ |
| 数据一致性 | Lua 原子操作 + 异步落库 |
| 系统解耦 | 消息队列异步处理订单 |
✅ 结语:
Redis + Lua 是实现秒杀系统的黄金组合。通过将核心逻辑封装在 Lua 脚本中,既能保证 原子性与一致性,又能发挥 Redis 的极致性能。结合合理的限流、异步、降级策略,可构建一个稳定、高效的秒杀系统。
💡 进阶建议:
- 使用 Redisson 的
RLock实现更复杂的锁逻辑- 结合 布隆过滤器 快速判断用户是否参与过
- 使用 Redis Cluster 水平扩展,支撑更大并发
1080

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



