外卖平台每天有1000万笔订单查询怎么优化?
作者:Java开发工程师
外卖平台项目中,每天要处理超过1000万笔订单查询请求 。这个量级对系统提出了很高的性能要求:高并发、低延迟、强一致性 。这篇文章将结合我的实际经验,从场景分析、架构优化,到核心代码实现,一步步拆解我们是如何做到高效处理这些请求的。
一、问题场景:每天1000万笔订单查询意味着什么?
1000万笔订单查询,平均下来:
- 每天:10,000,000 次
- 每小时:416,666 次
- 每分钟:6,944 次
- 每秒钟:约 115 次 QPS(查询/秒)
这还只是平均值,高峰期可能达到 10 倍 ,也就是 1000+ QPS
查询接口特点
- 查询接口:
GET /api/order/{orderId}
- 读多写少:95% 是查询操作
- 数据一致性要求高:订单状态必须准确(如支付状态、配送状态)
- 接口响应要求快:用户、商家、客服都会频繁使用此接口
二、优化目标与原则
我们的优化目标:
- 高并发支撑,高峰期不挂
- 响应时间稳定,P99 < 100ms
- 保证一致性,不返回错误状态
优化原则:
- 读写分离:将读操作尽可能从主库中剥离
- 冷热分离:大部分查询是"热订单",少数是历史订单
- 缓存优先:缓存能解决80%的请求
- 异步与延迟更新:牺牲一定实时性,换取系统稳定
三、架构设计思路
我们采用了以下架构策略:
客户端
│
▼
API 网关
│
▼
缓存层(Redis)
│
┌─┴────────┐
│ │
▼ ▼
MySQL主库 历史订单库(归档库)
查询优先级流程
- 查询 Redis 缓存(热点订单缓存)
- Redis 未命中 → 查询 MySQL 主库
- 订单状态为"已完成" → 异步同步至归档库
- 定期清理 Redis 缓存中过期订单
四、核心代码实现(Spring Boot + MyBatis + Redis)
下面我们以一个简化版本的订单查询接口来说明实现方式。
1. Redis 缓存配置
arduino
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, OrderDTO> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, OrderDTO> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
// 使用 Jackson 序列化对象
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(OrderDTO.class));
return template;
}
}
2. 订单查询 Service
java
@Service
public class OrderQueryService {
private static final String CACHE_KEY_PREFIX = "order:";
@Autowired
private RedisTemplate<String, OrderDTO> redisTemplate;
@Autowired
private OrderMapper orderMapper; // MyBatis Mapper
/**
* 查询订单主流程
* @param orderId 订单ID
* @return 订单详情
*/
public OrderDTO getOrderById(Long orderId) {
String cacheKey = CACHE_KEY_PREFIX + orderId;
// Step 1: 查询Redis缓存
OrderDTO cachedOrder = redisTemplate.opsForValue().get(cacheKey);
if (cachedOrder != null) {
return cachedOrder;
}
// Step 2: 查询数据库
Order order = orderMapper.selectById(orderId);
if (order == null) {
throw new OrderNotFoundException(orderId);
}
OrderDTO dto = convertToDTO(order);
// Step 3: 写入缓存,设定过期时间(如5分钟)
redisTemplate.opsForValue().set(cacheKey, dto, Duration.ofMinutes(5));
return dto;
}
private OrderDTO convertToDTO(Order order) {
// 简单转换,可用MapStruct优化
OrderDTO dto = new OrderDTO();
dto.setOrderId(order.getId());
dto.setStatus(order.getStatus());
dto.setAmount(order.getAmount());
dto.setCreatedAt(order.getCreatedAt());
return dto;
}
}
3. MyBatis Mapper 示例
less
@Mapper
public interface OrderMapper {
@Select("SELECT * FROM orders WHERE id = #{id}")
Order selectById(@Param("id") Long id);
}
4. Controller 接口暴露
less
@RestController
@RequestMapping("/api/order")
public class OrderController {
@Autowired
private OrderQueryService orderQueryService;
@GetMapping("/{orderId}")
public ResponseEntity<OrderDTO> getOrder(@PathVariable Long orderId) {
OrderDTO dto = orderQueryService.getOrderById(orderId);
return ResponseEntity.ok(dto);
}
}
五、更多优化技巧
1. 缓存击穿防护(防止热点key失效同时查询DB)
sql
// 使用Double Check + 分布式锁(Redisson)防止缓存击穿
2. 本地缓存(Caffeine)+ 分布式缓存(Redis)双层缓存
arduino
// Caffeine 提供毫秒级访问速度,适合高频访问订单
3. 数据归档与冷热分离
diff
- 热数据(未完成订单):保留在主库和Redis中
- 冷数据(已完成订单):每晚归档至历史库,并清理Redis缓存
六、高并发压测与优化
-
使用 JMeter/Locust 进行压测,模拟1000 QPS
-
监控Redis命中率、数据库连接数、接口响应时间
-
关键指标:
- Redis命中率 > 90%
- DB QPS 降低 80%
- P99 响应时间 <100ms
七、总结:优化是一场持久战
应对千万级别的订单查询请求,绝不是一次优化就能搞定的,而是一个持续演进的过程。我们需要:
- 理解业务特性
- 缓存优先思维
- 合理的架构分层
- 持续监控与压测
希望本文能为你在高并发查询优化的路上提供一点启发!