SpringBoot + Redis + Lua:秒杀系统设计,超卖防护 + 库存预热 + 流量削峰全方案

引言:秒杀系统的挑战

双11、618等大促活动,用户疯狂点击购买按钮,结果出现超卖,库存变成负数?或者系统直接被高并发请求压垮,用户看到的都是错误页面?再或者大量的无效请求消耗了系统资源,真正想购买的用户反而买不到?

这就是秒杀系统的经典难题。传统的电商系统架构无法应对瞬间爆发的高并发请求。今天我们就来聊聊如何用SpringBoot + Redis + Lua构建一个高并发的秒杀系统,实现超卖防护、库存预热、流量削峰。

秒杀系统的痛点分析

1. 超卖问题

在高并发场景下,多个请求同时读取库存,判断库存充足,然后都执行减库存操作,导致库存变为负数。

2. 系统崩溃

大量并发请求直接打到数据库,数据库连接数耗尽,系统响应变慢甚至崩溃。

3. 流量不均

用户可能在活动开始瞬间集中访问,造成流量冲击。

4. 库存不一致

数据库和缓存数据不一致,导致数据错误。

技术选型:为什么选择这些技术?

Redis:高性能缓存与原子操作

Redis具有以下优势:

  • 高性能:内存操作,响应快
  • 原子性:保证并发安全
  • 丰富的数据结构:支持字符串、哈希、列表等
  • 分布式支持:支持集群部署

Lua:原子性脚本执行

Lua脚本的优势:

  • 原子性:脚本执行期间其他命令无法执行
  • 高效性:减少网络往返
  • 灵活性:可以实现复杂逻辑

SpringBoot:快速开发与集成

SpringBoot提供了:

  • 自动配置:快速集成Redis
  • 注解支持:@Cacheable等便捷注解
  • AOP支持:便于统一处理

系统架构设计

我们的秒杀系统主要包括以下几个模块:

  1. 库存预热:提前加载库存到Redis
  2. 流量控制:限制请求频率
  3. 库存扣减:原子性扣减库存
  4. 订单处理:异步处理订单
  5. 结果返回:快速返回结果
  6. 补偿机制:处理异常情况

核心实现思路

1. 库存预热

在秒杀活动开始前,将商品库存加载到Redis:

@Service
public class StockPreheatService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private ProductRepository productRepository;
    
    /**
     * 预热库存
     */
    public void preheatStock(Long productId) {
        // 从数据库获取商品信息
        Product product = productRepository.findById(productId);
        
        // 将库存信息加载到Redis
        String stockKey = "stock:" + productId;
        redisTemplate.opsForValue().set(stockKey, product.getStock(), Duration.ofHours(2));
        
        // 设置商品信息
        String productKey = "product:" + productId;
        redisTemplate.opsForValue().set(productKey, product, Duration.ofHours(2));
        
        // 初始化秒杀状态
        String seckillStatusKey = "seckill:status:" + productId;
        redisTemplate.opsForValue().set(seckillStatusKey, "ready", Duration.ofHours(2));
    }
    
    /**
     * 批量预热库存
     */
    public void batchPreheatStock(List<Long> productIds) {
        productIds.parallelStream().forEach(this::preheatStock);
    }
}

2. Lua脚本实现原子性扣减

使用Lua脚本保证库存扣减的原子性:

@Component
public class SeckillLuaScript {
    
