基于SpringBoot + Redis + Lua 实现高并发秒杀系统实战

大家好,我是服务端技术精选的作者。今天咱们聊聊一个在电商领域极其重要的话题:高并发秒杀系统。

秒杀系统的挑战

在我们的日常开发工作中,经常会遇到这样的场景:

  • 限量商品开售瞬间,成千上万的用户同时访问
  • 系统在秒杀开始时直接崩溃,用户无法下单
  • 超卖现象频发,库存被抢购一空
  • 黑产机器人恶意刷单,正常用户买不到商品

传统的库存扣减方式在高并发场景下根本无法胜任,今天我们就来聊聊如何用Redis + Lua构建一个高并发的秒杀系统。

为什么选择Redis + Lua

相比传统的数据库事务方案,Redis + Lua有以下优势:

  • 高性能:内存操作,响应速度极快
  • 原子性:Lua脚本在Redis中是原子执行的
  • 低延迟:避免数据库的网络IO开销
  • 高并发:Redis单机可支撑10万+ QPS

系统架构设计

1. 整体架构

用户请求 → API网关 → 限流过滤 → Redis Lua脚本 → 库存扣减 → 订单创建

2. 核心组件

  • Redis:存储商品库存和用户限购信息
  • Lua脚本:原子性执行库存扣减逻辑
  • 消息队列:异步处理订单创建
  • 限流组件:防止恶意刷单

Lua脚本实现

1. 库存扣减脚本

-- 秒杀库存扣减脚本
-- KEYS[1]: 商品库存key
-- KEYS[2]: 用户限购key
-- ARGV[1]: 购买数量
-- ARGV[2]: 用户ID
-- ARGV[3]: 限购数量

local stock_key = KEYS[1]
local user_limit_key = KEYS[2]

local buy_num = tonumber(ARGV[1])
local user_id = ARGV[2]
local limit_num = tonumber(ARGV[3])

-- 检查库存是否充足
local current_stock = redis.call('GET', stock_key)
if not current_stock then
    return {code = 1, msg = '商品不存在'}
end

current_stock = tonumber(current_stock)
if current_stock < buy_num then
    return {code = 2, msg = '库存不足'}
end

-- 检查用户是否已购买过(限购)
local user_bought = redis.call('GET', user_limit_key)
if user_bought then
    user_bought = tonumber(user_bought)
    if user_bought >= limit_num then
        return {code = 3, msg = '已达到购买上限'}
    end
    -- 检查加上本次购买是否会超过限制
    if user_bought + buy_num > limit_num then
        return {code = 4, msg = '购买数量超过限制'}
    end
end

-- 扣减库存
redis.call('DECRBY', stock_key, buy_num)

-- 记录用户购买数量
if user_bought then
    redis.call('INCRBY', user_limit_key, buy_num)
else
    redis.call('SET', user_limit_key, buy_num)
end

-- 设置用户限购key过期时间(防止永久占用)
redis.call('EXPIRE', user_limit_key, 86400) -- 24小时

return {code = 0, msg = '秒杀成功', remaining_stock = current_stock - buy_num}

2. 库存初始化脚本

-- 商品库存初始化脚本
-- KEYS[1]: 商品库存key
-- ARGV[1]: 库存数量
-- ARGV[2]: 过期时间

local stock_key = KEYS[1]
local stock_num = tonumber(ARGV[1])
local expire_time = tonumber(ARGV[2])

-- 设置库存
redis.call('SET', stock_key, stock_num)

-- 设置过期时间
if expire_time > 0 then
    redis.call('EXPIRE', stock_key, expire_time)
end

return stock_num

SpringBoot集成实现

1. Redis配置

@Configuration
public class RedisConfig {
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
    
    @Bean
    public DefaultRedisScript<SeckillResult> seckillScript() {
        DefaultRedisScript<SeckillResult> script = new DefaultRedisScript<>();
        script.setLocation(new ClassPathResource("lua/seckill.lua"));
        script.setResultType(SeckillResult.class);
        return script;
    }
}

2. 秒杀服务实现

