💡 本文基于生产级秒杀系统架构,从零到一搭建一个支撑万级 QPS 的秒杀系统,涵盖 Redis 预扣库存、RabbitMQ 异步下单、分布式锁等核心技术,附完整代码。
一、项目背景与架构设计
秒杀系统的核心挑战是瞬时高并发与超卖问题。传统数据库直击方案在万级 QPS 下会直接被打垮。本文采用以下架构:
用户请求 → Nginx → Spring Boot → Redis(预扣库存) → RabbitMQ(削峰) → MySQL(持久化)
核心设计思路:
- Redis 预扣库存:利用 Redis 单线程特性保证原子性,避免超卖
- RabbitMQ 削峰:将同步下单转为异步,保护后端数据库
- 双重校验:Redis 预扣 + MySQL 事务确认,保证数据一致性
二、项目环境
组件 |
版本 |
Spring Boot |
2.7.x |
Redis |
6.2 |
RabbitMQ |
3.9 |
MySQL |
8.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秒内只能请求1次String userLimitKey = "seckill:limit:" + userId;if (redisTemplate.opsForValue().setIfAbsent(userLimitKey, "1", 5, TimeUnit.SECONDS)) { // 允许请求} else { return Result.error("操作过于频繁,请稍后再试"); }
五、性能压测数据
使用 JMeter 进行压测,模拟 10000 并发:
方案 |
QPS |
平均响应时间 |
错误率 |
直击数据库 |
800 |
1200ms |
35% |
Redis 预扣+MQ |
12000 |
15ms |
0% |
六、生产环境踩坑总结
⚠️ 踩坑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
本文由 摸鱼不慌 发布,转载请注明出处。