    private static final String SECKILL_SCRIPT = 
        "local stockKey = KEYS[1]\n" +
        "local orderId = ARGV[1]\n" +
        "local userId = ARGV[2]\n" +
        "\n" +
        "-- 检查库存\n" +
        "local stock = redis.call('GET', stockKey)\n" +
        "if not stock then\n" +
        "    return {code = 1, message = '库存不存在'}\n" +
        "end\n" +
        "\n" +
        "stock = tonumber(stock)\n" +
        "if stock <= 0 then\n" +
        "    return {code = 2, message = '库存不足'}\n" +
        "end\n" +
        "\n" +
        "-- 扣减库存\n" +
        "redis.call('DECR', stockKey)\n" +
        "\n" +
        "-- 记录用户秒杀记录,防止重复秒杀\n" +
        "local userKey = 'seckill:user:' .. ARGV[2] .. ':' .. KEYS[1]\n" +
        "local exists = redis.call('GET', userKey)\n" +
        "if exists then\n" +
        "    -- 库存回滚\n" +
        "    redis.call('INCR', stockKey)\n" +
        "    return {code = 3, message = '您已参与过本次秒杀'}\n" +
        "end\n" +
        "\n" +
        "-- 记录用户秒杀记录\n" +
        "redis.call('SET', userKey, '1', 'EX', 86400) -- 24小时过期\n" +
        "\n" +
        "return {code = 0, message = '秒杀成功', stock = stock - 1}";
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 执行秒杀
     */
    public SeckillResult executeSeckill(Long productId, Long userId) {
        String stockKey = "stock:" + productId;
        String[] keys = new String[]{stockKey};
        String[] args = new String[]{String.valueOf(System.currentTimeMillis()), String.valueOf(userId)};
        
        try {
            RedisScript<String> script = new DefaultRedisScript<>(SECKILL_SCRIPT, String.class);
            String result = (String) redisTemplate.execute(script, Arrays.asList(keys), args);
            
            // 解析结果
            return parseResult(result);
        } catch (Exception e) {
            log.error("秒杀执行失败", e);
            return SeckillResult.failure("系统异常,请稍后再试");
        }
    }
    
    private SeckillResult parseResult(String result) {
        // 解析Lua脚本返回的结果
        // 实际项目中需要根据具体格式解析
        return JSON.parseObject(result, SeckillResult.class);
    }
}

3. 流量削峰

使用令牌桶算法控制请求流量:

@Component
public class TrafficShapingService {
    
    private final Map<String, RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();
    
    /**
     * 获取令牌
     */
    public boolean tryAcquire(String key, int permits) {
        RateLimiter rateLimiter = rateLimiterMap.computeIfAbsent(key, 
            k -> RateLimiter.create(100)); // 每秒100个令牌
        
        return rateLimiter.tryAcquire(permits);
    }
    
    /**
     * 动态调整限流参数
     */
    public void updateRate(String key, double permitsPerSecond) {
        RateLimiter rateLimiter = rateLimiterMap.computeIfAbsent(key,
            k -> RateLimiter.create(permitsPerSecond));
        
        rateLimiter.setRate(permitsPerSecond);
    }
}

4. 秒杀服务

@Service
public class SeckillService {
    
    @Autowired
    private SeckillLuaScript seckillLuaScript;
    
    @Autowired
    private TrafficShapingService trafficShapingService;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private OrderProducer orderProducer; // 消息队列生产者
    
    public SeckillResult seckill(Long productId, Long userId) {
        // 1. 流量控制
        if (!trafficShapingService.tryAcquire("seckill:" + productId, 1)) {
            return SeckillResult.failure("当前参与人数过多,请稍后再试");
        }
        
        // 2. 检查秒杀活动状态
        String seckillStatusKey = "seckill:status:" + productId;
        String status = (String) redisTemplate.opsForValue().get(seckillStatusKey);
        if (!"active".equals(status)) {
            return SeckillResult.failure("秒杀活动未开始或已结束");
        }
        
        // 3. 执行秒杀(原子性操作)
        SeckillResult result = seckillLuaScript.executeSeckill(productId, userId);
        
        if (result.isSuccess()) {
            // 4. 异步创建订单
            OrderMessage orderMessage = new OrderMessage(productId, userId, result.getOrderId());
            orderProducer.sendOrderMessage(orderMessage);
        }
        
        return result;
    }
}

5. 消息队列处理订单

@Component
public class OrderConsumer {
    
    @Autowired
    private OrderService orderService;
    
    @RabbitListener(queues = "seckill.order.queue")
    public void processOrder(OrderMessage message, Channel channel, @Header Map<String, Object> headers) {
        try {
            // 创建订单
            Order order = new Order();
            order.setProductId(message.getProductId());
            order.setUserId(message.getUserId());
            order.setOrderTime(new Date());
            order.setStatus(OrderStatus.PENDING);
            
            orderService.createOrder(order);
            
            // 确认消息
            Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
            channel.basicAck(deliveryTag, false);
            
        } catch (Exception e) {
            log.error("处理订单失败", e);
            try {
                // 拒绝消息,重新入队
                Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
                channel.basicNack(deliveryTag, false, true);
            } catch (IOException ioException) {
                log.error("拒绝消息失败", ioException);
            }
        }
    }
}

