高并发系统设计:分库分表后的非分片键查询
核心考察点:数据库架构设计、分片路由策略 (Sharding Routing)、异构索引 (Heterogeneous Index)、基因法 (Gene Method) 以及 OLTP 与 OLAP 的分离。
一、 面试现场:为什么分完表就“瞎”了?
面试官问:“你们公司的订单表有 2 亿数据,怎么做的分库分表?”
候选人背得滚瓜烂熟:“简单!我们按用户 ID(user_id)取模,分了 16 个库,每个库 64 张表,一共 1024 张表。这样用户查自己的订单特别快,直接定位到具体的表。”
面试官:订单表 10 亿数据,按用户 ID (user_id) 分了 1024 张表。现在商家想查自己店铺的订单,或者用户想按订单号 (order_id) 查,怎么查?
候选人:遍历这 1024 张表...?
面试官:……(挂了)

核心痛点: 分库分表最大的代价是查询维度的丧失。
- 分片键 (Sharding Key):
user_id。查它很快,路由定位 O(1)。 - 非分片键:
merchant_id或order_id。中间件(MyCat/ShardingSphere)不知道数据在哪张表,只能发起全路由扫描 (Broadcast)。 - 后果:一条 SQL 裂变成 1024 条 SQL 同时轰炸数据库。并发一高,数据库必死。
二、 核心解法:多维度查询的三板斧
方案 1:异构索引表 (Heterogeneous Index) —— 空间换时间
适用于查询维度较少且固定的场景(如:B 端商家要查订单)。

原理:既然按 C 端用户分表导致 B 端商家查不了,那就再造一套表,专门按 B 端商家分片。
架构设计:
C 端主表:
order_c,按user_id分片。服务于消费者,存全量数据。B 端异构表:
order_b,按merchant_id分片。服务于商家。数据同步:
双写(不推荐):业务代码写两次,事务复杂,容易不一致。Canal + MQ(推荐):监听 C 端主表的 Binlog,通过 MQ 异步写入 B 端表。虽然有秒级延迟,但实现了最终一致性且解耦。
方案 2:基因法 (Gene Method) —— ID 查询的黑科技
适用于主键查询场景(如:已知 order_id 要查订单详情,但分片键是 user_id)。 千万别为了查 order_id 再去搞一张异构表,太浪费!

原理:将分片键 (
user_id) 的路由规则(基因)嵌入 到全局唯一 ID (order_id) 中。实现步骤:
定义基因:假设分库数为 16(二进制需 4 位)。
提取基因:
user_id = 99(二进制...01100011),取最后 4 位0011(即 3)。生成 ID:在生成雪花算法
order_id时,强制将最后 4 位替换为0011。查询路由:
查
user_id=99->99 % 16 = 3-> 定位到 03 库。查
order_id=...0011-> 提取后缀0011(即 3) -> 同样定位到 03 库。结论:同一个用户的订单,和这个订单 ID 本身,天然在同一个库中。无需查询索引表,直接通过 ID 算出库名。
方案 3:ES 大宽表 (ElasticSearch) —— 复杂搜索的终极杀招
适用于任意维度的复杂查询(如:运营后台查“昨天、北京地区、金额>1000”的订单)。
- 痛点:MySQL 即使不分库分表,也搞不定这种多字段组合检索。

架构:OLTP (交易) 与 OLAP (分析) 分离。
MySQL:只负责核心交易(下单、支付),按
user_id分片,保证极高的写入性能。ES:通过 Binlog 同步数据到 ElasticSearch,建立一张包含所有字段的大宽表。
查询:所有运营后台的查询、报表、复杂筛选,全部走 ES 索引,不碰 MySQL。
三、 进阶问答:数据倾斜 (Data Skew)
面试官:如果有一个大 V 用户(或大商家),订单量是普通人的 1000 倍,导致他所在的表特别大,甚至把磁盘打满了,怎么办?
解法:隔离策略
监控识别:通过监控找出 CPU 或 磁盘 IO 异常的热点分片。
例外配置:在路由规则中维护一个 VIP 名单。
物理隔离:
对于名单内的大 V,不走默认的 Hash 取模逻辑。
直接将其路由到单独的数据库实例或单独的表中。
甚至可以给大商家搭建独立的数据库集群,确保他不影响 99% 的普通用户。
四、 面试满分模板
面试官:分库分表后,非分片键怎么查询?
参考回答: “分库分表的核心矛盾是写性能与多维度读的冲突。在设计上,我遵循‘核心链路保切割,辅助链路走异构’的原则,具体分为三种场景:
C 端用户查(高频):这是核心交易链路,直接按
UserId取模路由,性能 O(1)。按订单号查(高频):我采用‘基因法’。在生成
OrderId时,将UserId的分片基因(如最后 4 位)嵌入到OrderId末尾。这样查订单号时,直接截取后缀即可定位分片,避免了全表扫描,也节省了额外的索引表成本。B 端商家或后台查(复杂):
对于商家端,我利用 Canal 同步 Binlog,构建按
MerchantId分片的异构索引表。对于运营后台的复杂组合查询,我将数据同步到 ElasticSearch,利用 ES 的倒排索引能力,彻底将分析型流量从 MySQL 核心交易库中剥离。
针对可能出现的数据倾斜(大 V 用户),我会采用隔离策略,将头部用户路由到独立的 VIP 库表中,防止单点热点拖垮整个集群。”