秒杀系统案例

wen java案例 1

本文目录导读:

秒杀系统案例

  1. 系统需求分析
  2. 系统架构设计
  3. 核心代码实现
  4. 性能优化方案
  5. 部署架构建议
  6. 实战提示

我来为你设计一个高并发秒杀系统的完整案例,这是一个经典的分布式系统设计难题,我会从架构到代码给出完整的解决方案。

系统需求分析

核心业务场景

  • 商品限量抢购(如1000件商品,10万人抢购)
  • 高并发访问(瞬时QPS可达10万+)
  • 需要防止超卖和重复购买

核心挑战

  1. 高并发:瞬间流量巨大
  2. 超卖问题:库存不能为负数
  3. 限购问题:同一用户限购1件
  4. 响应延迟:用户体验要求高

系统架构设计

┌─────────────┐     ┌──────────────┐     ┌──────────────┐
│  客户端/APP  │────▶│  负载均衡     │────▶│  静态资源CDN  │
└─────────────┘     │  (Nginx)     │     └──────────────┘
                    └──────────────┘
                           │
                    ┌──────▼──────┐
                    │  网关层      │
                    │ (限流/降级)  │
                    └──────┬──────┘
                           │
                    ┌──────▼────────────────────────────────┐
                    │            应用层 (集群部署)            │
                    │  秒杀接口 ─ 缓存预减 ─ 异步下单          │
                    └──────┬────────────────────────────────┘
                           │
              ┌────────────┼─────────────┐
              │            │             │
     ┌────────▼───┐ ┌─────▼─────┐ ┌─────▼─────┐
     │ Redis集群  │ │ 消息队列   │ │ 数据库集群  │
     │ (库存预减) │ │ (RabbitMQ)│ │ (最终库存) │
     └────────────┘ └───────────┘ └───────────┘

核心代码实现

秒杀接口设计(Controller层)

@RestController
@RequestMapping("/seckill")
public class SeckillController {
    @Autowired
    private SeckillService seckillService;
    @Autowired
    private RedisService redisService;
    /**
     * 秒杀接口
     * @param userId 用户ID
     * @param goodsId 商品ID
     */
    @PostMapping("/execute")
    public Result<String> seckill(@RequestParam Long userId, 
                                  @RequestParam Long goodsId) {
        // 1. 前置校验(限流拦截)
        if (!seckillService.preCheck(userId, goodsId)) {
            return Result.error("请求过于频繁或已参加过秒杀");
        }
        // 2. 执行秒杀
        boolean success = seckillService.executeSeckill(userId, goodsId);
        return success ? Result.success("秒杀成功") : Result.error("库存不足");
    }
}

秒杀核心服务(Service层)

@Service
@Slf4j
public class SeckillServiceImpl implements SeckillService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    @Autowired
    private GoodsMapper goodsMapper;
    private static final String STOCK_PREFIX = "seckill:stock:";
    private static final String USER_PREFIX = "seckill:user:";
    /**
     * 秒杀执行(核心逻辑)
     */
    @Override
    public boolean executeSeckill(Long userId, Long goodsId) {
        // 1. 检查用户是否已经秒杀过(防止重复秒杀)
        Boolean isExists = redisTemplate.hasKey(USER_PREFIX + goodsId + ":" + userId);
        if (Boolean.TRUE.equals(isExists)) {
            return false;
        }
        // 2. Redis库存预减(原子操作)
        Long stock = redisTemplate.opsForValue().decrement(STOCK_PREFIX + goodsId);
        if (stock < 0) {
            // 库存不足,回补库存
            redisTemplate.opsForValue().increment(STOCK_PREFIX + goodsId);
            return false;
        }
        // 3. 记录用户已秒杀(防止重复)
        redisTemplate.opsForValue().set(USER_PREFIX + goodsId + ":" + userId, 
                                       String.valueOf(System.currentTimeMillis()),
                                       24, TimeUnit.HOURS);
        // 4. 发送异步消息,生成订单
        try {
            SeckillMessage message = new SeckillMessage(userId, goodsId);
            rocketMQTemplate.convertAndSend("seckill-order-topic", message);
            return true;
        } catch (Exception e) {
            log.error("发送秒杀消息失败", e);
            // 消息发送失败,需要回滚
            rollbackStock(userId, goodsId);
            return false;
        }
    }
    /**
     * 回滚库存
     */
    private void rollbackStock(Long userId, Long goodsId) {
        redisTemplate.opsForValue().increment(STOCK_PREFIX + goodsId);
        redisTemplate.delete(USER_PREFIX + goodsId + ":" + userId);
    }
    /**
     * 前置校验
     */
    public boolean preCheck(Long userId, Long goodsId) {
        // 检查商品是否在秒杀窗口期
        SeckillGoods goods = goodsMapper.selectById(goodsId);
        if (goods == null) {
            return false;
        }
        long current = System.currentTimeMillis();
        if (current < goods.getStartTime() || current > goods.getEndTime()) {
            return false;
        }
        return true;
    }
}

Redis库存初始化

@Component
public class StockInitializer implements InitializingBean {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private GoodsMapper goodsMapper;
    private static final String STOCK_PREFIX = "seckill:stock:";
    @Override
    public void afterPropertiesSet() {
        // 系统启动时,将数据库库存加载到Redis
        List<Goods> goodsList = goodsMapper.selectAll();
        for (Goods goods : goodsList) {
            if (goods.getStock() > 0) {
                redisTemplate.opsForValue().set(
                    STOCK_PREFIX + goods.getId(),
                    String.valueOf(goods.getStock())
                );
            }
        }
    }
}