@Service
@Slf4j
public class SeckillService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private RedisScript<List> seckillScript;
    
    @Autowired
    private OrderService orderService;
    
    /**
     * 执行秒杀
     */
    public SeckillResult executeSeckill(Long productId, Long userId, Integer num) {
        try {
            // 构建Redis key
            String stockKey = "seckill:stock:" + productId;
            String userLimitKey = "seckill:user:" + productId + ":user:" + userId;
            
            // 执行Lua脚本
            List<String> keys = Arrays.asList(stockKey, userLimitKey);
            List<String> args = Arrays.asList(
                String.valueOf(num),      // 购买数量
                String.valueOf(userId),   // 用户ID
                "1"                       // 限购数量
            );
            
            List<Object> result = (List<Object>) redisTemplate.execute(
                seckillScript, 
                new DefaultRedisScript<>("return redis.call('EVALSHA', '...', 2, KEYS[1], KEYS[2], ARGV[1], ARGV[2], ARGV[3])"),
                keys, 
                args.toArray(new String[0])
            );
            
            if (result != null && result.size() >= 2) {
                int code = Integer.parseInt(result.get(0).toString());
                String msg = result.get(1).toString();
                
                SeckillResult seckillResult = new SeckillResult();
                seckillResult.setCode(code);
                seckillResult.setMsg(msg);
                
                if (code == 0) {
                    // 秒杀成功,异步创建订单
                    asyncCreateOrder(productId, userId, num);
                }
                
                return seckillResult;
            }
            
            return SeckillResult.fail("秒杀失败");
        } catch (Exception e) {
            log.error("秒杀异常", e);
            return SeckillResult.fail("系统异常,请稍后重试");
        }
    }
    
    /**
     * 异步创建订单
     */
    @Async
    public void asyncCreateOrder(Long productId, Long userId, Integer num) {
        try {
            // 创建订单
            Order order = new Order();
            order.setUserId(userId);
            order.setProductId(productId);
            order.setQuantity(num);
            order.setStatus("CREATED");
            order.setCreateTime(new Date());
            
            orderService.createOrder(order);
            
            log.info("订单创建成功: userId={}, productId={}, quantity={}", userId, productId, num);
        } catch (Exception e) {
            log.error("订单创建失败", e);
            // 可以考虑补偿机制
        }
    }
}

限流防护机制

1. 接口限流

@Component
public class RateLimiter {
    
    private final RateLimiter rateLimiter = RateLimiter.create(100); // 每秒100个令牌
    
    public boolean tryAcquire() {
        return rateLimiter.tryAcquire();
    }
}

@RestController
public class SeckillController {
    
    @Autowired
    private SeckillService seckillService;
    
    @Autowired
    private RateLimiter rateLimiter;
    
    @PostMapping("/seckill/{productId}")
    public Result seckill(@PathVariable Long productId, 
                         @RequestParam Long userId, 
                         @RequestParam(defaultValue = "1") Integer num) {
        // 接口限流
        if (!rateLimiter.tryAcquire()) {
            return Result.fail("请求过于频繁,请稍后再试");
        }
        
        // 参数校验
        if (num <= 0 || num > 5) { // 限制单次购买数量
            return Result.fail("购买数量不合法");
        }
        
        // 执行秒杀
        SeckillResult result = seckillService.executeSeckill(productId, userId, num);
        return Result.success(result);
    }
}

2. 黑产防护

@Component
public class AntiCrawlerService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public boolean isSuspiciousRequest(String ip, Long userId) {
        String ipKey = "anti_crawler:ip:" + ip;
        String userKey = "anti_crawler:user:" + userId;
        
        // 检查IP请求频率
        Long ipCount = redisTemplate.opsForValue().increment(ipKey);
        if (ipCount == 1) {
            redisTemplate.expire(ipKey, Duration.ofMinutes(1));
        }
        
        if (ipCount > 10) { // 1分钟内超过10次请求
            return true;
        }
        
        // 检查用户请求频率
        Long userCount = redisTemplate.opsForValue().increment(userKey);
        if (userCount == 1) {
            redisTemplate.expire(userKey, Duration.ofMinutes(1));
        }
        
        if (userCount > 5) { // 1分钟内超过5次请求
            return true;
        }
        
        return false;
    }
}

库存预热与管理

1. 库存预热

@Component
public class StockWarmUpService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @EventListener
    public void handleSeckillStart(SeckillStartEvent event) {
        // 预热秒杀库存
        warmUpStock(event.getProductId(), event.getStockNum());
    }
    
    private void warmUpStock(Long productId, Integer stockNum) {
        String luaScript = """
            local stock_key = KEYS[1]
            local stock_num = tonumber(ARGV[1])
            local expire_time = tonumber(ARGV[2])
            
            redis.call('SET', stock_key, stock_num)
            if expire_time > 0 then
                redis.call('EXPIRE', stock_key, expire_time)
            end
            
            return stock_num
            """;
        
        String stockKey = "seckill:stock:" + productId;
        List<String> keys = Arrays.asList(stockKey);
        List<String> args = Arrays.asList(
            String.valueOf(stockNum),
            "7200" // 2小时过期
        );
        
        redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            keys,
            args.toArray(new String[0])
        );
        
        log.info("库存预热完成: productId={}, stock={}", productId, stockNum);
    }
}

2. 库存监控

@Component
public class StockMonitor {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Scheduled(fixedRate = 30000) // 每30秒检查一次
    public void monitorStock() {
        // 获取所有秒杀商品的库存
        Set<String> keys = redisTemplate.keys("seckill:stock:*");
        
        if (keys != null) {
            for (String key : keys) {
                String stockStr = (String) redisTemplate.opsForValue().get(key);
                if (stockStr != null) {
                    int stock = Integer.parseInt(stockStr);
                    
                    // 库存不足告警
                    if (stock < 10) {
                        log.warn("库存不足告警: key={}, stock={}", key, stock);
                        // 发送告警通知
                    }
                }
            }
        }
    }
}

