Spring Boot + Redis + RabbitMQ 高并发秒杀系统实战

 💡 本文基于生产级秒杀系统架构,从零到一搭建一个支撑万级 QPS 的秒杀系统,涵盖 Redis 预扣库存、RabbitMQ 异步下单、分布式锁等核心技术,附完整代码。

一、项目背景与架构设计

秒杀系统的核心挑战是瞬时高并发超卖问题。传统数据库直击方案在万级 QPS 下会直接被打垮。本文采用以下架构:

用户请求 → Nginx → Spring Boot → Redis(预扣库存) → RabbitMQ(削峰) → MySQL(持久化)

核心设计思路

  1. Redis 预扣库存:利用 Redis 单线程特性保证原子性,避免超卖
  2. RabbitMQ 削峰:将同步下单转为异步,保护后端数据库
  3. 双重校验:Redis 预扣 + MySQL 事务确认,保证数据一致性

二、项目环境

组件版本
Spring Boot2.7.x
Redis6.2
RabbitMQ3.9
MySQL8.0

三、核心代码实现

1. Maven 依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.2</version>
    </dependency></dependencies>

2. Redis 配置类

@Configurationpublic class RedisConfig {    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);        
        // 使用 Jackson2JsonRedisSerializer 序列化 value
        Jackson2JsonRedisSerializer<Object> serializer = 
            new Jackson2JsonRedisSerializer<>(Object.class);        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
            ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);
        
        template.setValueSerializer(serializer);
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();        return template;
    }
}

3. 秒杀核心 Service(Redis 预扣库存)

@Service@Slf4jpublic class SeckillService {    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;    
    @Autowired
    private RabbitTemplate rabbitTemplate;    
    @Autowired
    private OrderMapper orderMapper;    
    // 商品库存 Key 前缀
    private static final String STOCK_KEY_PREFIX = "seckill:stock:";    // 已秒杀用户 Set Key 前缀
    private static final String USER_SET_PREFIX = "seckill:users:";    
    /**
     * 秒杀核心方法
     */
    public Result seckill(Long userId, Long goodsId) {        String stockKey = STOCK_KEY_PREFIX + goodsId;        String userSetKey = USER_SET_PREFIX + goodsId;        
        // 1. 校验用户是否已秒杀过(防止重复购买)
        Boolean isMember = redisTemplate.opsForSet().isMember(userSetKey, userId);        if (Boolean.TRUE.equals(isMember)) {            return Result.error("您已参与过该秒杀活动");
        }        
        // 2. Redis 预扣库存(原子操作)
        Long remaining = redisTemplate.opsForValue().decrement(stockKey);        if (remaining == null || remaining < 0) {            // 库存不足,回补
            redisTemplate.opsForValue().increment(stockKey);            return Result.error("秒杀失败,库存不足");
        }        
        // 3. 记录用户已秒杀
        redisTemplate.opsForSet().add(userSetKey, userId);        
        // 4. 发送消息到 RabbitMQ 异步下单
        SeckillMessage message = new SeckillMessage(userId, goodsId);
        rabbitTemplate.convertAndSend(            "seckill.exchange", 
            "seckill.route", 
            message
        );
        
        log.info("用户 {} 秒杀商品 {} 进入队列", userId, goodsId);        return Result.success("秒杀成功,订单处理中");
    }
}

4. RabbitMQ 配置

@Configurationpublic class RabbitMQConfig {    
    // 交换机
    public static final String SECKILL_EXCHANGE = "seckill.exchange";    // 队列
    public static final String SECKILL_QUEUE = "seckill.queue";    // 路由键
    public static final String SECKILL_ROUTING_KEY = "seckill.route";    
    @Bean
    public DirectExchange seckillExchange() {        return new DirectExchange(SECKILL_EXCHANGE);
    }    
    @Bean
    public Queue seckillQueue() {        return QueueBuilder.durable(SECKILL_QUEUE)
            .withArgument("x-message-ttl"60000)  // 消息60秒过期
            .withArgument("x-max-length"100000)  // 最大队列长度
            .build();
    }    
    @Bean
    public Binding binding() {        return BindingBuilder.bind(seckillQueue())
            .to(seckillExchange())
            .with(SECKILL_ROUTING_KEY);
    }
}

5. RabbitMQ 消费者(MySQL 落库)

