1. 从一张千万级订单表的噩梦说起
先说个场景。前两年我做了一个电商类项目,订单表在业务高峰期单日新增几十万条,两个月不到就突破了两千万。刚开始MySQL扛得住,索引优化+读写分离还能凑合。等到了三千万级,分页查询开始明显变慢,报表统计直接让人崩溃,DBA半夜打电话说主库的磁盘IO已经打满了。后来咬牙上了分库分表,才算把这口气喘过来。
那次用到的核心方案就是ShardingSphere,当时还是4.x的老版本。今年新项目启动,技术栈升级到Spring Boot 2.7 + ShardingSphere 5.2.1,配置方式和老版本差别不小,网上资料也比较杂。我把这次快速接入的完整过程、踩过的坑、以及一些设计取舍思路整理出来,希望对正要上手ShardingSphere 5.x的朋友有帮助。
这篇文章适合谁看?已经决定用分库分表,但还没选好具体落地方案的后端开发;或者已经引入了ShardingSphere但被规则配置、分片策略绕晕的兄弟。我会尽量把配置每一行的含义讲清楚,而不是扔一段配置就让你自己猜。
2. 为什么选ShardingSphere 5.2.1,而不是MyCat或Sharding-JDBC老版本
先说结论:5.x版本的ShardingSphere-JDBC,在Spring Boot生态下集成体验比4.x和MyCat都要顺。
2.1 ShardingSphere-JDBC与MyCat的本质区别
很多人会纠结ShardingSphere和MyCat怎么选。这两货定位完全不同:
- MyCat是代理层方案,独立部署一套中间件服务,应用通过JDBC连接MyCat,由MyCat做SQL解析、路由、分发和结果聚合。
- ShardingSphere-JDBC是客户端方案,以jar包形式嵌入应用进程,应用直连多个真实数据源,分片逻辑在JVM内部完成。
从性能上讲,ShardingSphere-JDBC走的是本地调用,少了代理层一次网络往返,损耗更低。从运维上讲,ShardingSphere-JDBC不需要单独维护一套中间件集群,对中小团队更友好。从开发体验上讲,Spring Boot工程引入一个依赖、配一段YAML就能跑,而MyCat需要额外搭建服务、维护XML规则文件,上手门槛高不少。
2.2 为什么不用4.x老版本
4.x时期ShardingSphere的配置风格是偏Spring Boot Starter的properties风格:
spring.shardingsphere.datasource.names=ds0,ds1 spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://localhost:3306/ds0 spring.shardingsphere.sharding.default-database-strategy.inline.sharding-column=user_id spring.shardingsphere.sharding.default-database-strategy.inline.algorithm-expression=ds$->{user_id % 2}5.x改成了标准化的YAML配置,分片策略改成了可插拔的算法定义,配置结构更清晰,而且把数据加密、读写分离、分片治理等功能做了模块化拆分。加上5.2.1这个版本在社区反馈里相对稳定,网上踩坑案例也多,出了问题基本上搜得到答案。
3. 环境准备与依赖引入,最容易翻车的细节全在这
3.1 版本匹配问题
ShardingSphere 5.2.1要求Spring Boot版本必须注意兼容性。我自己实测的稳定搭配是:
| 组件 | 版本 |
|---|---|
| Spring Boot | 2.7.6 |
| ShardingSphere-JDBC | 5.2.1 |
| MyBatis-Plus | 3.5.3 |
| mysql-connector-java | 8.0.31 |
| MySQL | 5.7 / 8.0 |
Spring Boot 3.x暂时不要配ShardingSphere 5.2.1,因为3.x基于Jakarta EE规范,ShardingSphere 5.2.1的某些SPI实现还停留在javax命名空间,会直接启动报错。我一开始图省事用了Spring Boot 3.0.2,结果报了一堆ClassNotFound,后来老老实实降回2.7.x才消停。
3.2 依赖引入,注意排除冲突
在pom.xml中引入ShardingSphere-JDBC依赖时,只需要一个核心依赖:
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.2.1</version> </dependency>这里有个大坑:shardingsphere-jdbc-core-spring-boot-starter会传递依赖一个旧版本的Spring Boot autoconfigure,如果你的Spring Boot版本恰好是2.7.x,一般没事,但如果是2.5以下,可能出现数据源自动配置冲突。建议额外排除:
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.2.1</version> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency>3.3 数据库准备
我这里模拟一个最经典的场景:订单表按用户ID分片,2个库,每个库4张表。也就是order_ds0库中有order_0到order_3四张表,order_ds1库同样这四张表。
先建两个库:
CREATE DATABASE order_ds0 DEFAULT CHARACTER SET utf8mb4; CREATE DATABASE order_ds1 DEFAULT CHARACTER SET utf8mb4;每个库中建同样的表结构:
CREATE TABLE order_0 ( id BIGINT NOT NULL, user_id BIGINT NOT NULL, order_no VARCHAR(64) NOT NULL, amount DECIMAL(10,2) NOT NULL, create_time DATETIME NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;在建物理表的时候,字段建议加上DEFAULT CURRENT_TIMESTAMP这类默认值,因为分片后如果应用中没显式赋值,代理层在某些场景下不会自动补默认值,容易造成插入失败。
4. 核心YAML配置逐行解析,搞懂每个参数的含义
这是本文的重头戏。ShardingSphere 5.2.1在Spring Boot项目中的配置不像老版本那样拆得七零八落,一个application.yml就能搞定。我把完整配置贴出来,然后逐段解释。
4.1 完整配置示例
spring: shardingsphere: datasource: names: ds0,ds1 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/order_ds0?useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/order_ds1?useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 rules: sharding: tables: order: actual-data-nodes: ds$->{0..1}.order_$->{0..3} table-strategy: standard: sharding-column: user_id sharding-algorithm-name: t_order_inline database-strategy: standard: sharding-column: user_id sharding-algorithm-name: db_order_inline key-generate-strategy: column: id key-generator-name: snowflake binding-tables: - order default-database-strategy: none sharding-algorithms: db_order_inline: type: INLINE props: algorithm-expression: ds$->{user_id % 2} t_order_inline: type: INLINE props: algorithm-expression: order_$->{user_id % 4} key-generators: snowflake: type: SNOWFLAKE props: sql-show: true4.2 数据源定义
datasource: names: ds0,ds1这里定义了分片涉及的所有真实数据源名称。注意jdbc-url,不是Spring Boot里常见的url。ShardingSphere有自己的属性绑定逻辑,写成url会直接启动失败,报DataSource的property not found之类的错。
数据源类型我用的HikariCP,ShardingSphere 5.x默认如果没有显式指定type,实际走的也是Hikari。如果你在项目里想换Druid,也可以改成:
type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver但我的建议是:没特殊需求就别换,Hikari性能足够好,而且ShardingSphere和Hikari的兼容性验证做得最充分。
4.3 逻辑表与实际物理表的映射
tables: order: actual-data-nodes: ds$->{0..1}.order_$->{0..3}这是整段配置的灵魂。order是逻辑表名,业务代码里SQL只写order,ShardingSphere自动把它路由到实际的物理表。
ds$->{0..1}表示ds0和ds1两个库,order_$->{0..3}表示order_0到order_3四张表。如果不写这句,ShardingSphere不知道物理表分布情况,所有SQL都会尝试路由到ds0.order,直接报表不存在。
这里有个注意事项:逻辑表名不要和某个物理表名重合。比如你定义了逻辑表order,结果物理表里恰好有张ds0.order,在部分老版本中可能出现路由混乱。规避方式很简单,逻辑表用单数,物理表加后缀,比如t_order_0、t_order_1,逻辑表就叫t_order。
4.4 分片算法与策略绑定
ShardingSphere 5.x把分片算法(algorithm)和分片策略(strategy)拆开了。算法负责计算路由结果,策略负责指定哪个字段参与计算、使用哪种算法。这个设计比4.x清晰很多。
sharding-algorithms: db_order_inline: type: INLINE props: algorithm-expression: ds$->{user_id % 2} t_order_inline: type: INLINE props: algorithm-expression: order_$->{user_id % 4}INLINE是内置的行表达式分片算法,简单说就是基于Groovy表达式做计算。这里user_id是分片键,user_id % 2决定去哪个库,user_id % 4决定去哪张表。
然后通过策略把算法和逻辑表关联起来:
table-strategy: standard: sharding-column: user_id sharding-algorithm-name: t_order_inline database-strategy: standard: sharding-column: user_id sharding-algorithm-name: db_order_inlinestandard策略适用于单分片键的精确查询和范围查询。它要求分片算法必须支持=和IN操作,INLINE类型天然支持。如果你的业务里经常用BETWEEN或范围查询,强烈建议用STANDARD类型自定义算法,因为INLINE对范围查询的路由处理比较粗糙,可能扩大扫描范围。
4.5 分布式主键生成
key-generate-strategy: column: id key-generator-name: snowflake分库分表后,数据库自增主键会冲突,所以必须由应用层生成全局唯一主键。ShardingSphere默认集成了雪花算法(Snowflake),生成的是一个64位Long类型ID。
这里有个细节:雪花ID默认生成的是一个带符号的Long,在JavaScript前端处理时可能溢出精度,因为JS的Number类型最大安全整数是2^53-1。如果你有前后端分离需求,后端返回的ID建议转成String类型再返回给前端。我当时就是用Jackson全局配置,把Long统一序列化为String,才解决了订单ID在前端失真的问题。
4.6 sql-show开关
props: sql-show: true开发环境建议打开,生产环境务必关掉。这个开关会把每条SQL的路由结果打印到日志里,比如原生SQL和实际路由后的SQL、路由到的数据源和表。排查路由问题基本靠它。
5. 实际运行效果与SQL路由验证
配置写完,我们来验证一下效果。我写一个简单的插入和查询Demo。
5.1 插入数据验证路由
假设有个订单服务接口:
@Service public class OrderService { @Resource private OrderMapper orderMapper; public void createOrder(Order order) { orderMapper.insert(order); } }插入一条user_id为1001的订单:
Order order = new Order(); order.setUserId(1001L); order.setOrderNo("NO1001"); order.setAmount(new BigDecimal("199.00")); orderService.createOrder(order);打开sql-show日志,会看到类似输出:
Logic SQL: INSERT INTO order (id, user_id, order_no, amount) VALUES (?, ?, ?, ?) Actual SQL: ds1 ::: INSERT INTO order_1 (id, user_id, order_no, amount) VALUES (?, ?, ?, ?)1001 % 2 = 1,所以进了ds1;1001 % 4 = 1,所以表是order_1。路由完全按预期走。
5.2 查询数据验证跨库聚合
再来看查询:
public List<Order> getOrdersByUserId(Long userId) { return orderMapper.selectByUserId(userId); }如果查userId = 1003,日志会显示:
Logic SQL: SELECT * FROM order WHERE user_id = ? Actual SQL: ds1 ::: SELECT * FROM order_3 WHERE user_id = ?这里要注意:WHERE user_id = ?条件里包含了分片键,ShardingSphere能精确路由到一张表。如果查询条件里没有user_id,比如WHERE order_no = ?,ShardingSphere只能广播到全部数据源和全部表,然后把结果合并返回。这就是所谓的全路由,数据量大时性能会很难看。
所以在设计分片键时,所有核心查询必须尽量带上分片键。这是分库分表项目里最重要的业务约束。
5.3 绑定表的作用:避免笛卡尔积
我们再扩展一下,如果有一张order_item明细表和order表关联查询:
binding-tables: - order, order_item绑定表的意思是:order和order_item在分片规则上保持一致(比如都按user_id分片),关联查询时,ShardingSphere会意识到两个表在同一个数据源的同一索引下,直接做本地join,而不会产生笛卡尔积。
如果没有配置绑定表,ORDER表和ORDER_ITEM表在join时的路由可能产生4x4=16种组合(两个库两张表分别交叉),性能非常差。配置后可减少到4种组合。这算是一个不起眼但收益巨大的配置项。
6. 我踩过的几个坑,逐个复盘
6.1 事务问题:@Transactional失效
分库分表后,跨库事务是最大的问题之一。ShardingSphere 5.2.1默认不开启分布式事务,这意味着如果你在一个@Transactional方法里同时写ds0和ds1的数据,普通的Spring事务管理是管不住第二个库的。
我第一次测的时候就踩了这个坑:一个下单逻辑,先插入order表(路由到ds1),再插入order_item表(可能路由到ds0),结果第二步抛异常,第一步的数据照样提交了。
解决方案有几种:
- 尽量把同一个业务实体的分片键设计在同一库,让一次操作只涉及一个数据源。比如订单主表和明细表都用user_id分片,且分片策略一致,就能保证一次操作落在同一个库。
- 如果真的需要跨库事务,引入
shardingsphere-transaction-xa-core依赖,或者用Seata这类分布式事务框架。
但老实说,对大部分业务场景,方案1比方案2更务实。分布式事务的性能损耗和实现复杂度都很高,尽量不要让核心链路依赖它。
6.2 INLINE表达式里的Groovy坑
INLINE算法的表达式是基于Groovy解析的,有几个坑必须注意:
第一,分片键值不能为null。如果插入数据时user_id没有赋值,ShardingSphere会在路由阶段抛异常。我在代码里统一做了参数校验,凡是不带user_id的写操作直接拒绝。
第二,表达式里不要写函数调用。我看到有些教程推荐ds$->{(user_id >> 4) % 2}这种写法,实测部分版本里Groovy解析可能有问题。建议哪怕多写几行,也保持表达式简单,只做取模运算。
第三,取模运算分布不均匀。如果user_id是自增趋势的,取模后的数据分布还算均匀。但如果user_id有某种规律(比如全是偶数),会导致所有数据都落到同一个库。建议在业务上保证分片键的离散性,或者分片键设计时考虑用hash值取模。
6.3 排序与分页的坑
分库分表后的ORDER BY和LIMIT不是你想的那么简单。
比如:
SELECT * FROM order WHERE user_id IN (1001, 1002) ORDER BY create_time DESC LIMIT 10这条SQL会路由到多个分片,每个分片各自执行LIMIT 10,然后ShardingSphere在内存中合并排序,最后取前10条。这里有一个经典问题:如果某个分片的数据不足10条,合并结果可能不是真正的全局前10条?
实际ShardingSphere的处理逻辑是:每个分片先取LIMIT 10(在5.x中会进行流式归并优化),然后在内存中统一排序,再取前10条。这种方式在数据均匀分布时是正确的,但如果某个分片的前10条在全局排序中根本排不上号,而这个分片又有大量数据排在后面,就可能出现结果偏差。
业界通用的做法是:分库分表场景下,不要做深分页。LIMIT 100000, 10这种查询在每个分片上都要各取100010条,性能极差。如果业务上必须做翻页,建议用游标方式或者限定最大翻页深度。
6.4 广播表的妙用
除了分片表,ShardingSphere还支持广播表。如果你的系统里有一些字典表、配置表,数据量不大但需要和所有分片表关联,可以把它们配置成广播表:
sharding: broadcast-tables: - t_dict配置后,t_dict会被自动复制到所有数据源中。写入广播表时,ShardingSphere会同时向所有数据源写入;查询广播表时,随机选择一个数据源即可。这样就可以在每个分片内做本地join,避免跨库关联。
6.5 字段大小写和数据库关键字冲突
如果表名或字段名是order,在MySQL里虽然不算严格关键字,但部分SQL解析器可能出问题。我建议把核心表命名为t_order这样的前缀形式,规避风险。
6.6 多数据源共存问题
很多项目里不只是分库分表,还有别的业务库。比如分片订单库之外,还需要连一个系统配置库。这种情况不能把配置库也放进ShardingSphere的datasource里,否则会被当成分片数据源参与路由。
ShardingSphere 5.2.1支持多数据源混用的能力有限,我的方案是:
- 分片涉及的库全部交给ShardingSphere管理,业务代码里使用默认数据源(也就是ShardingSphere的DataSource)。
- 独立配置库用单独的
DataSourceConfig手动配置,并通过@Qualifier指定。
6.7 启动时报错:Table 'order' doesn't exist
这种报错通常是因为逻辑表名与物理表名不匹配,或者actual-data-nodes写错。检查顺序:
- 物理表是否确实存在(用Navicat直接看一下)。
actual-data-nodes中的表名和库名是否完全匹配。- 逻辑表名是否和物理表名重复了。
7. 读写分离+分库分表同时配置
如果你的库还有主从架构,ShardingSphere 5.2.1支持读写分离和分库分表同时配置。一个实际项目中的配置大致长这样:
spring: shardingsphere: datasource: names: ds0,ds0_slave,ds1,ds1_slave ds0: jdbc-url: jdbc:mysql://主库0地址:3306/order_ds0 ds0_slave: jdbc-url: jdbc:mysql://从库0地址:3306/order_ds0 ds1: jdbc-url: jdbc:mysql://主库1地址:3306/order_ds1 ds1_slave: jdbc-url: jdbc:mysql://从库1地址:3306/order_ds1 rules: readwrite-splitting: >HintManager hintManager = HintManager.getInstance(); hintManager.setWriteRouteOnly(); try { // 这里查询强制走主库 } finally { hintManager.close(); }但这不是常态方案,最好的办法是在业务层面规避,比如插入后跳转页面时,页面数据从后端接口异步加载而不是依赖立刻返回的最新数据。
8. 分库分表上线前后的检查清单
这部分是我从两次实际项目经历中总结的,有必要在动手前逐条过一遍:
8.1 上线前
- 分片键是否在所有核心表的增删改查SQL中都存在?没有分片键的查询会全路由,务必提前发现并改造。
- 是否会用到
JOIN多表?如果是,确认相关表是否配置为绑定表。 - 是否有事务贯穿多个分片?如有,确认是否需要分布式事务方案。
- 是否有字段作为分片键但值可能是null?如有,代码层必须做拦截。
- 是否涉及字典表、地区表等公共数据?如有,配置为广播表。
- 分页查询的最大深度是否可控?如不可控,考虑查询方案改造。
- 全局主键策略是否确定?推荐雪花ID,注意JS精度问题。
8.2 上线后
- 打开
sql-show: true观察一段时间,确认每条SQL的路由结果符合预期。 - 监控各分片的数据量增长是否均匀,数据倾斜严重时考虑更换分片键或算法。
- 慢SQL排查:分片后的SQL可能因为路由不均匀导致单库压力过大,注意观察各库的负载。
9. 顺着这个方案还能往哪扩展
ShardingSphere 5.2.1的能力不只是分库分表。在这个项目基础上,还能做几件很自然的事:
一是数据加密。如果订单手机号、用户身份证这类敏感字段需要加密存储,可以配置数据加密规则,让应用层对业务无感。5.x把加解密也收敛到了rules体系下,配置风格和分片规则一致。
二是弹性迁移。如果现有的单库单表想平滑迁移到分库分表,ShardingSphere有数据迁移工具,不过5.2.1版本的迁移功能主要是面向ShardingSphere-Proxy的。如果只是想把存量数据手动拆到分片里,建议写一次性脚本,按分片算法计算目标库表后批量搬运。
三是ShardingSphere-Proxy。如果想完全不侵入应用,或者开发团队不想改数据库连接方式,Proxy模式是另一种选择。JDBC模式适合新项目从代码层直接集成,Proxy模式适合老系统改造、数据库团队接管的场景。
就拿我最近这个项目来说,ShardingSphere-JDBC跑了大半年,整体很稳,出问题的点大多集中在业务SQL不带分片键、事务跨库、以及主从延迟上。把这三个问题在项目前期设计好,后面基本不用操太多心。如果你正准备在Spring Boot项目里接分库分表,希望这份记录能帮你少走几步弯路。