简单实现一个电商秒杀系统流量暴增的场景案例

72 阅读3分钟

案例场景:电商秒杀系统的流量暴增

在电商秒杀活动中,QPS(每秒查询量)经常会在短时间内暴增,例如在活动开始的一瞬间,商品页面的请求量会从常规的几百或几千 QPS,瞬间飙升到数万甚至更多。

以下是一个典型的场景和解决方案:


场景描述

  1. 背景
    • 某电商平台进行秒杀活动,用户抢购某一款限量商品。
    • 秒杀活动时间为晚上8点,8点之前用户会不断刷新页面获取库存状态,8点后用户开始提交订单。
    • 正常情况下,商品详情页面的 QPS 为 1000,订单提交接口的 QPS 为 500。
    • 活动开始后,商品页面的 QPS 提升至 10,000,订单提交接口的 QPS 提升至 5,000。
  2. 挑战
    • 页面查询和库存信息更新的高频请求。
    • 瞬时订单提交导致数据库压力激增。
    • 需要保证秒杀结果的一致性和高性能。

解决方案设计

  1. 使用缓存优化读取压力
    • 商品页面的库存状态和详情信息在活动前缓存到 Redis,减少对数据库的直接访问。
  2. 限流和降级保护
    • 商品详情页面限流,每秒最多允许一定数量的请求,其余请求返回友好的提示。
    • 对订单提交接口加限流,超出阈值的请求直接拒绝。
  3. 异步订单处理
    • 秒杀接口将请求写入队列(如 RabbitMQ 或 Kafka),订单生成异步处理。
  4. 预扣减库存
    • 秒杀时对库存进行预扣减,防止超卖。
  5. 使用分布式锁保护库存
    • Redis 分布式锁保证多个实例间的库存更新操作一致性。

Spring Boot 实现代码

1. 项目结构
src/main/java/com/example/seckill
    ├── controller
    │     └── SeckillController.java
    ├── service
    │     ├── SeckillService.java
    │     └── AsyncOrderService.java
    ├── config
    │     └── RedisConfig.java
    ├── queue
    │     ├── OrderQueueConsumer.java
    │     └── OrderQueueProducer.java
    ├── model
    │     ├── Product.java
    │     └── Order.java
    └── repository
          └── ProductRepository.java

2. 核心代码
RedisConfig.java
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}

SeckillController.java
@RestController
@RequestMapping("/seckill")
public class SeckillController {

    @Autowired
    private SeckillService seckillService;

    @GetMapping("/product/{id}")
    public ResponseEntity<Object> getProduct(@PathVariable Long id) {
        return ResponseEntity.ok(seckillService.getProductDetails(id));
    }

    @PostMapping("/order/{id}")
    public ResponseEntity<Object> placeOrder(@PathVariable Long id, @RequestParam Long userId) {
        try {
            boolean success = seckillService.processOrder(id, userId);
            if (success) {
                return ResponseEntity.ok("Order placed successfully");
            } else {
                return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("Sold out or too many requests");
            }
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error occurred");
        }
    }
}

SeckillService.java
@Service
public class SeckillService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private OrderQueueProducer orderQueueProducer;

    private static final String STOCK_KEY = "product_stock_";

    public Object getProductDetails(Long productId) {
        // Read from cache
        return redisTemplate.opsForValue().get("product_" + productId);
    }

    public boolean processOrder(Long productId, Long userId) {
        String stockKey = STOCK_KEY + productId;

        // Decrement stock atomically
        Long stock = redisTemplate.opsForValue().decrement(stockKey);
        if (stock == null || stock < 0) {
            return false; // Sold out
        }

        // Send order request to queue
        orderQueueProducer.sendOrder(productId, userId);

        return true;
    }
}

AsyncOrderService.java
@Service
public class AsyncOrderService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Transactional
    public void createOrder(Long productId, Long userId) {
        // Create order in database
        // Decrease stock in database
        // This method should only process requests from the queue
    }
}

OrderQueueProducer.java
@Component
public class OrderQueueProducer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendOrder(Long productId, Long userId) {
        Map<String, Object> message = new HashMap<>();
        message.put("productId", productId);
        message.put("userId", userId);
        rabbitTemplate.convertAndSend("order.queue", message);
    }
}

OrderQueueConsumer.java
@Component
public class OrderQueueConsumer {

    @Autowired
    private AsyncOrderService asyncOrderService;

    @RabbitListener(queues = "order.queue")
    public void consumeOrder(Map<String, Object> message) {
        Long productId = (Long) message.get("productId");
        Long userId = (Long) message.get("userId");

        asyncOrderService.createOrder(productId, userId);
    }
}

ProductRepository.java
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT p FROM Product p WHERE p.id = :id")
    Product findProductForUpdate(@Param("id") Long id);
}

总结

  • 缓存:Redis 用于缓存商品详情和库存信息。
  • 限流:秒杀接口实现了限流逻辑,超出请求直接拒绝。
  • 队列:RabbitMQ 确保订单异步处理,削峰填谷。
  • 分布式锁:保证库存更新的一致性。
  • 高可用性:支持水平扩展。

这个设计可以有效应对秒杀场景下的 QPS 暴增。