超卖防护

1. 库存扣减验证

@Service
public class InventoryService {
    
    public boolean validateAndDeductInventory(Long productId, Integer num) {
        String luaScript = """
            local stock_key = KEYS[1]
            local buy_num = tonumber(ARGV[1])
            
            local current_stock = redis.call('GET', stock_key)
            if not current_stock then
                return {0, '商品不存在'}
            end
            
            current_stock = tonumber(current_stock)
            if current_stock < buy_num then
                return {0, '库存不足'}
            end
            
            -- 使用CAS模式扣减库存
            local new_stock = redis.call('DECRBY', stock_key, buy_num)
            
            if new_stock < 0 then
                -- 库存不够,回滚
                redis.call('INCRBY', stock_key, buy_num)
                return {0, '库存不足'}
            end
            
            return {1, '扣减成功', new_stock}
            """;
        
        String stockKey = "seckill:stock:" + productId;
        List<String> keys = Arrays.asList(stockKey);
        List<String> args = Arrays.asList(String.valueOf(num));
        
        List<Object> result = (List<Object>) redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, List.class),
            keys,
            args.toArray(new String[0])
        );
        
        if (result != null && result.size() > 0) {
            int code = Integer.parseInt(result.get(0).toString());
            return code == 1;
        }
        
        return false;
    }
}

性能优化策略

1. 连接池优化

spring:
  redis:
    lettuce:
      pool:
        max-active: 200    # 最大连接数
        max-idle: 50       # 最大空闲连接
        min-idle: 20       # 最小空闲连接
        max-wait: 2000ms   # 最大等待时间
    timeout: 1000ms        # 连接超时时间

2. 批量操作优化

@Service
public class BatchSeckillService {
    
    public List<SeckillResult> batchSeckill(List<SeckillRequest> requests) {
        // 使用Redis Pipeline批量执行
        List<Object> results = redisTemplate.executePipelined(
            (RedisCallback<Object>) connection -> {
                for (SeckillRequest request : requests) {
                    String stockKey = "seckill:stock:" + request.getProductId();
                    String userKey = "seckill:user:" + request.getProductId() + ":user:" + request.getUserId();
                    
                    // 执行Lua脚本
                    connection.eval(
                        seckillScript.getSha1().getBytes(),
                        2,
                        stockKey.getBytes(),
                        userKey.getBytes(),
                        String.valueOf(request.getNum()).getBytes(),
                        String.valueOf(request.getUserId()).getBytes(),
                        "1".getBytes()
                    );
                }
                return null;
            }
        );
        
        // 处理结果
        return processResults(results);
    }
}

异常处理与补偿

1. 订单补偿机制

@Component
public class OrderCompensationService {
    
    @Scheduled(fixedRate = 60000) // 每分钟检查一次
    public void checkAndCompensate() {
        // 检查Redis中成功但订单创建失败的秒杀记录
        // 实现补偿逻辑
    }
    
    public void compensateFailedOrder(Long productId, Long userId, Integer num) {
        // 补偿逻辑:回滚库存等
        String stockKey = "seckill:stock:" + productId;
        redisTemplate.opsForValue().increment(stockKey, num);
        
        log.warn("补偿库存: productId={}, userId={}, num={}", productId, userId, num);
    }
}

实际应用效果

通过Redis + Lua的秒杀系统,我们可以实现:

  • 高并发处理:单机可支撑数万QPS
  • 精准库存控制:防止超卖和重复购买
  • 快速响应:毫秒级响应时间
  • 可靠保障:原子性操作,数据一致性

注意事项

在实现秒杀系统时,需要注意以下几点:

  1. Redis集群:单机Redis存在单点故障,建议使用集群模式
  2. 内存管理:合理设置Redis内存限制,避免内存溢出
  3. 网络优化:确保应用服务器与Redis在同一机房
  4. 监控告警:建立完善的监控体系
  5. 安全防护:防止恶意刷单和攻击

最佳实践

  1. 预热库存:秒杀开始前预热Redis库存
  2. 分层防护:网关限流→应用限流→Redis限流
  3. 异步处理:秒杀成功后异步创建订单
  4. 降级策略:系统压力过大时快速降级
  5. 灰度发布:新功能先小范围验证

总结

Redis + Lua是实现高并发秒杀系统的绝佳方案,通过原子性的Lua脚本保证了数据的一致性,同时利用Redis的高性能特性支撑了高并发访问。

记住,秒杀系统的设计需要从多个维度考虑,既要保证高性能,也要确保数据准确,还要有完善的防护机制。

希望这篇文章对你有所帮助!如果你觉得有用,欢迎关注【服务端技术精选】公众号,获取更多后端技术干货。


标题:基于SpringBoot + Redis + Lua 实现高并发秒杀系统实战
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/01/23/1769148262044.html

    0 评论
avatar