消息队列消费者(异步下单)

@Component
@RocketMQMessageListener(topic = "seckill-order-topic", 
                         consumerGroup = "seckill-order-group")
@Slf4j
public class SeckillOrderConsumer implements RocketMQListener<SeckillMessage> {
    @Autowired
    private OrderService orderService;
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Override
    public void onMessage(SeckillMessage message) {
        Long userId = message.getUserId();
        Long goodsId = message.getGoodsId();
        // 生成订单
        try {
            Order order = new Order();
            order.setUserId(userId);
            order.setGoodsId(goodsId);
            order.setOrderNo(generateOrderNo());
            order.setCreateTime(new Date());
            orderService.createOrder(order);
            // 订单成功后,更新数据库库存
            boolean updated = orderService.decreaseStock(goodsId);
            if (!updated) {
                // 数据库中库存不足,补偿
                log.error("数据库库存更新失败: goodsId={}", goodsId);
            }
        } catch (Exception e) {
            log.error("创建订单异常", e);
            // 可以加入死信队列或重试机制
        }
    }
    private String generateOrderNo() {
        return "ORDER" + System.currentTimeMillis() + 
               String.format("%04d", new Random().nextInt(10000));
    }
}

限流器实现(防止刷单)

@Component
public class RateLimiter {
    // 使用Guava RateLimiter或自研令牌桶
    private final Map<Long, RateLimiter> goodsRateLimiters = new ConcurrentHashMap<>();
    /**
     * 基于IP和用户ID的限流
     */
    public boolean tryAcquire(Long userId, String ip) {
        // 1. 用户级别限流:同一用户1秒内只能请求1次
        String userKey = "user:" + userId;
        if (!allow("rate:user:" + userId, 1, 1)) {
            return false;
        }
        // 2. IP级别限流:同一IP 1秒内只能请求10次
        if (!allow("rate:ip:" + ip, 10, 1)) {
            return false;
        }
        // 3. 商品级别限流:控制瞬时并发
        RateLimiter goodsLimiter = goodsRateLimiters.computeIfAbsent(
            goodsId, k -> RateLimiter.create(1000)); // 每秒1000个请求
        return goodsLimiter.tryAcquire();
    }
    /**
     * 基于Redis的滑动窗口限流
     */
    private boolean allow(String key, int limit, int windowSeconds) {
        long currentTime = System.currentTimeMillis();
        String member = String.valueOf(currentTime);
        // 使用ZSET实现滑动窗口
        Long count = redisTemplate.opsForZSet().count(key, 
                                 currentTime - windowSeconds * 1000, currentTime);
        if (count < limit) {
            redisTemplate.opsForZSet().add(key, member, currentTime);
            // 清理过期数据
            redisTemplate.expire(key, windowSeconds + 1, TimeUnit.SECONDS);
            return true;
        }
        return false;
    }
}

数据库表设计

-- 秒杀商品表
CREATE TABLE `seckill_goods` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `goods_name` varchar(100) NOT NULL COMMENT '商品名称',
  `stock` int(11) NOT NULL COMMENT '库存',
  `price` decimal(10,2) NOT NULL COMMENT '秒杀价格',
  `start_time` datetime NOT NULL COMMENT '秒杀开始时间',
  `end_time` datetime NOT NULL COMMENT '秒杀结束时间',
  `version` int(11) DEFAULT '0' COMMENT '乐观锁版本号',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='秒杀商品表';
-- 订单表
CREATE TABLE `seckill_order` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `order_no` varchar(50) NOT NULL COMMENT '订单号',
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `goods_id` bigint(20) NOT NULL COMMENT '商品ID',
  `status` tinyint(4) DEFAULT '0' COMMENT '订单状态',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_goods` (`user_id`,`goods_id`) -- 唯一约束防止重复下单
) ENGINE=InnoDB COMMENT='秒杀订单表';

性能优化方案

多级缓存策略

// 一级缓存:本地缓存(Caffeine)
// 二级缓存:Redis分布式缓存
// 三级缓存:数据库

数据库优化

-- 使用乐观锁防止超卖
UPDATE seckill_goods 
SET stock = stock - 1, version = version + 1 
WHERE id = #{goodsId} 
  AND stock > 0 
  AND version = #{version};
-- 如果更新失败,说明库存不足或版本冲突

异步订单处理

// 秒杀成功后,不立即创建订单
// 而是发送MQ消息,后台异步处理
// 用户可以在"我的订单"中查看结果

部署架构建议

生产环境建议:
├── 4台Nginx(负载均衡)
├── 4台前端服务器(静态资源)
├── 4台应用服务器(部署秒杀服务)
├── 1个Redis集群(3主3从)
├── 1个RocketMQ集群(2个节点)
└── 数据库集群(主从复制,读写分离)

实战提示

  1. 压测准备:使用JMeter进行压力测试
  2. 监控告警:使用Prometheus + Grafana
  3. 降级方案:秒杀结束/异常时,快速返回错误
  4. 数据一致性:Redis和数据库的最终一致性
  5. 限流策略:防止恶意刷接口

这个案例涵盖了秒杀系统的核心设计思路,你可以根据实际业务场景调整和优化,如果你有具体问题(比如某个模块的实现细节),我可以进一步详细讲解。

抱歉,评论功能暂时关闭!