订单表按 user_id 分片,但查询维度是 merchant_id,导致全表扫描,如何优化

解读

  1. 场景定位:典型的“分片键 ≠ 业务查询键”错位,国内互联网常见,尤其在订单、支付、物流等 C 端业务。
  2. 性能表象:
    • 单次查询落到所有分片,线程池、连接池瞬间打满;
    • 随着分片数线性增加,RT 成倍放大,QPS 天花板提前触顶;
    • 监控出现“某分片 CPU 100 %,其余空闲”的明显热点。
  3. 面试意图:考察候选人能否把“存储层分片规则”与“业务层访问模式”打通,给出可落地的数据层+应用层+测试验证闭环,而非单纯背“加索引”。

知识点

  1. 水平分片(Sharding)原理:
    • 分片键决定数据到物理节点的路由函数,常见哈希/范围/一致性哈希。
    • 非分片键查询需广播(scatter),结果归并(gather),代价 O(n)。
  2. 二级索引在分布式场景的局限:
    • 本地索引只能裁剪本分片,无法避免跨片。
    • 全局二级索引需额外写放大与分布式事务,国内 MySQL 生态无原生支持。
  3. 冗余表/异构索引:
    • 按 merchant_id 再建一张“订单号 → user_id”映射表,或“订单宽表”冗余到 ES/HBase,属于“空间换时间”。
  4. 异步队列 + 最终一致:
    • 利用 Canal、DTS 监听 binlog,将变更同步到下游索引表,延迟一般 <1 s,满足国内“准实时”即可接受。
  5. 冷热分级:
    • 近 30 天订单放在热库按 user_id 分片,历史订单归档到按 merchant_id 分片的冷库,降低广播范围。
  6. 压测验证方法:
    • 用 Gatling/JMeter 构造 merchant_id 维度压测模型,观察 95th RT、P99 网络 IO、连接池等待队列;
    • 通过 Arthas 抓栈确认是否大量线程阻塞在“跨片结果归并”;
    • 灰度发布期间对比全表扫描与冗余索引方案的 CPU 利用率曲线,确保优化后 SLA 提升 5~10 倍。

答案

  1. 短期止血:
    a. 在业务层增加“user_id 推导”逻辑——利用用户登录态把 merchant_id 查询转化为“merchant_id + user_id”组合,使路由可计算;
    b. 若无法推导,则强制限制查询时间区间 ≤7 天,减少分片扫描范围;
    c. 对核心 merchant 开通“白名单”异步预热,将热点数据缓存到 Redis 集群,读性能提升 10 倍。
  2. 中期架构:
    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。
  3. 长期演进:
    a. 引入 LSM-Tree 引擎(TiDB/Lindorm)原生全局二级索引,把分片键与查询键解耦;
    b. 订单域做“CQRS”读写分离:写侧保持 user_id 分片,读侧按 merchant/商品/地域多维度构建物化视图,满足运营后台、商家 BI 等多维需求;
    c. 性能基线固化:把 merchant 维度压测脚本纳入 CI,每次索引变更必须回放 1 k并发、5 亿数据量级,RT 与错误率回归通过方可上线。

拓展思考

  1. 如果业务继续引入“商品 ID”维度,如何保持三维(user+merchant+sku)查询都可高性能?
    → 可构建“星型”冗余宽表到 ClickHouse,利用列存+分区+跳数索引,实现亚秒级 OLAP;同时通过 Flink 双流 Join 保证分钟级延迟。
  2. 分片键选择有没有“银弹”?
    → 没有。国内大厂实践是“1+N”原则:1 条主分片键保证 80 % 核心链路,N 条异构索引覆盖 20 % 长尾,配合成本预算做“可接受”的写放大。
  3. 性能测试同学如何量化优化收益?
    → 除了 RT、QPS,还要给出“单分片 CPU 利用率标准差”——优化前 60 %,优化后降到 15 %,证明热点消除;同时输出“每 1 % CPU 下降可节省多少台 16C32G 物理机”,让管理层一眼看懂 ROI。