【面试场景题】外卖平台如何扛住高峰期上千qps订单查询流量
文章目录
- 一、系统化优化方案
- 1. 流量分层:精准分流,避免"大水漫灌"
- 2. 系统防御自保:构建"弹性缓冲带"
- 3. 冷热数据分离:减少无效IO
- 4. 分库分表+读写分离:突破单机瓶颈
- 5. 多级缓存:拦截90%+查询
- 6. ES搜索引擎:优化复杂查询
- 二、优化方案的验证方法
- 1. 性能压测:模拟高峰场景
- 2. 线上监控:全链路指标跟踪
- 3. 灰度发布:逐步验证效果
- 三、预期效果
针对外卖平台高峰时段每分钟10万+订单查询的卡顿问题,结合流量分层、系统防御、数据分离等核心思路,以下是具体优化方案及验证方法:
一、系统化优化方案
1. 流量分层:精准分流,避免"大水漫灌"
将订单查询流量按"重要性+实时性"分层,引导至不同处理链路,避免高优先级请求被低优先级请求阻塞:
- 核心实时流量(如用户查"正在配送的订单"、商家查"待接单列表"):
- 特征:QPS占比约30%,响应时间要求<100ms,数据变更频繁(状态每秒更新)。
- 处理链路:本地缓存(Caffeine)→ 分布式缓存(Redis Cluster)→ MySQL主从(热数据)。
- 非核心准实时流量(如用户查"今日已完成订单"):
- 特征:QPS占比约50%,响应时间要求<500ms,数据10分钟内更新即可。
- 处理链路:分布式缓存 → MySQL从库(温数据)→ 定时任务异步更新缓存。
- 低频归档流量(如用户查"30天前订单"):
- 特征:QPS占比约20%,响应时间可放宽至2-3秒,数据几乎不变。
- 处理链路:ES搜索引擎(历史订单索引)→ 对象存储(OSS归档)。
实现:API网关通过请求参数(如时间范围、订单状态)自动路由至对应链路,例如:
// 网关路由逻辑示例
if (orderQuery.isRealTime()) {routeToHighPriorityService(); // 核心实时链路,分配独立线程池
} else if (orderQuery.isArchived()) {routeToEsService(); // 归档链路,走ES集群
} else {routeToNormalService(); // 准实时链路
}
2. 系统防御自保:构建"弹性缓冲带"
通过限流、熔断、隔离机制,确保系统在流量峰值下不崩溃,核心功能优先可用:
- 分级限流:
- 单用户限流:普通用户每分钟最多20次查询,商家账号每分钟最多100次(通过Redis的
incr
+过期时间实现)。- 接口总限流:核心实时接口设置阈值1万QPS(按服务器数量分摊),超过部分返回"稍等重试"(用Sentinel/Resilience4j实现)。
- 资源隔离:
- 线程池隔离:核心实时链路使用独立线程池(如200线程),避免被低频查询耗尽资源。
- 存储隔离:核心订单表与历史订单表使用不同的MySQL实例,防止历史查询拖垮实时库。
- 熔断降级:
- 当MySQL从库响应时间超500ms持续10秒,自动熔断准实时链路,降级为"仅返回缓存数据"(可能有延迟,但保证可用)。
- 当ES集群故障,归档查询直接返回"当前无法查询历史订单",不影响核心流程。
3. 冷热数据分离:减少无效IO
按时间维度拆分订单数据,将"热数据"(高频访问)和"冷数据"(低频访问)存储在不同介质:
- 热数据(近2小时订单):
- 存储:MySQL主从集群(分库分表,按"用户ID哈希+订单创建时间"分片,单表数据量控制在500万以内)。
- 索引:针对
user_id+create_time
、merchant_id+status
建立联合索引,覆盖90%的查询场景。
- 温数据(2小时~30天订单):
- 存储:MySQL从库(只读)+ Redis(缓存全量数据,过期时间7天)。
- 同步:通过Canal监听MySQL binlog,实时同步至Redis(避免查询穿透到DB)。
- 冷数据(30天前订单):
- 存储:ES集群(按"用户ID+时间范围"分片)+ OSS(原始数据归档)。
- 同步:每日凌晨通过Spark批处理将30天前数据从MySQL迁移至ES,同时删除MySQL中的冷数据。
4. 分库分表+读写分离:突破单机瓶颈
- 分库分表:
- 规则:按"用户ID % 8"分8个库,每个库按"订单创建时间(天)"分表(如
order_20240901
),总数据量分散到8×365≈3000张表,单表压力降低。- 中间件:用Sharding-JDBC路由查询,支持跨表分页(如按时间范围查多个分表)。
- 读写分离:
- 写操作(创建订单、更新状态)走主库,读操作(查询)走3个从库(通过MyCat配置读写分离,自动负载均衡)。
- 从库延迟控制:主从同步用"半同步复制",确保延迟<100ms(避免用户查到旧状态)。
5. 多级缓存:拦截90%+查询
构建"本地缓存→分布式缓存→DB"的多级缓存架构,最大化减少DB访问:
- L1:本地缓存(Caffeine):
- 存储:商家端"待接单列表"(1分钟过期)、用户端"最近3个订单"(5分钟过期)。
- 优势:单机查询延迟<1ms,无网络开销,拦截60%的重复查询。
- L2:分布式缓存(Redis Cluster):
- 存储:全量热数据+温数据(Key为
order:{orderId}
、user:{userId}:orders
),过期时间随数据热度动态调整(热数据30分钟,温数据24小时)。- 优化:热点Key(如头部商家的订单列表)单独分片,避免缓存雪崩;用布隆过滤器过滤"不存在的订单ID",防止缓存穿透。
- 缓存更新策略:
- 实时更新:订单状态变更时(如接单、配送),通过消息队列(Kafka)异步更新Redis和本地缓存(避免同步更新阻塞主流程)。
6. ES搜索引擎:优化复杂查询
针对"多条件筛选"(如商家查"昨天10点-12点的已完成订单")和历史订单查询,用ES替代MySQL:
- 索引设计:
- 主索引:
order_index
,包含order_id
、user_id
、merchant_id
、status
、create_time
等字段,按merchant_id
分片,create_time
排序。- 分词优化:对"订单备注"等文本字段用IK分词器,支持模糊查询(如用户查"带发票的订单")。
- 查询优化:
- 用
filter
代替query
(过滤不计算相关性,速度更快),结合range
查询时间范围,避免全索引扫描。- 开启ES缓存(
query_cache
),缓存高频查询语句(如商家重复查"待配送订单")。
二、优化方案的验证方法
通过"压测+监控+灰度发布"三重验证,确保优化效果达到预期:
1. 性能压测:模拟高峰场景
- 环境:搭建与生产一致的压测环境(8台应用服务器、3台MySQL从库、Redis Cluster 6节点、ES 3节点)。
- 工具:用JMeter/LoadRunner模拟流量:
- 核心实时查询:6000 QPS(60%),参数为最近1小时订单。
- 准实时查询:3000 QPS(30%),参数为1-30天订单。
- 归档查询:1000 QPS(10%),参数为30天前订单。
- 指标验证:
- 响应时间:P95<100ms(核心)、P95<500ms(准实时)、P95<2s(归档)。
- 资源使用率:MySQL从库CPU<70%,Redis命中率>95%,应用服务器线程池空闲率>30%。
- 错误率:所有接口错误率<0.1%(不含限流降级的正常返回)。
2. 线上监控:全链路指标跟踪
部署监控系统(Prometheus+Grafana+SkyWalking),实时监控以下指标:
- 流量层:各链路QPS占比、网关路由成功率、限流次数。
- 缓存层:Redis命中率(目标≥95%)、本地缓存命中率(目标≥60%)、缓存更新延迟(<1s)。
- 存储层:
- MySQL:从库查询耗时(P95<200ms)、主从延迟(<100ms)、连接池使用率(<80%)。
- ES:查询耗时(P95<1s)、索引分片负载均衡度。
- 服务层:接口响应时间、线程池活跃数、JVM GC频率(Young GC<1次/秒)。
3. 灰度发布:逐步验证效果
- 灰度策略:先对10%的用户(如某城市用户)开放优化后的系统,对比未灰度用户的卡顿率。
- 对比指标:
- 卡顿率:灰度组订单查询卡顿(响应>1s)比例下降≥90%。
- 资源节省:MySQL从库IOPS从1万降至3000以下,Redis带宽占用增加但未超上限。
- 全量发布:灰度验证24小时无异常后,按"30%→50%→100%"逐步全量发布,发布过程中实时监控指标,发现问题立即回滚。
三、预期效果
优化后,系统可支撑每分钟15万+订单查询(预留50%冗余),核心指标达到:
- 响应速度:90%的查询<100ms,无明显卡顿。
- 资源利用率:MySQL从库负载降低60%,Redis命中率稳定在96%以上。
- 可用性:高峰时段系统可用性≥99.99%,限流降级仅影响<5%的非核心查询。
通过这套方案,既能解决当前卡顿问题,又能支撑未来业务增长(如订单量翻倍)。