高级特性实现

1. 库存分片

@Component
public class ShardedStockService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 分片库存扣减
     */
    public boolean deductStock(Long productId, int quantity) {
        String baseKey = "stock:" + productId;
        int shardCount = 10; // 分片数量
        
        // 计算需要扣减的分片
        int remaining = quantity;
        for (int i = 0; i < shardCount && remaining > 0; i++) {
            String shardKey = baseKey + ":shard:" + i;
            Long currentStock = redisTemplate.opsForValue().increment(shardKey, -remaining);
            
            if (currentStock < 0) {
                // 回滚已扣减的库存
                redisTemplate.opsForValue().increment(shardKey, remaining + currentStock);
                remaining = -currentStock.intValue();
            } else {
                remaining = 0;
            }
        }
        
        return remaining == 0;
    }
}

2. 用户限流

@Component
public class UserRateLimitService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 用户请求限流
     */
    public boolean isAllowed(Long userId, String action, int limit, int windowSeconds) {
        String key = "rate_limit:" + userId + ":" + action;
        String luaScript = 
            "local key = KEYS[1]\n" +
            "local limit = tonumber(ARGV[1])\n" +
            "local window = tonumber(ARGV[2])\n" +
            "local current = redis.call('GET', key)\n" +
            "if current == false then\n" +
            "    redis.call('SET', key, 1)\n" +
            "    redis.call('EXPIRE', key, window)\n" +
            "    return 1\n" +
            "end\n" +
            "current = tonumber(current)\n" +
            "if current < limit then\n" +
            "    redis.call('INCR', key)\n" +
            "    return 1\n" +
            "else\n" +
            "    return 0\n" +
            "end";
        
        RedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
        Long result = (Long) redisTemplate.execute(script, Arrays.asList(key), 
            String.valueOf(limit), String.valueOf(windowSeconds));
        
        return result == 1;
    }
}

3. 预售模式

@Service
public class PreSaleService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 预售秒杀
     */
    public SeckillResult preSaleSeckill(Long productId, Long userId, String deposit) {
        String preSaleKey = "presale:" + productId;
        String depositKey = "deposit:" + productId + ":" + userId;
        
        // 检查是否已支付定金
        String userDeposit = (String) redisTemplate.opsForValue().get(depositKey);
        if (!deposit.equals(userDeposit)) {
            return SeckillResult.failure("请先支付定金");
        }
        
        // 执行秒杀逻辑
        return executeSeckill(productId, userId);
    }
}

性能优化建议

1. 连接池优化

spring:
  redis:
    lettuce:
      pool:
        max-active: 200
        max-idle: 50
        min-idle: 10
        max-wait: 2000ms
    timeout: 1000ms

2. 批量操作

public class BatchOperationService {
    
    public void batchUpdateStock(List<StockUpdate> updates) {
        String luaScript = 
            "for i = 1, #KEYS, 1 do\n" +
            "    redis.call('DECRBY', KEYS[i], ARGV[i])\n" +
            "end\n" +
            "return 'OK'";
        
        List<String> keys = updates.stream().map(u -> "stock:" + u.getProductId()).collect(Collectors.toList());
        List<String> args = updates.stream().map(u -> String.valueOf(u.getQuantity())).collect(Collectors.toList());
        
        redisTemplate.execute(new DefaultRedisScript<>(luaScript, String.class), keys, args.toArray(new String[0]));
    }
}

安全考虑

1. 防刷机制

@Component
public class AntiBrushService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public boolean isBrushing(String ip, Long userId) {
        String ipKey = "brush:ip:" + ip;
        String userKey = "brush:user:" + userId;
        
        // 检查IP请求频率
        Long ipCount = redisTemplate.opsForValue().increment(ipKey);
        if (ipCount > 10) { // 10秒内超过10次请求
            redisTemplate.expire(ipKey, Duration.ofSeconds(10));
            return true;
        }
        
