☰
ShardingJdbc 数据分片实战:从分片规则设计到跨库查询与扩容迁移
2026/10/19 9:03:00 网站建设 项目流程

1. 为什么单库单表撑不住业务增长

1.1 从一次线上告警说起

凌晨两点,监控大屏上一条订单查询接口的响应时间曲线突然拉成了一条直线,P99 从 80ms 飙到 4.2s,随后数据库连接池被打满,上游服务开始大面积超时。排查下来原因并不复杂:订单表数据量突破了 8000 万行,单表 B+ 树索引层级从 3 层涨到 4 层,每次回表多一次磁盘 IO,叠加晚高峰的并发写入,磁盘 IOPS 直接顶到上限。

这不是个例。任何一个业务只要活得够久,数据量增长就是必然。单库单表在数据量达到千万级以后,会依次遇到几个瓶颈:单表数据量过大导致索引效率下降、单库磁盘容量和连接数触顶、写入热点集中在同一台机器、DDL 操作锁表时间不可接受。垂直拆业务能缓解一部分,但同一张核心表(比如订单、流水、日志)的数据量增长是躲不掉的,这时候就必须做数据分片。

1.2 数据分片到底解决什么问题

数据分片(Sharding)的核心思路就一句话:把一张大表的数据,按照某个规则拆到多个数据库、多张表里去。拆完之后,每个库、每张表的数据量都控制在一个合理范围内,查询和写入的压力被分散到多台机器上。

分片有两个维度:

  • 分库:把数据分散到不同的数据库实例,解决单机连接数、磁盘容量、IO 瓶颈。
  • 分表:把数据分散到同一个库的多张表,解决单表数据量过大导致的索引效率问题。

实际生产中通常是分库分表一起做,比如拆成 4 个库、每个库 4 张表,总共 16 个分片。这样既分散了机器压力,又控制了单表规模。

1.3 ShardingSphere-ShardingJdbc 的定位

分库分表这件事,如果纯手工做,意味着你要在业务代码里写路由逻辑、维护分片规则、处理跨库 JOIN、搞定分布式主键、还要写一堆数据迁移脚本。这套东西写下来,业务代码基本就废了。

ShardingSphere 的 ShardingJdbc 组件就是来解决这个问题的。它的定位是一个轻量级的 Java 框架,以 jar 包形式引入,不需要额外部署中间件,本质上是一个增强版的 JDBC 驱动。你的业务代码还是写标准的 SQL,ShardingJdbc 在底层帮你做 SQL 解析、路由计算、SQL 改写、结果归并。对上层应用来说,它几乎是无感知的。

注意:ShardingJdbc 是客户端分片方案,分片逻辑在应用进程内完成,不依赖代理层。这和 ShardingSphere-Proxy 是两条不同的技术路线,选型时要根据团队运维能力来定。

这篇文章我会从分片规则设计、核心配置、SQL 路由原理、分布式主键、跨库查询处理、数据迁移、常见坑排查这几个维度,把 ShardingJdbc 数据分片这件事讲透。适合正在做分库分表选型、或者已经用了 ShardingJdbc 但踩了坑的同学参考。

2. 分片方案设计:规则定得好,后面少熬夜

2.1 分片键怎么选

分片键(Sharding Key)是整个分片方案的地基,选错了后面全是坑。选分片键的核心原则是:查询条件里高频出现、数据分布均匀、能覆盖绝大多数查询场景。

以订单表为例,常见的候选分片键有user_id、order_id、create_time。怎么选?

  • 如果业务查询以"查某个用户的订单列表"为主,选user_id,这样同一个用户的订单落在同一个分片,查询能精准路由。
  • 如果查询以"按订单号查详情"为主,选order_id,但要注意订单号如果是自增的,分布可能不均匀。
  • 如果查询以"按时间范围统计"为主,选create_time,但时间分片容易产生热点(比如按天分片,当天的数据全打在一个分片上)。

我的经验是:优先选业务查询的天然维度。订单场景选user_id是最稳的,因为 C 端查询 90% 以上都带用户维度。如果确实存在多维度查询需求,可以考虑基因法或者冗余表,这个后面细说。

