分库分表这个话题,聊的人多,真正落地的人少。中间件选型、分片键设计、配置写法,每一步都有坑。最近在一个新项目里把 ShardingSphere-jdbc 5.5.0 和 Spring Boot 完整集成了一遍,从依赖引入到分片规则配置,再到读写分离和问题排查,踩了不少雷。这篇就当作一份实战记录,给准备入手的兄弟做个参考。
先说结论:如果你的项目是 Java 技术栈、Spring Boot 应用,数据量已经涨到单表扛不住的程度,ShardingSphere-jdbc(也就是 JDBC 模式)是目前最平滑的切入方式。它不要求你改 SQL、不引入额外的中间件节点,以一个增强版数据源的形式嵌在应用里,配置好分片规则之后,逻辑表映射到物理表、SQL 路由改写这些脏活累活都交给它处理。这套东西的入门门槛不高,但配置项里的细节非常多,网上很多文章停留在跑通 demo 的阶段,真正接近生产环境的配置和坑点很少有人写透。
1. 为什么选 ShardingSphere-jdbc:两条技术路线的取舍
1.1 代理模式与驱动模式,选哪种
分库分表中间件大致分两类。一类是代理模式,典型代表是 MyCat 和 ShardingSphere-Proxy。应用连接的是代理,代理再转发到真实的数据库。这种方案对应用侵入性最小,但多了一层网络转发,延迟会增加,而且代理节点本身成了高可用架构里的一环,运维成本不低。
另一类是驱动模式,也就是 ShardingSphere-jdbc。它本质上是一个增强版的 JDBC 驱动和数据源包装器。应用直接连真实库,SQL 从发出到执行这一整条路径上,解析、改写、路由都是在应用内存里完成的。没有额外的网络跳转,性能损耗远小于代理模式,部署上就是加一个依赖、写一段配置的事。
我选驱动模式的另一个理由是调试方便。代理模式下,SQL 从应用到代理再到数据库,链路长了,出了问题很难判断是哪一段出的岔子。JDBC 模式下,控制台能把改写后的真实 SQL 直接打出来,一眼就能看出 SQL 被路由到了哪个库、哪张表,排查问题爽快得多。
如果你的团队没有专职的 DBA 和中间件运维人员,JDBC 模式更容易接受,因为它的形态就是一个数据源,Spring Boot 里替换成本极低。
1.2 5.x 的配置体系变化
想特别提醒一点,ShardingSphere 在 4.x 升到 5.x 的时候,配置方式是推倒重来的。4.x 时期用的是 properties 风格,一堆sharding.rules.table...前缀的键值对,可读性差,而且很多命名在 5.x 里已经废弃。
5.x 全面转向 YAML 风格,分片算法做成了可插拔模式——每个算法定义在sharding-algorithms节点下,起个名字,表规则里通过sharding-algorithm-name去引用。这个设计比 4.x 干净很多,算法复用、调整都方便。
5.5.0 是 5.x 系列里比较成熟的版本,对 Spring Boot 2.x 和 3.x 都有适配。如果你直接用 Spring Boot 3.x,不用太担心老版本那种 javax 和 jakarta 的兼容问题,但还是要确认没有其他老依赖在拖后腿,这个后面细说。
1.3 你的数据量真的需要分片吗
这话可能有些人不爱听,但该说还是要说:分库分表不是银弹,它解决的问题相对单一,就是单表数据量过大导致的操作性能下降。
如果你的库还在单表百万级别,索引加上去、慢查询优化一下、Redis 缓存顶一顶,大概率能撑很久。这个投入产出比远高于上一套分库分表中间件。真正需要分片的场景一般满足这几条:单表数据量几千万甚至上亿、写入吞吐上不去了、单库连接数不够或者磁盘容量顶不住。
更关键的是,一旦定了分片规则,分片键的选择就是个架构决策,后面想改基本等于重做一套。所以动手之前,一定把业务查询模式想清楚,把两年内的数据量增长也算进去。
2. 工程搭建:版本组合与依赖引入
2.1 版本选型避坑
先把环境交代清楚,我这次项目的组合是:
| 组件 | 版本 |
|---|---|
| Spring Boot | 2.7.18 |
| ShardingSphere-jdbc | 5.5.0 |
| MyBatis-Plus | 3.5.5 |
| MySQL | 8.0.x |
| JDK | 1.8 |
为什么没直接上 Spring Boot 3.x?因为项目里还有不少老依赖,从 javax 迁到 jakarta 的工作量不小。如果你是新项目,Spring Boot 3.2+ 配 5.5.0 完全没问题,官方已经适配了新命名空间。
版本这里有个容易被坑的点:ShardingSphere 的版本号和 Spring Boot 的版本号不是简单的“一一对应”关系,中间隔了好几个小版本。建议用 5.4.x 或 5.5.x,这两个版本对 Spring Boot 2.7 和 3.x 的兼容性调整都做完了,别去用 5.0.0 这种早古版本,生产环境会教做人。
2.2 Maven 坐标的正确姿势
依赖坐标是个大坑,我必须单独拿出来说。网上大量文章写的还是老坐标:
<!-- 网上常见的旧写法,5.5.0 里不要用 --> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.5.0</version> </dependency>这个坐标在很多 5.x 小版本里已经调整了。5.5.0 官方推荐的是直接引入shardingsphere-jdbc这个聚合坐标,它会自动带上 Spring Boot 集成需要的模块:
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc</artifactId> <version>5.5.0</version> </dependency>我第一次就是照着旧文章拉依赖,IDE 里包怎么都拉不下来,最后去翻官方文档才发现坐标换了。这种坑完全可以通过看官方文档避免,别偷懒。
2.3 单数据源约束与 ORM 配合
配置 ShardingSphere 之后,一个隐含的约束要记在心里:Spring 容器里的 DataSource 会被 ShardingSphere 接管,你不能在项目里自己定义第二个数据源。如果你原来的项目用了 dynamic-datasource 这种多数据源框架,要先停掉,把多个库统一挪到 ShardingSphere 的datasource配置块里管理。不然会出现数据源互相覆盖、bean 循环依赖等莫名其妙的问题。
和 MyBatis-Plus 配合倒是很顺利。MyBatis-Plus 的selectById、selectPage这些方法在逻辑表上正常工作,ShardingSphere 会把 SQL 改写成对真实表的操作。但注意一点:MyBatis-Plus 的代码生成器或者手写的 SQL 里,表名一定要写逻辑表名(比如t_order),不能写t_order_0这种物理表名。写了物理表名,ShardingSphere 就不会帮你路由,这条 SQL 会直接打到它解析出的那个数据源上,等于绕过了分片规则。
3. 分片规则配置:从分片键到表规则
3.1 分片键怎么定才合理
动手配置之前,先把分片逻辑想清楚。我习惯用订单系统举例来推演。
假设有 2 个物理库(ds0、ds1),订单表逻辑上叫t_order,物理上拆成 4 张表(每个库 2 张:t_order_0、t_order_1)。这里最关键的选择是分片键。
订单表的高频查询场景一般就是两类:查某用户的订单列表(WHERE user_id = ?),查某笔订单详情(WHERE order_id = ?)。如果只拿user_id一个字段做分片键,按order_id查详情就会变成全路由——ShardingSphere 不知道这个订单在哪个分片上,只能所有分片都查一遍。如果只拿order_id做分片键,按用户查列表又会全路由。
所以在真实项目里,我倾向于采用“复合分片”的思路:库分片键用user_id,表分片键也用user_id(或者和用户维度强相关的字段)。这样一来,同一个用户的所有订单都在同一个库同一张表里,查询走精确路由,不会跨库 join。代价是单个用户的数据量特别大时会存在热点,但绝大多数业务场景里,单用户数据量远达不到这个量级,这个取舍是划算的。
另外记住一个原则:分片键最好选择分布均匀、不会频繁更新的字段。手机号、用户 ID 这类就很合适。状态、类型这种枚举值少的字段千万别拿来分片,否则数据都堆在几个分片上,分片形同虚设。
3.2 一份完整 YAML 配置拆解
直接给一份我能跑通的完整配置,逐段拆开讲:
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/db_order_0?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: root@123 max-pool-size: 20 min-pool-size: 5 ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.11:3306/db_order_1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: root@123 max-pool-size: 20 min-pool-size: 5 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_mod table-strategy: standard: sharding-column: user_id sharding-algorithm-name: table_mod key-generate-strategy: column: id key-generator-name: snowflake_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_mod table-strategy: standard: sharding-column: user_id sharding-algorithm-name: table_mod binding-tables: - t_order, t_order_item broadcast-tables: - t_dict sharding-algorithms: db_mod: type: MOD props: sharding-count: 2 table_mod: type: MOD props: sharding-count: 2 key-generators: snowflake_id: type: SNOWFLAKE props: worker-id: 1 max-tolerate-time-difference-seconds: 5 props: sql-show: true数据源部分:ds0、ds1是数据源标识符,必须和规则里的ds$->{0..1}对应上。$->{0..1}本质是 Groovy 表达式,展开就是ds0、ds1。如果数据源起名不规范,比如叫primary_order_datasource,表达式就得写成$->{['primary_order_datasource', 'secondary_order_datasource']}这种,可读性很差。我建议一律用ds0、ds1这种短命名。
真实表映射:actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}表示每个库里有t_order_0和t_order_1两张表。注意,这个表达式只负责路由,不负责建表。数据库里没有对应的物理表的话,启动可能不报错,但 SQL 一执行就会报“表不存在”,所以建表脚本必须和表达式一致。
分片策略:database-strategy和table-strategy下面我都用了standard标准分片策略,意思是用单一分片键决定路由。sharding-column指定分片键是user_id,sharding-algorithm-name引用下面定义的db_mod取模算法。
key-generate-strategy:分布式主键生成。分库分表之后绝对不能用数据库自增主键,多分片之间主键必冲突。这里用 ShardingSphere 内置的SNOWFLAKE雪花算法生成器,worker-id在同一个分片环境里要全局唯一,多个应用实例如果配了相同的 worker-id,生成的 ID 会重复,这是个生产环境的隐患。
props 配置:sql-show: true打开 SQL 日志,开发阶段必开,排查路由问题靠它。生产环境记得关掉,日志量非常大,而且会把物理表结构暴露在日志里。
3.3 分片算法选型对比
5.x 内置的算法类型很多,我用一个表把常用的列出来,方便你选型:
| 算法类型 | 说明 | 适用场景 |
|---|---|---|
| MOD | 分片键直接取模 | 分片键是数字,且分布均匀,比如数字 ID |
| HASH_MOD | 分片键先哈希再取模 | 分片键是字符串,比如手机号、订单号,需要打散分布 |
| INLINE | Groovy 行表达式,支持条件判断 | 需要按状态、按区间定制路由逻辑,灵活度高 |
| INTERVAL | 按时间区间分片 | 日志表、流水表,按天或按月分片 |
| CLASS_BASED | 自定义算法实现类 | 内置算法满足不了复杂业务逻辑时兜底 |
我上面配置按user_id % 2分库、user_id % 2分表,是因为user_id是个从用户表带过来的数字 ID,本身分布均匀,MOD 就够了。如果分片键是订单号这种字符串,建议用 HASH_MOD,让哈希先把分布打散再取模,不然字符串直接取模可能全挤在同一个分片里。
分片策略除了 standard,5.x 还支持complex(复合分片键)和hint(强制路由)。complex 需要自己实现ComplexKeysShardingAlgorithm,适合一个表里多个分片键配合路由的场景。hint 不走 SQL 的 WHERE 条件,而是由代码显式指定要路由到哪个分片,适合分片键不在 SQL 里、但在调用上下文中能拿到的场景。基础配置阶段,standard 加 MOD/HASH_MOD 已经能覆盖大部分需求。
3.4 绑定表与广播表
绑定表这个配置能解决一个很大的性能问题,必须专门讲。
订单表和订单明细表是高频 join 的场景。如果两张表的分片键都是user_id,分片数量一致,那么配置了binding-tables之后,ShardingSphere 就知道这两张表中同一个用户的数据一定落在同一个分片上,join 的时候不会跨库跨表。
如果不配置绑定表会怎样?ShardingSphere 会按笛卡尔积来路由。4 个订单分片 × 4 个明细分片,一次 join 就变成 16 次路由查询,慢到你怀疑人生。这是实际项目里最容易踩的隐形性能坑。
广播表是另一类表:字典表、配置表这种所有分片都要保留一份完整数据的表。比如t_dict,每个库都建一张一模一样的表,配置了broadcast-tables之后,INSERT/UPDATE/DELETE 会自动同步到所有数据源,查询只会路由到其中一个分片。这让字典表的读写都不用你做额外处理。
3.5 雪花主键与前端精度问题
这里有个特别容易被忽略的坑:ShardingSphere 雪花算法生成的 ID 是 19 位 Long,精度超出了 JavaScript 的 Number 安全整数范围(2^53)。也就是说,后端 ID 传给前端之后,前端拿这个 ID 再传回来做查询,末位数字已经变了,根本查不到数据。
这个坑在前后端分离的项目里特别常见。解决方案不复杂:接口返回时把 Long 类型的主键序列化成字符串。如果你用 Jackson,可以给主键字段加@JsonSerialize(using = ToStringSerializer.class),或者全局配置一个 Long 转 String 的序列化器。数据库里继续用 bigint,Java 里继续用 Long,只是 JSON 传输层转成字符串,两边都舒服。
4. 读写分离叠加配置
4.1 主从数据源配置
如果你的分片库每个都做了主从复制,ShardingSphere 可以在分片之上再叠加读写分离。每个分片的数据源从“单个库”变成“一组主从库”。配置上是在rules层加一个readwrite-splitting规则:
spring: shardingsphere: rules: readwrite-splitting: >t_order: actual-data-nodes: rw_ds$->{0..1}.t_order_$->{0..1}这样分片和读写分离就叠加起来了。要理解的是,读写分离里的read-data-source-names可以配多个从库,但写库只有一个。从库挂了的话,需要配置动态感知或者依赖负载均衡策略健康检查,这是生产环境要额外考虑的点。
4.2 负载均衡与事务的取舍
读库负载均衡算法有三种主流选择:
- ROUND_ROBIN:轮询,请求均匀分布到各个从库,适合从库配置一样的场景
- RANDOM:随机分配,适合从库数量多、连接压力分散的场景
- WEIGHT:按权重分配,适合从库硬件性能有差异的场景
实际项目里,如果两个从库配置一样,直接 ROUND_ROBIN。如果从库是新旧机器混用的,用 WEIGHT,给性能好的机器更高权重。
这里有个事务相关的铁律必须知道:服务里一旦开启事务,读写分离就失效了,所有操作都会走主库。原因很现实——从库和主库之间有复制延迟,事务内读从库可能读到旧数据,这会造成逻辑错误。所以如果你在 Service 方法上加了@Transactional,哪怕方法里只有一条 SELECT,这条 SELECT 也会打在主库上。
不要试图去关掉这个特性,这是为了保证数据一致性必须做的取舍。如果你接受不了,可以从业务上避免长事务、避免在事务里做查询,把读操作尽量放到事务外面。
5. 常见问题与排查技巧实录
5.1 启动失败排查
最常见的问题是启动直接报数据源配置错误,集中在下面几个点:
依赖坐标错误。5.5.0 要用
shardingsphere-jdbc这个坐标,不是老文章里的-starter结尾那个。依赖拉不下来,先查坐标对不对。YAML 缩进层级错乱。
datasource和rules都在spring.shardingsphere下面,缩进错了会导致配置被静默忽略,Spring Boot 找不到数据源就崩了。YAML 的报错信息经常不明确,优先检查缩进对齐。项目里残留其他数据源配置。比如
spring.datasource.url、spring.datasource.druid.*这类配置,只要存在就会被 Spring Boot 自动配置抢走数据源初始化流程。ShardingSphere 模式下,把其他数据源配置全部删掉,只留spring.shardingsphere.datasource这一套。
5.2 SQL 路由日志怎么看
打开sql-show: true之后,控制台会打印类似这样的日志:
ShardingSphere-SQL: Logic SQL: SELECT * FROM t_order WHERE user_id = 123 ShardingSphere-SQL: Actual SQL: ds0 ::: SELECT * FROM t_order_0 WHERE user_id = 123这种日志是排查问题的核心工具。:::左边是数据源名,右边是改写后的真实 SQL。看到这条日志,你就能确认:逻辑 SQL 被正确路由到了 ds0,表也被改写成了t_order_0。
排查问题的思路是四步走:
- 看逻辑 SQL 是否就是你写的原始 SQL
- 看
:::左边的数据源对不对,不对就是库分片规则有问题 - 看真实表名对不对,不对就是表分片规则有问题
- 看 WHERE 条件里分片键的值是否能命中正确的分片
5.3 分页查询踩坑
分库分表之后,分页查询性能问题会被放大。比如这条 SQL:
SELECT * FROM t_order ORDER BY create_time LIMIT 100000, 20ShardingSphere 的处理方式是:每个分片都取前 100020 条数据,然后归并排序,最后取第 100000 到 100020 条。4 个分片就是扫描 4 份 100020 条数据,深分页时性能断崖式下跌。
这个问题的解法一般有三种:
- 业务层面限制最大翻页数,超过 100 页就提示用户改用条件筛选
- 用游标分页替代 offset 分页:
WHERE create_time > ? ORDER BY create_time LIMIT 20,每次把上一次查询的最大 create_time 传进去 - 深分页场景直接交给 Elasticsearch 这类搜索引擎,MySQL 只负责承接最近的热数据查询
另外,如果你用的是 MyBatis-Plus 的selectPage,记得确认分页插件版本和 ShardingSphere 兼容。我的经验是,MyBatis-Plus 3.5.x 配合 ShardingSphere 5.5.0 可以正常工作,但如果你自己写拦截器再做一次分页改写,很容易造成双重分页,这种问题排查起来非常费劲。
5.4 分片键更新限制与 DDL 同步
分片键不能出现在 UPDATE 语句的 SET 子句里。比如:
UPDATE t_order SET user_id = 100 WHERE id = 1这条 SQL 会被 ShardingSphere 直接拒绝,因为改了分片键等于数据要从一个分片迁移到另一个分片,中间的链路极其复杂。如果业务上真的需要变更用户归属,正确做法是:先查旧数据、新分片插入、旧分片删除、处理分布式事务,千万别指望一条 UPDATE 搞定。
还有一个日常要用的注意事项:分库分表之后,DDL 操作不会自动同步。你在t_order_0上加了字段,t_order_1不会自动跟着变。所有建表、加字段、加索引的操作,都需要对每个真实表执行一遍。表少的时候手动执行还行,表多了建议写成循环脚本统一执行,不然漏掉一张表,线上跑 SQL 就开始报“列不存在”,这种线上事故我见过太多次了。
事务方面再补一句:ShardingSphere 默认的本地事务不支持跨库分布式事务。如果单个事务里同时操作了 ds0 和 ds1 的表,一致性是没法保证的。跨库写操作一定要引入 Seata 或者 ShardingSphere 的 XA 事务模式,那是另一套配置和架构决策了,基础阶段先记住这个边界。
做这套配置的时候,我最大的体会是:ShardingSphere 不复杂,但它的配置项和概念非常多,逻辑表、物理表、分片算法、分片策略、绑定表、广播表、分布式主键,每一个都值得花时间吃透。先把基础的分片跑通,再逐步叠加读写分离、分布式事务这些能力,比自己上来就一把梭要稳得多。最后再分享一个建议:配置上线之前,一定要写一个校验脚本,确认每个分片上的物理表结构完全一致,字段缺失这种问题在分片环境下会造成数据错乱,这是无论怎么强调都不为过的习惯。