        // 检查用户请求频率
        Long userCount = redisTemplate.opsForValue().increment(userKey);
        if (userCount > 5) { // 10秒内超过5次请求
            redisTemplate.expire(userKey, Duration.ofSeconds(10));
            return true;
        }
        
        // 设置过期时间
        redisTemplate.expire(ipKey, Duration.ofSeconds(10));
        redisTemplate.expire(userKey, Duration.ofSeconds(10));
        
        return false;
    }
}

2. 接口防重

@RestController
public class SeckillController {
    
    @Autowired
    private SeckillService seckillService;
    
    @Autowired
    private AntiBrushService antiBrushService;
    
    @PostMapping("/seckill/{productId}")
    public ResponseEntity<SeckillResult> seckill(
            @PathVariable Long productId,
            @RequestParam Long userId,
            @RequestHeader("X-Real-IP") String ip,
            @RequestParam String requestId) { // 防重标识
        
        // 防刷检查
        if (antiBrushService.isBrushing(ip, userId)) {
            return ResponseEntity.ok(SeckillResult.failure("请求过于频繁"));
        }
        
        // 防重检查
        String requestKey = "request:" + requestId;
        if (redisTemplate.hasKey(requestKey)) {
            return ResponseEntity.ok(SeckillResult.failure("请勿重复提交"));
        }
        
        // 设置请求标识,防止重复
        redisTemplate.opsForValue().set(requestKey, "1", Duration.ofMinutes(5));
        
        SeckillResult result = seckillService.seckill(productId, userId);
        return ResponseEntity.ok(result);
    }
}

监控与告警

1. 关键指标监控

@Component
public class SeckillMetricsCollector {
    
    private final MeterRegistry meterRegistry;
    
    public void recordSeckillAttempt(Long productId, boolean success) {
        Counter.builder("seckill_attempts_total")
            .tag("product_id", productId.toString())
            .tag("result", success ? "success" : "failure")
            .register(meterRegistry)
            .increment();
    }
    
    public void recordStock(Long productId, int stock) {
        Gauge.builder("seckill_stock")
            .tag("product_id", productId.toString())
            .register(meterRegistry, stock, s -> (double) s);
    }
}

2. 异常告警

@Component
public class SeckillAlertService {
    
    public void checkStockAlert(Long productId, int currentStock) {
        if (currentStock < 100) { // 库存不足告警
            alertService.sendAlert("库存不足告警", 
                String.format("商品[%d]库存不足,当前库存: %d", productId, currentStock));
        }
    }
}

最佳实践

1. 分阶段限流

public class PhasedRateLimitService {
    
    public double getRateLimit(String phase) {
        switch (phase) {
            case "before":
                return 10;  // 活动前:10 QPS
            case "start":
                return 1000; // 活动开始:1000 QPS
            case "peak":
                return 5000; // 高峰期:5000 QPS
            case "end":
                return 100;  // 活动结束:100 QPS
            default:
                return 100;
        }
    }
}

2. 熔断降级

@Service
public class SeckillCircuitBreaker {
    
    private final CircuitBreaker circuitBreaker;
    
    public SeckillResult seckillWithCircuitBreaker(Long productId, Long userId) {
        return circuitBreaker.executeSupplier(() -> seckillService.seckill(productId, userId));
    }
}

总结

通过SpringBoot + Redis + Lua的组合,我们可以构建一个高并发的秒杀系统。关键在于:

  1. 库存预热:提前加载库存到Redis
  2. 原子操作:使用Lua脚本保证扣库存原子性
  3. 流量控制:通过限流算法削峰填谷
  4. 异步处理:订单处理异步化
  5. 安全防护:防刷、防重、防超卖

记住,秒杀系统不是一蹴而就的,需要根据实际业务场景持续优化。掌握了这些技巧,你就能构建一个稳定高效的秒杀系统,告别超卖和系统崩溃的烦恼。


标题:SpringBoot + Redis + Lua:秒杀系统设计,超卖防护 + 库存预热 + 流量削峰全方案
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/01/03/1767450416770.html

    0 评论
avatar