2.2 分片算法怎么定

ShardingSphere 内置了多种分片算法,常用的几类:

算法类型适用场景特点
INLINE(行表达式)简单取模、按范围配置简单,基于 Groovy 表达式
MOD(取模)数据分布均匀场景分布均匀,但扩容麻烦
HASH(哈希取模)分片键非数字先哈希再取模
RANGE(范围)时间、ID 区间易产生热点
COMPLEX(复合)多分片键组合灵活但配置复杂
HINT(强制路由)非分片键查询通过 Hint 指定分片

取模分片是最常用的,配置简单、分布均匀。但它有个致命问题:扩容时数据迁移量大。比如从 4 库扩到 8 库,取模基数变了,几乎所有数据都要重新分布。所以如果预期未来会扩容,要么一开始就预留足够分片数(比如直接分 32 库,前期只用 4 个),要么用一致性哈希。

2.3 分片数量怎么算

分片数量不是拍脑袋定的,要结合几个因素算:

  • 当前数据量:假设订单表现在 5000 万行,日增 50 万行。
  • 单表容量红线:单表控制在 1000 万行以内比较稳妥(这个值取决于行宽和索引数量,行宽大的表要更小)。
  • 未来 2-3 年增长:按日增 50 万算,3 年增长约 5.5 亿行。

那么需要的分片数 = (5000万 + 5.5亿) / 1000万 ≈ 60 个分片。可以设计成 8 库 × 8 表 = 64 个分片,留一点余量。

实操心得:分片数建议取 2 的幂次,方便后续做一致性哈希或者翻倍扩容。另外分片数不宜过多,每个分片都是一个真实的物理表,分片太多会导致连接数膨胀、元数据管理复杂。

2.4 分库还是分表,还是都要

这个问题的判断依据是瓶颈在哪:

  • 磁盘容量或连接数瓶颈:必须分库,因为单机资源有限。
  • 单表数据量瓶颈:分表即可,但如果单机磁盘扛不住,还是要分库。
  • 写入热点:分库能分散到不同机器,效果更好。

大多数中大型业务是分库分表同时做。比如 4 库 × 4 表,共 16 个分片。这样单库数据量是总量的 1/4,单表数据量是总量的 1/16,两个维度的压力都缓解了。

3. 核心配置与实操落地

3.1 依赖引入与版本选择

ShardingJdbc 的版本迭代比较快,5.x 和 4.x 的配置方式差异很大。5.x 之后配置项做了大量精简和重构,推荐直接用 5.x 稳定版。

<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core</artifactId> <version>5.4.1</version> </dependency>

如果是 Spring Boot 项目,可以用 starter:

<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.4.1</version> </dependency>

注意:5.x 版本对 JDK 要求是 JDK 8 及以上,如果项目还在 JDK 8,建议用 5.3.x 或 5.4.x。JDK 17 项目可以用最新版。版本选择上,不要盲目追新,选一个社区活跃、bug 修复及时的稳定版即可。

3.2 数据源配置

假设我们做 2 库 × 2 表的分片,共 4 个分片。先配置两个真实数据源:

spring: shardingsphere: datasource: names: ds0,ds1 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.10:3306/order_db0?useSSL=false username: root password: xxx ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.11:3306/order_db1?useSSL=false username: root password: xxx

这里每个数据源都用了 HikariCP 连接池,这是 ShardingSphere 官方推荐的连接池。连接池大小要根据分片数和并发量算:假设单库 QPS 峰值 2000,单连接能扛 50 QPS,那单库需要 40 个连接,乘以分片数就是总连接数。

3.3 分片规则配置

接下来配置分片规则,这是核心部分:

rules: sharding: tables: t_order: actual-data-nodes: ds$->{0..1}.t_order_$->{0..1} database-strategy: standard: sharding-column: user_id sharding-algorithm-name: db-inline table-strategy: standard: sharding-column: user_id sharding-algorithm-name: table-inline sharding-algorithms: db-inline: type: INLINE props: algorithm-expression: ds$->{user_id % 2} table-inline: type: INLINE props: algorithm-expression: t_order_$->{user_id % 2}

