分片之后,SQL 的代价变了
单表一亿行时,加索引、归档、读写分离往往还能撑;再往上,分库分表就成了必选项。但分片不是把数据切开就完事——同一条 SQL,在单库里是一次索引查找,在分片环境里可能变成“广播到 16 个库、每个库查一遍、再在内存里归并”。很多性能问题不是分片中间件慢,而是 SQL 的访问模式变了,而开发者还在按单库的直觉写代码。
查询路由的三种情况
| 查询条件 | 路由结果 | 代价 |
|---|---|---|
带完整分片键,如 order_id = ? |
定位到单个分片 | 最低 |
| 带部分分片键或范围条件 | 路由到部分分片 | 中等,取决于分片数 |
不带分片键,如 status = ? |
广播到所有分片 | 最高,连接数和内存都翻倍 |
分片键的选择基本决定了系统的查询能力。订单表按 order_id 分片,用户端却按 user_id 查订单,那就只能全路由;反过来按 user_id 分片,运营后台按订单号查,同样尴尬。常见解法是选一个主分片键,再为另一个维度建映射表或冗余表,把“按另一个键查”变成一次映射加一次精确路由。
IN 查询也要小心。WHERE order_id IN (100 个值) 如果这些值散落在所有分片,中间件会为每个分片建连接、分别执行,连接池瞬间被占满。能按分片键归并的,先在应用层分组,再分批查询。
深分页与跨库 JOIN
LIMIT 100000, 20 在单库是扫描后丢弃,在分片环境是每个分片都扫描 100020 行,再归并排序取前 20 行。分片数越多、页码越深,内存和 CPU 越夸张。三种替代方案:
- 游标分页:按有序主键翻页,
WHERE id > lastId ORDER BY id LIMIT 20,每页只查一次索引范围; - 二次查询:先在各分片查出符合条件的 ID 和时间,归并后再按 ID 回查明细;
- 换存储:复杂条件、全文检索、多维度排序交给 Elasticsearch,数据库只负责按 ID 取详情。
跨库 JOIN 更直接:ShardingSphere 不支持跨库物理 JOIN,只能在内存里归并,数据量一大就 OOM。工程上的做法是绑定表(同分片规则的表可以路由到同一分片做 JOIN)、广播表(小字典表每个库一份)、冗余字段(把常用的名称、状态冗余进主表),以及在应用层做批量 IN 查询再组装。
try (HintManager hintManager = HintManager.getInstance()) {
hintManager.addTableShardingValue("t_order", 1024L);
return orderMapper.selectByUserId(userId);
}
HintManager 用于“查询条件里没有分片键,但我知道该去哪个分片”的场景,比如运营后台按订单号查。它强制指定路由,用完必须关闭,否则线程复用时会影响后续 SQL。它是一把手术刀,不是日常查询的常规手段。
事务:先分清跨了几层
事务的处理取决于一次操作跨了几个物理库:
- 单分片:就是普通本地事务,
@Transactional足够; - 同库多表:仍然是本地事务,ShardingSphere 会路由到同一个数据源;
- 跨库:本地事务管不了,需要 Seata AT/XA 或者最终一致性方案。
很多团队踩的坑是:以为加了 @Transactional 就万事大吉,实际上 ShardingSphere 会为每个分片拿独立连接,各自提交,中间失败就出现“A 库扣了、B 库没加”。要么引入 Seata,要么把跨库操作改成“本地事务 + 消息补偿”。
用 Seata 时,undo_log 要在每个物理库都建,不是只在逻辑库建一张。分片数越多,这个遗漏越难发现,往往要等到第一次回滚失败才暴露。
常见坑
坑一:分片键选错。 选了一个“写入方便但查询不用”的字段,上线后 80% 的查询都是全路由。分片键要按核心查询场景选,而不是按表结构选。
坑二:跨分片唯一索引。 数据库的唯一索引只在单表内生效,分片后 order_no 可能在不同分片重复。要么用分布式 ID 保证全局唯一,要么单独维护一张唯一键表。
坑三:自增主键冲突。 每个分片都从 1 开始自增,合并后必然重复。分片表的主键要用雪花 ID 或号段 ID。
坑四:COUNT(*) 和聚合查询。 全路由的 COUNT 要把所有分片扫一遍,数据量大时非常慢。能走缓存就走缓存,能预聚合就预聚合。
坑五:分片键更新。 把订单从用户 A 转到用户 B,分片键变了,数据要跨库迁移。这类操作要么禁止,要么走专门的数据迁移流程。
坑六:连接数放大。 4 个库 × 每库 20 个连接 = 80 个连接,再乘上多实例,很容易打满数据库的 max_connections。连接池要按分片总数重新估算。
排查步骤
- 打开 ShardingSphere 的
sql-show,看一条 SQL 实际被改写成几条、路由到哪些库表。 - 对慢查询看执行计划,确认是索引问题还是全路由问题。
- 看连接池活跃数和等待数,判断是不是分片放大了连接需求。
- 跨库事务失败时,查各物理库的
undo_log和 Seata 日志,确认分支是否注册成功。 - 深分页慢时,先用游标分页或 ES 替换,再考虑加缓存,不要盲目加索引。
评论