单表数据量超过千万行、单库写入QPS逼近硬件上限,就需要分库分表。ShardingSphere-JDBC是客户端形态的分库分表中间件,以jar包嵌入应用,无独立代理层,性能损耗低,适合大多数Java技术栈。分片键选择和扩容方案是落地时的两大难点,选错分片键,后续所有优化都是在还债。
分片键选择:查询模式决定数据分布
分片键的选择原则:绝大多数高频查询条件里都包含这个字段,且写入分布均匀。以订单库为例,C端查询基本都带user_id,运营后台查询带order_time和商家ID,两边的查询模式冲突。常见的取舍是以user_id做分片键,保证单用户订单落在同一库,单表查询不带分片键的走广播或异构索引表。
-- 数据分布规划
-- db0: t_order_0, t_order_1
-- db1: t_order_2, t_order_3
-- 分片键 user_id % 4 确定表,再路由到库
-- user_id=1001 的订单永远落在 t_order_1
SELECT * FROM t_order WHERE user_id = 1001; -- 单表路由
反例是用自增order_id做分片键:写入确实均匀,但C端查询都带user_id不带order_id,每次查询都广播到所有分片再聚合,分库分表把单表查询压力放大成了全集群扫描。
ShardingSphere-JDBC配置:分片规则落地
Spring Boot项目引入shardingsphere-jdbc依赖,配置分片规则。实际分片算法推荐用INLINE表达式,简单场景够用且性能最好。
# application.yml 核心配置
rules:
- !SHARDING
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..3}
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: db-inline
table-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: order-table-inline
sharding-algorithms:
db-inline:
type: INLINE
props:
algorithm-expression: ds$->{user_id % 2}
order-table-inline:
type: INLINE
props:
algorithm-expression: t_order_$->{user_id % 4}
actual-data-nodes定义真实节点,4张表分布在2个库。查询SQL里带user_id时精确路由到单库单表,不带分片键时全路由,控制台SQL日志里能看到路由结果,上线前重点检查全路由SQL的占比。
全局ID生成:避免分片后主键冲突
分表后数据库自增主键不再全局唯一,需要应用层ID方案。常用三种:雪花算法、号段模式、UUID。雪花算法生成有序64位ID,含时间戳和机器位,需要解决时钟回拨问题,适合大多数场景。
// 雪花算法时间戳位设置,容忍时钟回拨2秒
public class SnowflakeIdGen {
private final long epoch = 1735689600000L; // 2025-01-01
private final long workerId;
public synchronized long nextId() {
long now = System.currentTimeMillis();
if (now < lastTimestamp) {
// 回拨在2ms内等待,超过则抛异常告警
if (lastTimestamp - now < 2) {
try { wait(lastTimestamp - now); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
} else {
throw new IllegalStateException("时钟回拨过大");
}
now = System.currentTimeMillis();
}
// 时间戳左移22位拼机器位与序列号,此处省略
return compose(now, workerId, sequence);
}
}
机器位分配用配置中心统一管理,或者用Redis自增序列临时领取,避免多实例拿到相同workerId生成重复ID。
平滑扩容方案:双写迁移与数据回填
2库4表扩到4库8表,直接重写分片路由会导致老数据位置错乱。平滑扩容走双写迁移:老库继续服务,新库结构建好;写请求双写新老两个集群;存量数据按新规则回填;读逐步切到新集群;观察无误后停老库写。
扩容期间的读写策略:
1. 双写开启,写老库为主,写新库为异步(失败只记日志)
2. 存量数据回填:按分片键扫描老库,按新规则写入新库
3. 回填完成后做数据校验:
SELECT COUNT(*), SUM(CRC32(CONCAT(user_id, order_id)))
FROM t_order WHERE create_time < '切换时间点';
-- 新老两侧结果必须一致
4. 读流量灰度切换到新库:5% -> 50% -> 100%
5. 写老库改为仅日志,观察一周后下线老库
回填务必按分片键并行,单线程回填千万级数据要跑数小时。校验环节用聚合签名对比而不是逐行比对,两张千万级表逐行diff性能不可接受。
跨分片查询治理:异构索引与ES分流
运营后台的复杂条件查询、多维统计、模糊搜索,这类需求不该打在分片集群上。成熟做法是Binlog同步到Elasticsearch,后台查询走ES,分片集群只服务核心交易链路。ES里文档结构面向查询设计,分片集群面向交易设计,两边各司其职。同步链路用Canal或Flink CDC,延迟通常在秒级,后台查询对实时性的要求基本都能满足。把这个边界划清楚,分库分表方案才不会陷入为长尾查询不断加广播的泥潭。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql-fen-ku-fen-biao-shi-zhan-shardingsphere-fen-pian-ce/