这段配置的含义:

  • actual-data-nodes:定义真实的数据节点分布,ds$->{0..1}.t_order_$->{0..1}展开后是ds0.t_order_0、ds0.t_order_1、ds1.t_order_0、ds1.t_order_1四个节点。
  • database-strategy:库分片策略,按user_id % 2决定去哪个库。
  • table-strategy:表分片策略,按user_id % 2决定去哪个表。

这样user_id = 100的订单,100 % 2 = 0,会路由到ds0.t_order_0;user_id = 101的订单路由到ds1.t_order_1。

3.4 分布式主键配置

分库分表后,数据库自增主键就失效了,因为每个分片的自增序列是独立的,会产生重复 ID。ShardingSphere 内置了雪花算法(Snowflake)主键生成器:

key-generators: snowflake: type: SNOWFLAKE props: worker-id: 1

然后在表配置里指定:

t_order: key-generate-strategy: column: order_id key-generator-name: snowflake

雪花算法生成的 ID 是 64 位长整型,结构是:1 位符号位 + 41 位时间戳 + 10 位工作机器 ID + 12 位序列号。41 位时间戳可以用 69 年,10 位工作 ID 支持 1024 个节点,12 位序列号支持每毫秒 4096 个 ID。

实操心得:worker-id 在多实例部署时一定要区分开,否则会生成重复 ID。可以用 IP 末位或者注册中心分配。如果对 ID 有业务含义要求(比如要包含日期),可以自定义主键生成器实现ShardingKeyGenerator接口。

3.5 建表语句准备

真实表需要提前在对应的库里建好。注意每个分片的表结构必须完全一致:

