分片键一选错,查询就开始变形
分库分表最难的不是把数据切开,而是切开之后那些“不按分片键查”的需求。以订单表按 user_id 分片为例:C 端“我的订单”走单分片,非常好;运营后台按订单号查、按商家查、按时间段导出,全都没有 user_id,只能把 SQL 广播到所有分片再归并。
广播查询的成本随分片数线性增长:8 个分片就是 8 次数据库往返、8 份结果在内存里排序、8 个连接被占用。分片从 8 扩到 64 之后,一个导出接口可能直接把连接池打满,进而拖垮在线查询——这就是分库分表最常见的“越拆越慢”。
全局查询的四条出路
基因法。 把分片信息编码进业务主键,让另一个维度的查询也能路由。经典做法是生成订单号时把 user_id 的低位拼进去,查订单号时先解出分片位,再定位到具体分片。代价是主键不再连续、长度变长,而且基因只能带一个维度。
冗余索引表。 建一张 order_id -> user_id 的映射表,按订单号分片,查订单时先查映射再查明细。写路径要维护两张表,一致性靠同一个本地事务保证。它的空间成本可控,实现也简单,是后台查询最常用的兜底。
异构查询引擎。 把需要多维检索的字段同步到 Elasticsearch 或 ClickHouse,数据库只承担按主键和分片键的精确查询。这是最彻底的做法,代价是引入同步链路和最终一致,报表口径要重新对齐。
广播归并。 数据量小、并发低的场景完全可以接受,比如一天跑一次的核对任务。关键是给它独立的连接池和超时,别和在线流量抢资源。
四条路不是互斥的:主键查询用基因法,后台检索用 ES,对账用广播,组合起来才够用。
数据迁移:双写、校验、切流
存量迁移的稳妥顺序是:影子表建好 → 双写(新库失败不阻塞主流程,只记录补偿)→ 存量按分片批量搬 → 全量校验 → 读切流按比例放大 → 写切流 → 观察一个业务周期后停双写。
双写阶段最忌讳“新库写失败就回滚主流程”。分片库出问题的概率比单库高,一旦强绑定,可用性会下降一个数量级。正确做法是新库失败只打日志并投递补偿任务,靠对账追平。
校验不能只比总行数。常见做法是三层:总行数与金额汇总、按分片维度的分桶比对、以及随机抽样做字段级 diff。差集要能定位到主键,否则发现了差异也无从修复。
-- 第一层:总量比对(分片库与原库分开统计,差额即缺失量)
SELECT COUNT(*) AS cnt, COALESCE(SUM(amount), 0) AS total_amount
FROM t_order
WHERE create_time >= '2026-04-01' AND create_time < '2026-05-01';
-- 第二层:按用户分桶比对,定位到具体区间
SELECT user_id % 64 AS bucket, COUNT(*) AS cnt
FROM t_order
WHERE create_time >= '2026-04-01' AND create_time < '2026-05-01'
GROUP BY user_id % 64
ORDER BY bucket;
两边的 bucket 分布应该完全一致,不一致的那个桶就是需要重点排查的范围。
一份分片配置
spring:
shardingsphere:
datasource:
names: ds0,ds1
ds0:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://10.0.3.11:3306/shop?useSSL=false
username: shop_rw
password: ${DB_PASSWORD}
ds1:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://10.0.3.12:3306/shop?useSSL=false
username: shop_rw
password: ${DB_PASSWORD}
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds${0..1}.t_order_${0..3}
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: db-inline
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: tb-inline
shardingAlgorithms:
db-inline:
type: INLINE
props:
algorithm-expression: ds${user_id % 2}
tb-inline:
type: INLINE
props:
algorithm-expression: t_order_${user_id % 4}
不同 5.x 小版本的 YAML 结构有差异(尤其 rules 与 dataSources 的位置),升级前一定按所用版本文档核对,照抄旧版本配置会出现“数据源加载成功但分片规则没生效”的静默错误。上线前用 ShardingSphere 的 SQL 日志确认实际路由到了哪张表,这是最直接的验证。
常见坑
分片键用自增 ID。 新数据全落在最后一个分片,热点写满一个库,其他库空转。
分片数一次定太少。 按当前数据量算 4 个分片,半年后又要重分片,而重分片是迁移里最贵的一步。通常按三年数据量规划,并预留 2 的幂次扩容路径。
跨分片 join 和聚合。 order by create_time limit 10 会被改写成“每个分片取 10 条再归并”,分页越深越慢,深层分页几乎没有优雅解。
分布式事务直接套 Seata。 分片后的事务成本本来就高,能用“同分片内本地事务 + 跨分片最终一致”就不要上强一致方案。
忽略自增主键冲突。 分片后每个表都有自己的自增序列,id 会重复,必须换成全局 ID 或复合主键。
评论