什么时候才轮到分库分表
单表数据量涨到几千万,查询开始变慢,很多人的第一反应是“上分库分表”。但在动手之前,先确认几件事:索引是否合理、有没有大字段拖累、历史数据能不能归档、读写分离能不能扛住。分库分表是最后手段,不是性能优化的起点。
一个粗略的判断是:单表行数超过千万、并且以每年千万级增长;单库连接数、磁盘 IO 或写入 TPS 已经接近上限;业务查询天然带分片键。三个条件同时成立,才值得引入 ShardingSphere。否则,加索引、归档、缓存、读写分离的收益更高,复杂度却低得多。
垂直拆还是水平拆
垂直拆分两种:按业务把不同表拆到不同库,比如订单库、用户库;或者把宽表里的大字段、低频字段拆到扩展表。它解决的是“表太宽、库太杂”,不解决单表行数问题。
水平拆是把同一张表按规则拆成多个结构相同的分片,比如 t_order_0 到 t_order_3。核心是分片键的选择:基数要高、分布要均匀、大多数查询要能带上它。订单表常用 user_id 或 order_id,前者方便查“我的订单”,后者方便按订单号查详情。如果两种查询都很多,可以用基因法,把用户 ID 的尾部编进订单 ID,让两个维度都能路由到同一个分片。
分片键一旦选定,后续扩容会很痛苦。提前规划分片数量,比如先分 16 个库、每个库 16 张表,而不是一开始只分 2 个库,等数据涨了再迁移。
ShardingSphere 5.5 配置
Spring Boot 3.2 项目引入 org.apache.shardingsphere:shardingsphere-jdbc:5.5.0。从 5.3 开始,官方已经移除 spring.shardingsphere.* 的 Starter 配置方式,网上很多旧教程还在用,照抄会直接启动失败。现在推荐让 ShardingSphere 接管 DataSource,通过 JDBC URL 加载 YAML:
spring:
datasource:
driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
url: jdbc:shardingsphere:classpath:sharding.yaml
sharding.yaml 放在 src/main/resources 下,数据源和分片规则写在同一个文件里:
dataSources:
ds_0:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://127.0.0.1:3306/order_db_0?useSSL=false
username: root
password: root
ds_1:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://127.0.0.1:3306/order_db_1?useSSL=false
username: root
password: root
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..1}.t_order_${0..1}
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: order-db-inline
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: order-table-inline
keyGenerateStrategies:
t_order_order_id:
keyGenerateType: column
keyGeneratorName: snowflake
logicTable: t_order
keyGenerateColumn: order_id
shardingAlgorithms:
order-db-inline:
type: INLINE
props:
algorithm-expression: ds_${user_id % 2}
order-table-inline:
type: INLINE
props:
algorithm-expression: t_order_${user_id % 2}
keyGenerators:
snowflake:
type: SNOWFLAKE
props:
sql-show: true
sql-show 只建议在开发和预发打开,它会打印逻辑 SQL 和路由后的真实 SQL,排查分片规则时非常有用。生产环境打开会刷大量日志,还可能带出参数敏感信息。
分片之后的新问题
跨分片查询是最先遇到的。SELECT * FROM t_order WHERE status = 'PAID' 没有分片键,ShardingSphere 只能广播到所有分片,再在内存里归并排序;分片越多,放大越明显。COUNT(*)、GROUP BY、ORDER BY 加 LIMIT 都有类似问题,深分页尤其严重。
分布式事务是第二个坑。跨库写要么用 XA,强一致但性能差;要么用本地消息表、Seata AT 这类最终一致方案。最好的办法还是设计上避免跨分片事务:把同一业务聚合根的数据放在同一个分片,支付和订单的联动通过消息而不是跨库事务完成。
全局唯一 ID 也要重新设计。AUTO_INCREMENT 在多个分片里会重复,ShardingSphere 内置雪花算法可以生成 order_id,但要注意时钟回拨和机器号分配。业务上如果有“按订单号查”的需求,还要保证订单号能反解出分片键。
常见坑
1. 分片键和查询条件不匹配
按 order_id 分片,但 90% 的查询是按 user_id 查列表,结果每次都是全路由。分片键要按主要查询模式选,不能只看主键。
2. 直接上分库分表,没做归档
历史数据占了大半,分片后每个分片仍然很大。先归档冷数据,再分片,能少走很多弯路。
3. 忽略 SQL 兼容性
ShardingSphere 对子查询、UNION、部分函数和跨分片 JOIN 的支持有限。上线前把核心 SQL 跑一遍,看日志里的真实执行计划,不要假设“单库能跑,分片也能跑”。
4. 分布式事务滥用
每个写操作都包 XA,连接持有时间变长,吞吐骤降。能本地事务解决的就本地解决,跨分片的部分用消息补偿。
5. 扩容没有方案
从 2 个库扩到 4 个库,取模规则变化,历史数据要重新分布。提前用一致性哈希或成倍扩容,并准备好双写、校验、切流的数据迁移脚本。
排查步骤
分片路由不对时,先在 sharding.yaml 的 props 里把 sql-show 设为 true,确认逻辑 SQL 有没有带分片键。
日志里会看到 Logic SQL 和 Actual SQL,如果 Actual SQL 是多条、命中了所有分片,说明发生了广播。再看分片算法表达式,用具体参数手算一遍路由结果。需要强制走某个分片时,可以用 HintManager:
try (HintManager hintManager = HintManager.getInstance()) {
hintManager.addDatabaseShardingValue("t_order", 1L);
hintManager.addTableShardingValue("t_order", 1L);
return orderMapper.selectById(orderId);
}
最后到真实分片库上执行 EXPLAIN,确认索引没有被分片改写破坏。
小结
分库分表解决的是“单库单表容量和写入上限”,代价是查询、事务、ID、运维复杂度全面上升。先用尽索引、归档、缓存、读写分离这些低成本手段,再考虑 ShardingSphere。分片键设计得好,后面的坑少一半。
评论