案例场景:电商秒杀系统的流量暴增
在电商秒杀活动中,QPS(每秒查询量)经常会在短时间内暴增,例如在活动开始的一瞬间,商品页面的请求量会从常规的几百或几千 QPS,瞬间飙升到数万甚至更多。
以下是一个典型的场景和解决方案:
场景描述
- 背景:
- 某电商平台进行秒杀活动,用户抢购某一款限量商品。
- 秒杀活动时间为晚上8点,8点之前用户会不断刷新页面获取库存状态,8点后用户开始提交订单。
- 正常情况下,商品详情页面的 QPS 为 1000,订单提交接口的 QPS 为 500。
- 活动开始后,商品页面的 QPS 提升至 10,000,订单提交接口的 QPS 提升至 5,000。
- 挑战:
- 页面查询和库存信息更新的高频请求。
- 瞬时订单提交导致数据库压力激增。
- 需要保证秒杀结果的一致性和高性能。
解决方案设计
- 使用缓存优化读取压力:
- 商品页面的库存状态和详情信息在活动前缓存到 Redis,减少对数据库的直接访问。
- 限流和降级保护:
- 商品详情页面限流,每秒最多允许一定数量的请求,其余请求返回友好的提示。
- 对订单提交接口加限流,超出阈值的请求直接拒绝。
- 异步订单处理:
- 秒杀接口将请求写入队列(如 RabbitMQ 或 Kafka),订单生成异步处理。
- 预扣减库存:
- 秒杀时对库存进行预扣减,防止超卖。
- 使用分布式锁保护库存:
- 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 暴增。