Redis + Lua 实现秒杀系统(限流、库存控制、防超卖)详解

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 脚本实现核心逻辑

🎯 目标:一个原子操作完成

  1. 检查用户是否已购买
  2. 检查库存是否充足
  3. 扣减库存
  4. 记录用户已购买
  5. 返回成功/失败

✅ 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 的极致性能。结合合理的限流、异步、降级策略,可构建一个稳定、高效的秒杀系统。

💡 进阶建议

  • 使用 RedissonRLock 实现更复杂的锁逻辑
  • 结合 布隆过滤器 快速判断用户是否参与过
  • 使用 Redis Cluster 水平扩展,支撑更大并发
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值