@Component@Slf4jpublic class SeckillConsumer {    
    @Autowired
    private OrderMapper orderMapper;    
    @Autowired
    private GoodsMapper goodsMapper;    
    @RabbitListener(queues = "seckill.queue")
    public void handleSeckillMessage(SeckillMessage message, Channel channel, 
                                     @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {        try {            Long userId = message.getUserId();            Long goodsId = message.getGoodsId();            
            // 1. 数据库库存校验(兜底)
            Goods goods = goodsMapper.selectById(goodsId);            if (goods == null || goods.getStock() <= 0) {
                log.warn("商品 {} 库存不足", goodsId);
                channel.basicAck(deliveryTag, false);                return;
            }            
            // 2. 创建订单
            Order order = new Order();
            order.setUserId(userId);
            order.setGoodsId(goodsId);
            order.setStatus(0); // 待支付
            order.setCreateTime(LocalDateTime.now());
            orderMapper.insert(order);            
            // 3. 扣减数据库库存(乐观锁防止超卖)
            int updated = goodsMapper.decreaseStock(goodsId);            if (updated == 0) {
                log.warn("商品 {} 数据库扣库存失败", goodsId);
            }
            
            log.info("用户 {} 秒杀商品 {} 下单成功,订单ID: {}", 
                userId, goodsId, order.getId());            
            // 手动 ACK
            channel.basicAck(deliveryTag, false);
            
        } catch (Exception e) {
            log.error("处理秒杀消息失败", e);            // 重试3次后进入死信队列
            channel.basicNack(deliveryTag, false, true);
        }
    }
}

6. 商品 Mapper(乐观锁扣库存)

@Mapperpublic interface GoodsMapper extends BaseMapper<Goods> {    
    @Update("UPDATE goods SET stock = stock - 1, " +
            "version = version + 1 WHERE id = #{goodsId} " +
            "AND stock > 0 AND version = #{version}")
    int decreaseStockWithVersion(@Param("goodsId") Long goodsId, 
                                 @Param("version") Integer version);    
    @Update("UPDATE goods SET stock = stock - 1 " +
            "WHERE id = #{goodsId} AND stock > 0")
    int decreaseStock(@Param("goodsId") Long goodsId);
}

7. Controller 层

@RestController@RequestMapping("/seckill")@Slf4jpublic class SeckillController {    
    @Autowired
    private SeckillService seckillService;    
    @PostMapping("/{goodsId}")
    public Result doSeckill(@PathVariable Long goodsId, 
                            @RequestHeader("X-User-Id") Long userId) {        return seckillService.seckill(userId, goodsId);
    }    
    /**
     * 初始化库存到 Redis
     */
    @PostMapping("/init/{goodsId}")
    public Result initStock(@PathVariable Long goodsId) {        // 从数据库加载库存到 Redis
        // ...
        return Result.success("库存初始化完成");
    }
}

四、关键优化点

1. 防止超卖的双重保障

// 第一层:Redis 原子递减Long remaining = redisTemplate.opsForValue().decrement(stockKey);// 第二层:MySQL 乐观锁int updated = goodsMapper.decreaseStockWithVersion(goodsId, version);

2. 接口限流(基于 Redis 令牌桶)

public boolean tryAcquire(String key, int qps) {    String luaScript = 
        "local key = KEYS[1] " +        "local limit = tonumber(ARGV[1]) " +        "local current = tonumber(redis.call('get', key) or '0') " +        "if current + 1 > limit then " +        "   return 0 " +        "else " +        "   redis.call('incrby', key, 1) " +        "   redis.call('expire', key, 1) " +        "   return 1 " +        "end";    
    Long result = redisTemplate.execute(        new DefaultRedisScript<>(luaScript, Long.class),
        Collections.singletonList(key),
        String.valueOf(qps)
    );    return result != null && result == 1L;
}

3. 防刷策略

// 用户维度限流:同一用户5秒内只能请求1String userLimitKey = "seckill:limit:" + userId;if (redisTemplate.opsForValue().setIfAbsent(userLimitKey, "1"5, TimeUnit.SECONDS)) {    // 允许请求} else {    return Result.error("操作过于频繁,请稍后再试");
}

五、性能压测数据

使用 JMeter 进行压测,模拟 10000 并发:

方案QPS平均响应时间错误率
直击数据库8001200ms35%
Redis 预扣+MQ1200015ms0%

六、生产环境踩坑总结

⚠️ 踩坑1:Redis 与数据库库存不一致

解决方案:定时任务对账,以数据库为准修复 Redis 库存

⚠️ 踩坑2:RabbitMQ 消息堆积

解决方案:增加消费者实例数 + 设置队列 TTL 和死信队列

⚠️ 踩坑3:用户重复秒杀

解决方案:Redis Set 记录已参与用户 + 数据库唯一索引兜底

七、完整项目结构

src/main/java/com/example/seckill/
├── config/
│   ├── RedisConfig.java
│   └── RabbitMQConfig.java
├── controller/
│   └── SeckillController.java
├── service/
│   └── SeckillService.java
├── mq/
│   └── SeckillConsumer.java
├── mapper/
│   ├── GoodsMapper.java
│   └── OrderMapper.java
└── entity/
    ├── Goods.java
    ├── Order.java
    └── SeckillMessage.java

本文由 摸鱼不慌 发布,转载请注明出处。
文章链接:生产级实战:基于Redis Lua的分布式幂等框架设计与实现 - 摸鱼不慌

0
0
0
0
评论
未登录
暂无评论