大表分库分表后的跨分片查询设计:从全局索引到查询路由的全部方案
大表分库分表后的跨分片查询设计从全局索引到查询路由的全部方案一、用户想查他所有的订单——分库分表后这个简单需求变成了一道分布式查询难题当订单表按user_id分 1024 个分片后查询user_id 123 的所有订单只需路由到单个分片。但产品需求是查询所有用户的订单中金额最大的 Top 100、或者是根据订单号非分片键查找订单。这些跨分片查询打破了分库分表的单分片路由假设成为分库分表架构中最棘手的挑战。实际上分库分表后的查询模式可以分为三类每类对应不同的解决方案查询模式示例解决方案分片键命中WHERE user_id 123直接路由到单个分片非分片键点查WHERE order_no ORD001全局索引 / 基因法全表聚合查询GROUP BY status ORDER BY count DESC分片聚合 合并关联查询JOIN user ON order.user_id全局表 / 冗余字段二、跨分片查询的五种方案架构flowchart TB Q[跨分片查询] -- R{查询路由选择} R --|方案1| S1[全局索引表br/order_no → shard_id 映射] R --|方案2| S2[基因法br/分片键内置在ID中] R --|方案3| S3[广播查询br/并发查询所有分片合并] R --|方案4| S4[冗余存储br/ES/HBase 全局二级索引] R --|方案5| S5[垂直拆分br/常用字段单独查询] S1 -.-|适合| C1[非分片键点查] S2 -.-|适合| C2[订单号→用户ID→分片] S3 -.-|适合| C3[聚合查询Top N] S4 -.-|适合| C4[复杂组合条件搜索] S5 -.-|适合| C5[多维度查询]三、五种方案的代码实现方案1: 全局索引表-- 全局索引表存储 (order_no, user_id) 的映射 CREATE TABLE global_order_index ( order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, PRIMARY KEY (order_no) ) ENGINEInnoDB; -- 插入订单时同步写入全局索引表 -- 查询流程先查索引获取 user_id → 再路由到分片def query_by_order_no(order_no): 通过订单号查询订单使用全局索引 # 1. 查询全局索引表获取分片键 cursor global_db.cursor() cursor.execute( SELECT user_id FROM global_order_index WHERE order_no %s, (order_no,) ) row cursor.fetchone() if not row: return None user_id row[0] shard_id user_id % 1024 # 计算分片 # 2. 路由到对应分片查询 shard_conn get_shard_connection(shard_id) cursor shard_conn.cursor() cursor.execute( SELECT * FROM orders WHERE user_id %s AND order_no %s, (user_id, order_no) ) return cursor.fetchone()方案2: 基因法ID 中包含分片信息class GeneShardingStrategy: 基因法在订单号中嵌入用户ID基因 SHARD_COUNT 1024 staticmethod def generate_order_no(user_id: int, sequence: int) - str: 生成包含分片基因的订单号 # 前4位 user_id % 1024分片基因 gene user_id % GeneShardingStrategy.SHARD_COUNT # 中间 时间戳 import time ts int(time.time() * 1000) # 后8位 序列号 seq_part f{sequence:08d} return f{gene:04d}{ts}{seq_part} staticmethod def extract_shard_id(order_no: str) - int: 从订单号中提取分片 ID gene int(order_no[:4]) return gene staticmethod def extract_user_id_gene(order_no: str) - int: 提取用户 ID 基因用于辅助定位 return int(order_no[:4]) def query_by_order_no_gene(order_no): 使用基因法查询订单 shard_id GeneShardingStrategy.extract_shard_id(order_no) shard_conn get_shard_connection(shard_id) cursor shard_conn.cursor() cursor.execute( SELECT * FROM orders WHERE order_no %s, (order_no,) ) return cursor.fetchone()方案3: 广播查询分片聚合from concurrent.futures import ThreadPoolExecutor, as_completed def top_n_cross_shard(n: int 100): 跨分片查询金额最大的 Top N 订单 def query_shard(shard_id): conn get_shard_connection(shard_id) cursor conn.cursor() cursor.execute( SELECT * FROM orders ORDER BY amount DESC LIMIT %s, (n * 2,) # 每个分片取 2N 条为合并留余量 ) return cursor.fetchall() # 并发查询所有分片 all_results [] with ThreadPoolExecutor(max_workersmin(1024, 64)) as executor: futures { executor.submit(query_shard, i): i for i in range(1024) } for future in as_completed(futures): shard_id futures[future] try: results future.result(timeout30) all_results.extend(results) except Exception as e: print(f分片 {shard_id} 查询失败: {e}) # 全局排序取 Top N all_results.sort(keylambda x: x[amount], reverseTrue) return all_results[:n]四、方案选择决策矩阵场景推荐方案代价订单号 → 订单信息的精确查找基因法订单号不连续、不美观低频率的非分片键查询 1 QPS全局索引表维护一个额外的全局表高并发的多条件搜索ES 冗余索引额外 ES 集群成本 数据一致性挑战报表类聚合分析广播查询连接数和资源消耗随分片数线性增长周期性全表扫描离线 ETL → ClickHouse数据准实时分钟级延迟五、总结分库分表后的跨分片查询没有一招制胜的方案而是需要根据查询模式和性能需求选择不同的策略组合设计阶段就考虑查询模式如果 90% 的查询通过分片键就能完成20% 的边际场景不值得过度设计基因法是性价比最高的点查方案零额外存储成本零延迟代价只是ID 不好看广播查询要限流并发查询 1024 个分片的操作不能频繁发生需要有 Cache 层保护ES/Redis 冗余索引是最后的武器用于解决复杂搜索场景但要接受数据一致性从强一致降级为最终一致在实际生产系统中基因法承担了 70% 的非分片键查询主要是订单详情页全局索引表覆盖了 15%运营后台的点查ES 冗余索引负责剩下 15% 的复杂搜索场景。三种方案互补而非选择其一。