订单表按 user_id 分片,但查询维度是 merchant_id,导致全表扫描,如何优化
解读
- 场景定位:典型的“分片键 ≠ 业务查询键”错位,国内互联网常见,尤其在订单、支付、物流等 C 端业务。
- 性能表象:
- 单次查询落到所有分片,线程池、连接池瞬间打满;
- 随着分片数线性增加,RT 成倍放大,QPS 天花板提前触顶;
- 监控出现“某分片 CPU 100 %,其余空闲”的明显热点。
- 面试意图:考察候选人能否把“存储层分片规则”与“业务层访问模式”打通,给出可落地的数据层+应用层+测试验证闭环,而非单纯背“加索引”。
知识点
- 水平分片(Sharding)原理:
- 分片键决定数据到物理节点的路由函数,常见哈希/范围/一致性哈希。
- 非分片键查询需广播(scatter),结果归并(gather),代价 O(n)。
- 二级索引在分布式场景的局限:
- 本地索引只能裁剪本分片,无法避免跨片。
- 全局二级索引需额外写放大与分布式事务,国内 MySQL 生态无原生支持。
- 冗余表/异构索引:
- 按 merchant_id 再建一张“订单号 → user_id”映射表,或“订单宽表”冗余到 ES/HBase,属于“空间换时间”。
- 异步队列 + 最终一致:
- 利用 Canal、DTS 监听 binlog,将变更同步到下游索引表,延迟一般 <1 s,满足国内“准实时”即可接受。
- 冷热分级:
- 近 30 天订单放在热库按 user_id 分片,历史订单归档到按 merchant_id 分片的冷库,降低广播范围。
- 压测验证方法:
- 用 Gatling/JMeter 构造 merchant_id 维度压测模型,观察 95th RT、P99 网络 IO、连接池等待队列;
- 通过 Arthas 抓栈确认是否大量线程阻塞在“跨片结果归并”;
- 灰度发布期间对比全表扫描与冗余索引方案的 CPU 利用率曲线,确保优化后 SLA 提升 5~10 倍。
答案
- 短期止血:
a. 在业务层增加“user_id 推导”逻辑——利用用户登录态把 merchant_id 查询转化为“merchant_id + user_id”组合,使路由可计算;
b. 若无法推导,则强制限制查询时间区间 ≤7 天,减少分片扫描范围;
c. 对核心 merchant 开通“白名单”异步预热,将热点数据缓存到 Redis 集群,读性能提升 10 倍。
- 中期架构:
a. 建立异构索引表:
- 表结构:(merchant_id, order_id, user_id, order_status, gmt_create)
- 分片键:merchant_id
- 同步方案:Canal → Kafka → 消费者组批量写入,保证幂等;
b. 查询流程:先按 merchant_id 路由到索引表拿到 order_id 列表,再回订单主表做局部 IN 查询,整体 RT 从 1.2 s 降至 80 ms。
- 长期演进:
a. 引入 LSM-Tree 引擎(TiDB/Lindorm)原生全局二级索引,把分片键与查询键解耦;
b. 订单域做“CQRS”读写分离:写侧保持 user_id 分片,读侧按 merchant/商品/地域多维度构建物化视图,满足运营后台、商家 BI 等多维需求;
c. 性能基线固化:把 merchant 维度压测脚本纳入 CI,每次索引变更必须回放 1 k并发、5 亿数据量级,RT 与错误率回归通过方可上线。
拓展思考
- 如果业务继续引入“商品 ID”维度,如何保持三维(user+merchant+sku)查询都可高性能?
→ 可构建“星型”冗余宽表到 ClickHouse,利用列存+分区+跳数索引,实现亚秒级 OLAP;同时通过 Flink 双流 Join 保证分钟级延迟。
- 分片键选择有没有“银弹”?
→ 没有。国内大厂实践是“1+N”原则:1 条主分片键保证 80 % 核心链路,N 条异构索引覆盖 20 % 长尾,配合成本预算做“可接受”的写放大。
- 性能测试同学如何量化优化收益?
→ 除了 RT、QPS,还要给出“单分片 CPU 利用率标准差”——优化前 60 %,优化后降到 15 %,证明热点消除;同时输出“每 1 % CPU 下降可节省多少台 16C32G 物理机”,让管理层一眼看懂 ROI。