CREATE TABLE t_order_0 ( order_id BIGINT NOT NULL, user_id BIGINT NOT NULL, order_no VARCHAR(64) NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (order_id), KEY idx_user_id (user_id), KEY idx_create_time (create_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

t_order_1、以及 ds1 库里的两张表,结构完全一样,只是表名不同。建表脚本建议用工具批量生成,避免手写出错。

4. SQL 路由原理与跨库查询处理

4.1 一条 SQL 的完整执行链路

当你执行SELECT * FROM t_order WHERE user_id = 100时,ShardingJdbc 内部经历了五个阶段:

  1. SQL 解析:把 SQL 解析成 AST(抽象语法树),提取出表名、分片键、查询条件。
  2. 路由计算:根据分片键user_id = 100和分片算法,计算出目标分片是ds0.t_order_0。
  3. SQL 改写:把逻辑表名t_order改写为真实表名t_order_0。
  4. SQL 执行:把改写后的 SQL 发到ds0数据源执行。
  5. 结果归并:如果是多分片查询,把多个分片的结果合并、排序、分页。

这个过程对业务代码完全透明,你写的还是t_order,ShardingJdbc 在底层帮你做了所有脏活。

4.2 精准路由 vs 全路由

精准路由:查询条件里带了分片键,能直接定位到具体分片。比如WHERE user_id = 100,只查一个分片,性能最好。

全路由:查询条件里没有分片键,比如WHERE order_no = 'xxx',ShardingJdbc 不知道数据在哪个分片,只能把 SQL 发到所有分片执行,然后归并结果。这就是常说的"广播查询",性能差,分片越多越慢。

注意:全路由是分库分表后性能下降的主要原因。设计分片方案时,一定要梳理清楚业务的所有查询维度,确保核心查询都能走精准路由。如果确实有非分片键的高频查询,要么建冗余表,要么用基因法把分片键信息编码进查询字段。

4.3 跨库 JOIN 怎么办

分库分表后,跨库 JOIN 是最大的痛点。ShardingJdbc 支持跨库 JOIN,但性能很差,因为它要把参与 JOIN 的表都拉到内存里做笛卡尔积。

处理跨库 JOIN 的几种方案:

  • 字段冗余:把需要 JOIN 的字段冗余到主表里,用空间换时间。比如订单表冗余商品名称、用户昵称。
  • 广播表:对于字典表、配置表这种数据量小、更新少的表,配置成广播表,每个库都存一份全量数据。
  • ER 分片:把有绑定关系的表(比如订单表和订单明细表)用同一个分片键,保证关联数据落在同一个分片,这样 JOIN 就变成了分片内 JOIN。
  • 业务层组装:拆成两次查询,在应用层做数据拼装。这是最灵活的方式,但代码复杂度高。

广播表配置示例:

broadcast-tables: - t_dict - t_config

ER 分片配置示例,订单明细表用order_id作为分片键,但绑定到订单表的user_id分片:

t_order_item: actual-data-nodes: ds$->{0..1}.t_order_item_$->{0..1} database-strategy: standard: sharding-column: user_id sharding-algorithm-name: db-inline table-strategy: standard: sharding-column: user_id sharding-algorithm-name: table-inline

这样订单和订单明细的user_id相同,就会落在同一个分片,JOIN 在分片内完成。

4.4 分页查询的坑

分页是分库分表里最容易出问题的地方。假设LIMIT 100, 10,ShardingJdbc 会改写成LIMIT 0, 110发到所有分片,然后归并排序取第 100-110 条。分片越多,每个分片要扫描的数据量越大,性能越差。

优化思路:

  • 禁止深分页:业务上限制最大翻页数,比如只允许翻到第 100 页。
  • 二次查询法:先查各分片的偏移量,再精确查询。ShardingJdbc 5.x 对分页做了优化,但深分页依然慢。
  • 游标分页:用WHERE id > last_id LIMIT 10代替LIMIT offset, size,避免大偏移量。

5. 数据迁移与扩容方案

5.1 存量数据怎么迁

分片方案上线时,存量数据需要从原单库迁移到分片后的多个库表。迁移方案有几种:

  • 停机迁移:停服,用工具把数据按分片规则导出再导入。简单但影响业务,适合数据量小、可接受停机的场景。
  • 双写迁移:新老库同时写,后台跑迁移任务同步存量数据,数据追平后切读流量到新库。复杂但平滑。
  • 日志订阅迁移:通过订阅数据库变更日志,实时同步到新库。适合数据量大、不能停机的场景。

迁移工具可以用 ShardingSphere-Scaling(5.x 内置的弹性迁移组件),或者自己写迁移脚本。核心逻辑是:读原库数据,按分片算法计算目标分片,写入新库。

5.2 扩容时怎么减少迁移量

取模分片扩容时,比如从 2 库扩到 4 库,取模基数从 2 变成 4,原来user_id % 2 = 0的数据,现在可能变成% 4 = 0或% 4 = 2,需要重新分布。

减少迁移量的方案:

  • 成倍扩容:从 2 库扩到 4 库,原来在 ds0 的数据,一半留在 ds0,一半迁到 ds2。迁移量是总量的一半。
  • 一致性哈希:扩容时只影响相邻节点的数据,迁移量小,但实现复杂。
  • 预留分片:一开始就分 32 个库,前期只用 4 个,扩容时把预留的库启用,不需要迁移数据。

实操心得:如果预期业务会快速增长,建议一开始就预留足够的分片数。比如按 3 年后的数据量设计分片数,前期虽然有些分片是空的,但扩容时直接启用即可,省去迁移的麻烦。

5.3 双写切换的注意事项

双写切换是最常用的平滑迁移方案,但有几个坑:

  • 写顺序:先写新库再写老库,还是反过来?建议先写老库(保证回滚能力),再写新库。
  • 数据一致性:双写期间如果一边写失败,要有补偿机制。可以用本地消息表或者定时对账。
  • 读切换:读流量切换要灰度,先切 1% 观察,再逐步放大。
  • 回滚预案:切换过程中如果发现新库有问题,要能快速切回老库。

6. 常见问题与排查技巧实录

6.1 问题速查表

问题现象可能原因排查方向解决方案
查询报错"找不到表"真实表未创建检查 actual-data-nodes 配置和实际表名按分片规则建表
主键重复worker-id 冲突检查多实例的 worker-id 配置用 IP 或注册中心分配唯一 worker-id
查询性能突然下降走了全路由看 SQL 是否带分片键优化查询条件或建冗余表
分页数据错乱归并排序问题检查 ORDER BY 字段是否在 SELECT 中确保排序字段被查询出来
跨库 JOIN 慢笛卡尔积看执行计划改 ER 分片或字段冗余
连接数暴涨分片数过多检查连接池配置调整连接池大小或减少分片数

6.2 几个真实的踩坑记录

坑一:分片键用了字符串,取模结果不符合预期。

有个项目用order_no做分片键,order_no是字符串,直接取模会报错。后来改成先哈希再取模,用了HASH_MOD算法。但哈希取模有个问题:哈希值分布可能不均匀,导致数据倾斜。解决办法是选一个哈希分布均匀的算法,或者用user_id这种天然均匀的数值字段。

坑二:ORDER BY 和 LIMIT 组合导致结果错乱。

SELECT * FROM t_order ORDER BY create_time DESC LIMIT 10,如果create_time不在 SELECT 字段里,ShardingJdbc 归并时可能拿不到排序字段,导致结果错乱。解决办法是把排序字段加到 SELECT 里,或者用ORDER BY的字段必须出现在查询结果中。

坑三:事务跨分片失效。

ShardingJdbc 支持本地事务和柔性事务。默认是本地事务,跨分片的事务不保证原子性。如果业务需要跨分片事务,要配置XA或BASE事务。但 XA 性能差,BASE 事务需要业务做补偿。我的建议是:尽量把需要事务的操作设计在同一个分片内,比如同一个用户的订单和订单明细用同一个分片键。

坑四:分片算法表达式写错,导致路由到不存在的分片。

INLINE 算法用的是 Groovy 表达式,写错了不会报错,而是路由到错误的分片。比如ds$->{user_id % 2}写成了ds$->{user_id % 3},但实际只有 ds0 和 ds1,就会报错。解决办法是配置好后用单元测试验证路由结果。

6.3 监控与运维建议

分库分表后,监控要跟上:

  • 分片级监控:每个分片的 QPS、响应时间、连接数都要单独监控,及时发现热点分片。
  • 慢查询监控:分片后的慢查询更隐蔽,要开启慢查询日志,定期分析。
  • 数据倾斜监控:定期统计各分片的数据量,如果差异超过 20%,说明分片键选择有问题。
  • 连接池监控:分片数乘以单库连接数就是总连接数,要确保不超过数据库的最大连接数限制。

实操心得:建议在测试环境做一次全链路压测,模拟真实流量,观察各分片的负载情况。很多问题只有在高并发下才会暴露,比如连接池打满、热点分片、归并排序内存溢出等。

7. 一些个人体会

分库分表这件事,技术方案本身不复杂,难的是业务适配和长期运维。我见过太多项目,分片方案设计得很漂亮,上线后因为一个非分片键的查询没处理好,导致全路由拖垮整个系统。

几个真心建议:

第一,分片键的选择要前置到业务设计阶段。不要等表大了才想分片,那时候业务查询已经定型,改造成本极高。在业务初期就规划好分片键,后续扩展会从容很多。

第二,不要过度分片。分片数不是越多越好,每个分片都是一个真实的物理表,分片太多会导致连接数膨胀、元数据管理复杂、运维成本上升。按实际数据量和增长预期算,留一点余量即可。

第三,测试环境要模拟真实数据量。很多分片相关的问题(比如深分页、跨库 JOIN)在小数据量下根本测不出来,必须用接近生产的数据量做压测。

第四,准备好回滚方案。分片改造是伤筋动骨的操作,上线前一定要有回滚预案。双写切换时,老库要保留足够长时间,确认新库稳定后再下线。

最后分享一个小技巧:ShardingJdbc 提供了ShardingSphere-SQL这个工具,可以在日志里打印出 SQL 的路由结果。排查问题时,把日志级别调到 DEBUG,能看到每条 SQL 被路由到了哪些分片,非常直观。这个功能在定位全路由和路由错误时特别有用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询