1. 先搞清楚ShardingSphere到底帮你做了什么
很多人一听到“分库分表”就头皮发麻,觉得自己业务还没到那个量级,没必要折腾。其实ShardingSphere并不是大厂专属的“重型武器”,它更像是一个数据层的“路由管家”——你告诉他哪条数据去哪张表、哪个请求该走主库还是从库,剩下的拆分、聚合、路由这些脏活累活,都由它代劳。
我最早接触ShardingSphere是在一次电商订单系统的重构中。当时订单表已经有两千多万行,单表查询明显吃力,尤其是按用户ID和订单状态做筛选的接口,慢查询日志里几乎全是它。我们不是没想过换数据库、上缓存,但历史数据迁移成本、团队学习成本都不小。最终定下来的方案,就是在不改业务代码的前提下,用ShardingSphere做分库分表,顺便把一直没用起来的MySQL主从架构一并接入,实现读写分离。整套方案落地之后,接口耗时从平均800ms降到了80ms左右,效果非常直接。
所以这篇文章我不打算去复述官方文档那一套,而是基于我实际在Spring Boot项目里接入ShardingSphere-JDBC的经验,把“分库分表+读写分离”从原理到配置、从踩坑到调优完整过一遍。适合正在做系统容量规划、被单表数据量困扰、或者准备从单库单表向分布式存储过渡的团队参考。如果你是刚接触ShardingSphere的小白,这篇文章也能帮你建立一条清晰的学习路径。
需要先说明一点:这里讲的是ShardingSphere-JDBC,也就是以jar包形式嵌入应用内的客户端模式。这是目前中小团队用得最广、改造代价最小的方案。后文所有配置和代码,都基于Spring Boot 2.7.x与ShardingSphere 5.1.2版本。
2. 分库分表的底层逻辑与配置拆解
2.1 分片之前,先想清楚要不要分
在动手配置ShardingSphere之前,我建议你先冷静一下。分库分表不是银弹,它本身就是一种“用复杂度换性能”的妥协。如果你的数据量还没到单表千万级、单库连接数还没打满,强行分片只会给自己找麻烦。
什么时候才真正需要分库分表?我总结成两条硬指标:
- 单表数据量超过2000万行,或表容量达到物理磁盘、备份窗口能明显感知的瓶颈。
- 单库的写并发或连接数已成为系统瓶颈,例如数据库连接池频繁报“Too many connections”。
这两条都没触碰到的情况下,优先做索引优化、归档冷数据、引入缓存,性价比远高于分片。反过来说,如果这两条已经命中其中一条,那确实应该认真考虑ShardingSphere了。
ShardingSphere的核心理念是“逻辑表 + 真实表映射”。你在SQL里写的逻辑表名,经过ShardingSphere解析、路由、改写之后,才会真正落到底层某个物理表的SQL。这个机制最大的优势,是业务侧几乎感知不到分片的存在。比如订单系统里,你在Mapper里写的SQL仍然是SELECT * FROM t_order WHERE order_id = ?,但ShardingSphere会根据配置好的分片算法,帮你把这条SQL路由到t_order_0或t_order_1,甚至跨库路由。
配置之前先把分片键定下来,这是整个方案的地基。分片键选择的核心原则就一条:要被最频繁、最核心的查询条件命中。比如订单查询最常见的入口是用户端“我的订单列表”,那么user_id就是天然的分片键;而如果是后台运营按order_id查订单,那order_id更适合。两个查询都高频的情况并不少见,这时候只能用冗余表或二级索引来解决,后面我会详细讲。
2.2 下单前必须搞懂的分片算法
ShardingSphere内置了多种分片算法,我实际用下来,最常见的就是三类:取模、哈希取模、时间范围。这里直接给一个对比表,方便你选型时有个直观参考。
| 分片算法 | 适用场景 | 优点 | 缺点 | 数据分布 |
|---|---|---|---|---|
| MOD(取模) | 数据量稳定、无需扩容 | 配置简单,路由均匀 | 扩容时需要迁移数据或二次取模 | 均匀 |
| HASH_MOD(哈希取模) | 分片键非数值型或分布不均 | 对字符串等非数值分片键友好,分布更均匀 | 计算开销略高于MOD | 较均匀 |
| INTERVAL(时间范围) | 日志、流水、时序数据 | 天然按时间分区,方便归档和冷热分离 | 热点集中在新数据所在的表 | 不均匀 |
MOD和HASH_MOD的区别,很多新手容易混淆。MOD是直接对数值分片键取模,如果你拿user_id这种自增数值做分片键,MOD就够用。但如果分片键是订单号、手机号这类字符串,直接用MOD会得到不均匀的分布,这时候需要用HASH_MOD先做哈希散列,再取模。另外有一个坑,MySQL里字符串哈希用的是CRC32,ShardingSphere支持自定义策略,生产环境我建议直接写一个统一的哈希算法,避免依赖特定数据库实现。
时间范围分片适合日志类场景,但要注意热点问题。比如按月分表,当月的数据永远集中在最新一张表上,读写的热点并没有被分散,只是把历史数据隔离出去而已。如果业务对近期数据访问极频繁,这就不是一个好的分片方案,更适合做归档。
2.3 分片配置示例:5分钟落地一个双库双表
配置这一节,我直接给一个最常见的“双库双表”例子。业务模型假设如下:订单库按user_id分片,分成ds0和ds1两个库,每个库里面t_order_0和t_order_1两张表。
先看配置文件,ShardingSphere 5.x用的是YAML风格,兼容Spring Boot的application.yml。
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.100:3306/order_ds0?useSSL=false&serverTimezone=Asia/Shanghai username: root password: root123 ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.100:3306/order_ds1?useSSL=false&serverTimezone=Asia/Shanghai username: root password: root123 rules: sharding: tables: t_order: actual-data-nodes: ds$->{0..1}.t_order_$->{0..1} table-strategy: standard: sharding-column: user_id sharding-algorithm-name: t_order_table_hash_mod key-generate-strategy: column: order_id key-generator-name: snowflake sharding-algorithms: t_order_table_hash_mod: type: HASH_MOD props: sharding-count: 2 key-generators: snowflake: type: SNOWFLAKE props: worker-id: 1 props: sql-show: true这套配置里,最核心的是actual-data-nodes。它定义了数据节点的分布范围,ds$->{0..1}表示两个数据源,t_order_$->{0..1}表示每个库两张表,两两组合,正好四个物理节点。ShardingSphere会按照这个表达式,把逻辑表t_order映射到四个物理表上。
分片算法这里我用了HASH_MOD,取模基数是2,意味着数据会平铺到两张表里。key-generate-strategy用了雪花算法生成分布式ID,这样可以避免分库分表后使用数据库自增主键导致的主键冲突问题。注意,order_id同时作为主键和逻辑主键,雪花算法生成的ID是全局唯一的,可以放心跨库跨表使用。
配置完成后启动项目,ShardingSphere会在启动阶段校验分片规则。如果actual-data-nodes的表达式写错,启动时会直接报错提示找不到数据节点,这比我见过其他一些组件的“运行期才炸”要友好得多。建议分片键选好后,第一次先在测试环境把路由结果打印出来,确认每条数据到底落在哪个库哪张表,再考虑正式切换。
3. 读写分离配置:把查询压力从主库分流出去
3.1 主从架构与ShardingSphere的配合方式
分库分表解决的是“数据量大”的问题,而读写分离解决的是“并发高”的问题,两件事可以同时做,也可以独立做。如果你的集群目前只有一台MySQL,分库分表落地之后,单库的读写压力仍然存在,这时候接入读写分离就是自然而然的一步。
ShardingSphere的读写分离是“透明”的,你不需要在业务代码里区分主库从库的连接。它通过定义writeDataSourceName和readDataSourceNames,把写操作路由到主库,把查询操作自动分发到从库。这意味着你的Mapper层的代码完全不用改,只需要在配置里声明主从关系即可。
不过有一类特殊操作必须留意:事务内的读写。如果在一个@Transactional事务里先写后读,默认情况下ShardingSphere会把后续的读操作也路由到主库,避免因为主从复制延迟导致读到旧数据。这个机制叫做“同一事务内读写一致性”。ShardingSphere还允许通过hint强制某些查询走主库,比如登录后立即展示用户信息的场景,我会在后面的配置里单独说明。
主从复制的延迟问题,是读写分离绕不开的话题。MySQL默认的复制方式是异步复制,主库写入binlog后不等从库应用完成就返回成功,极端情况下从库可能落后主库几百毫秒甚至更久。对一致性要求非常高的业务,比如库存扣减、支付流水查询,建议要么强制走主库,要么在主从延迟监控上做一些告警。
3.2 Spring Boot + MySQL主从读写分离配置示例
继续用前面订单系统的场景。现在有了一台主库ds-master和一台从库ds-slave,注意这里的ds0和ds1是分片库,而读写分离是在每个分片库内部再做主从。如果你做的是单库读写分离,配置会更简单;但如果你既分片又读写分离,需要这样嵌套配置。
spring: shardingsphere: datasource: names: ds0-master,ds0-slave,ds1-master,ds1-slave ds0-master: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.100:3306/order_ds0?useSSL=false username: root password: root123 ds0-slave: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.101:3306/order_ds0?useSSL=false username: root password: root123 ds1-master: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.100:3306/order_ds1?useSSL=false username: root password: root123 ds1-slave: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.101:3306/order_ds1?useSSL=false username: root password: root123 rules: readwrite-splitting: >try (HintManager hintManager = HintManager.getInstance()) { hintManager.setWriteRouteOnly(); // 这条查询强制走主库 Order order = orderMapper.selectById(orderId); }这个API在分库分表+读写分离同时开启时依然有效。但要注意,HintManager在事务内使用有坑,它会在事务开启时自动失效,所以如果你在@Transactional方法里使用,需要额外小心,最好在事务外层使用。
3.3 金仓数据库的读写分离兼容配置
有朋友问到Spring Boot下金仓(KingbaseES)的读写分离配置,这里单独说一下。金仓作为国产关系型数据库,兼容PostgreSQL协议,但ShardingSphere对它的支持并不是开箱即用,需要做几个关键适配。
首先,驱动必须用金仓自带的JDBC驱动kingbase8,不能使用PostgreSQL驱动替代,否则部分类型映射会出错。Maven坐标是:
<dependency> <groupId>cn.com.kingbase</groupId> <artifactId>kingbase8</artifactId> <version>8.6.0</version> </dependency>数据源配置里,driver-class-name要写成com.kingbase8.Driver,jdbc-url格式是jdbc:kingbase8://192.168.1.100:54321/order_ds0,注意金仓的默认端口不是PostgreSQL的5432,而是54321。如果你们部署时改过端口,以实际为准。
还有一个坑,金仓的系统表和部分语法与MySQL不同。ShardingSphere在启动时会通过metadata加载表结构信息,金仓环境下偶尔会出现“表不存在”或者“relation does not exist”的报错。这个问题的根源在于ShardingSphere默认使用information_schema查询元数据,而金仓对information_schema的实现和PostgreSQL有细微差异。解决办法是在分片规则配置中显式声明逻辑表的字段列表,跳过启动时的元数据探测:
spring: shardingsphere: rules: sharding: tables: t_order: actual-data-nodes: ds0.t_order_$->{0..1} # 显式声明字段,避免元数据探测 table-metadata: t_order: columns: - id - user_id - order_id - amount - status这个配置不是官方文档里常用的,但在我实际项目中确实解决了金仓环境下启动报错的问题。如果你们用金仓做读写分离,其他配置逻辑和MySQL一致,不需要额外改造。
4. 完整实操:从零搭建订单系统的分片+读写分离
4.1 建表SQL与数据初始化
理论讲完,直接上一套完整的可运行方案。业务模型还是用订单表,下面是建表SQL,注意要在每个分片库的每个分片表里都执行一遍。
CREATE TABLE `t_order_0` ( `order_id` bigint(20) NOT NULL COMMENT '订单ID,雪花算法生成', `user_id` bigint(20) NOT NULL COMMENT '用户ID,分片键', `amount` decimal(10,2) DEFAULT NULL COMMENT '订单金额', `status` tinyint(4) DEFAULT NULL COMMENT '订单状态', `create_time` datetime DEFAULT NULL COMMENT '下单时间', PRIMARY KEY (`order_id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;物理表的结构完全一致,只是表名不同。不必纠结建表脚本在多个节点上重复执行的问题,可以写一个简单的Shell脚本循环执行,或者用Flyway这类迁移工具配合多数据源配置。
数据初始化阶段,我建议一定在表里插入几条带特定user_id的测试数据,比如user_id=1、user_id=2、user_id=3各一条,方便后面验证分片结果。不要在这里偷懒,等分片配置出了问题再去排查数据分布,会浪费很多时间。
4.2 项目依赖与启动类配置
Spring Boot项目引入ShardingSphere,Maven依赖如下:
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.1.2</version> </dependency>需要注意版本兼容性。5.1.2对应Spring Boot 2.x,如果你用的是Spring Boot 3.x,需要升级到5.3+;另外shardingsphere-jdbc-core-spring-boot-starter这个组件的坐标在5.4之后改过,如果拉取不到,去Maven仓库看最新的artifactId。
启动类的配置也有一点特殊要求。因为ShardingSphere会接管数据源,SpringBootApplication上不要加@MapperScan之外的特殊数据源配置,否则容易冲突。我的Get启动类的写法是:
@SpringBootApplication @MapperScan(basePackages = "com.example.order.mapper") public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }不要继承DataSourceAutoConfiguration做任何手工构建数据源的操作。ShardingSphere的starter会自动装配好一切,你只需要在配置文件中声明多个数据源即可。官方文档在这个地方写得比较克制,但我见过不少新手在这里踩坑:自己又定义了一个DataSource的@Bean,结果和ShardingSphere的数据源发生了冲突,启动后SQL全部走了本地数据源,分片配置形同虚设。
4.3 核心代码实现示例
为了验证分片,我写了一个极简的OrderMapper,就两个接口:插入订单和查询订单列表。
public interface OrderMapper { int insert(Order order); List<Order> selectByUserId(@Param("userId") Long userId); Order selectByOrderId(@Param("orderId") Long orderId); }XML文件如下:
<insert id="insert"> INSERT INTO t_order (order_id, user_id, amount, status, create_time) VALUES (#{orderId}, #{userId}, #{amount}, #{status}, #{createTime}) </insert> <select id="selectByUserId" resultType="Order"> SELECT * FROM t_order WHERE user_id = #{userId} </select> <select id="selectByOrderId" resultType="Order"> SELECT * FROM t_order WHERE order_id = #{orderId} </select>注意,这里的t_order全部是逻辑表名,不是物理表名。ShardingSphere会对SQL做解析,识别出分片键user_id或order_id后,改写成实际物理表名再下发到底层数据库。
插入数据的Service层如下:
@Service public class OrderService { @Resource private OrderMapper orderMapper; public void createOrder(Long userId, BigDecimal amount) { Order order = new Order(); order.setOrderId(SnowflakeIdUtils.nextId()); order.setUserId(userId); order.setAmount(amount); order.setStatus(1); order.setCreateTime(new Date()); orderMapper.insert(order); } public List<Order> listByUserId(Long userId) { return orderMapper.selectByUserId(userId); } }雪花ID的生成,这里我不建议每个项目都自己实现一套,直接用MyBatis-Plus的IdWorker,或者引入Hutool的Snowflake工具类,都足够稳定。关键点是workerId的分配,多节点部署时一定要保证每台机器唯一,否则生成的主键有可能冲突。
4.4 验证分片路由结果
配置好以后,写一个简单的Controller或者测试方法,连续插入几条不同user_id的数据,然后分别在两个物理库里查询。
我实际跑了一轮,得到的结果是:
user_id = 1的数据落在ds0.t_order_1user_id = 2的数据落在ds1.t_order_0user_id = 3的数据落在ds0.t_order_1
这个分布结果恰好印证了HASH_MOD算法的计算过程:hash(user_id) % 4分别对应了四个物理节点。因为你用的是user_id做分片键,查询时只要带上这个字段,ShardingSphere就能直接定位到具体的物理表。根据副带的打印信息,SQL改写前是SELECT * FROM t_order WHERE user_id = 1,改写后变成了SELECT * FROM t_order_1 WHERE user_id = 1,路由目标精准落在了一张表上。
但如果你查询时没有带分片键会怎样?比如直接SELECT * FROM t_order WHERE amount > 100,ShardingSphere无法定位分片范围,只能全路由,也就是对四个物理表都执行一次查询,再把结果集合合并。这种操作的性能损耗极大,所以业务侧要时刻保持“查询尽量带分片键”的自觉。全路由偶尔做一次全表扫描没有问题,但不能成为常态。
5. 常见问题与排查技巧实录
5.1 数据分布不均与扩容困境
这是分库分表之后最先遇到的问题。比如HASH_MOD取模基数是4,一开始数据均匀,但业务增长到一定程度后,单一分片的数据量还是逼近瓶颈,就需要扩容。扩容的难点在于取模基数变了,原有数据的分布全部需要重新计算和迁移。比如从4个分片扩到8个分片,原来hash % 4 = 1的数据,现在hash % 8可能等于5,全部要搬。
解决思路有两个方向。一个是提前规划分片数,比如直接分成32片、64片,短期内不用动;另一个是引入一致性哈希算法,它可以让扩容时只有部分数据需要迁移。ShardingSphere没有内置一致性哈希,但支持自定义分片算法,实现起来不难。建议有长期增长预期的团队,从第一天就用一致性哈希,省掉后面扩容的折腾。
另外,冷热数据不均也是常态。比如按user_id分片的老用户数据和新用户数据,老用户查得多、写得多,新用户可能一直不活跃。这时候HASH_MOD带来的均匀分摊反而会造成“热分片”问题。业务上要配合缓存和归档策略,避免单分片负载过高。
5.2 分布式事务怎么选
分库分表之后,原本单库上的ACID事务变成了跨库事务,ShardingSphere提供了几种方案,我直接把对比列出来。
| 事务方案 | 一致性级别 | 性能损耗 | 适用场景 |
|---|---|---|---|
| LOCAL(本地事务) | 单库强一致 | 无 | 事务操作只涉及单一分片 |
| XA(两阶段提交) | 跨库强一致 | 较高 | 金融级、强一致要求高 |
| BASE(柔性事务) | 最终一致 | 较低 | 高并发、允许短暂不一致 |
我个人的建议是:绝大多数业务场景,能用LOCAL就不要上XA。因为分库分表下,事务操作通常会带上分片键,比如更新某个用户的订单,只会命中一个分片,那就完全可以用本地事务,不必承担XA的性能损失。只有那种一次操作牵扯多个用户、多个订单的强一致场景,才值得考虑XA。
ShardingSphere 5.x在Spring Boot中使用XA事务很简单,加一个@Transactional注解,再引入对应的XA事务依赖即可。但千万不要在XA事务里做长事务操作,它会把所有分片连接锁住,高并发下必然死锁。经验值是事务内所有SQL操作控制在200ms以内。
5.3 分页排序与聚合函数的全路由问题
跨分片的分页查询和聚合是另一个高频踩坑点。比如SELECT * FROM t_order ORDER BY create_time LIMIT 10, 20,如果没有分片键条件,ShardingSphere会对每个分片执行一次LIMIT 10, 20,然后在自己内存里合并排序,最后再取第10到30条。如果数据总量巨大,内存开销非常恐怖。
这里我给一个实用建议:分页查询永远带上分片键范围条件。你不能只传pageNum和pageSize,要同时传入userId。这样ShardingSphere就能把SQL路由到单个分片,分页就变成了普通的单表分页,性能和准确性都有保障。后台管理系统的全局列表查询,建议用搜索引擎或宽表来支撑,不要硬抗物理分片。
COUNT(*)聚合也一样,不带分片键则要对所有分片执行一遍再汇总,对多个分片实时count确实是重活。如果业务上有高频的计数需求(比如未读消息数、订单总数),建议用缓存存储计数,或者用冗余表异步汇总。
5.4 绑定表与广播表的正确使用
分库分表场景下,多数SQL都是单表操作。但如果你的订单表和订单明细表都要分片,而且明细表也按user_id分片,那么关联查询时就要配置绑定表。绑定的意思是,两张表在分片时使用相同的分片键和分片算法,这样关联查询时不需要跨库跨表JOIN,直接路由到同一分片内的两张表即可。
spring: shardingsphere: rules: sharding: binding-tables: - t_order, t_order_item绑定表配置好处非常明显:避免笛卡尔积。如果不配置绑定,ShardingSphere对t_order JOIN t_order_item这种SQL会做全路由,四个分片两两组合,产生16种路由结果,再合并数据。配置绑定后,直接缩减为4种。
广播表则是那些不需要分片、但每个分片库都要存在相同数据的表,比如省份字典、订单状态枚举。ShardingSphere做SQL路由时,广播表的修改操作会自动同步到每个分片库,查询时则任意路由到一个分片即可。配置方式:
spring: shardingsphere: rules: sharding: broadcast-tables: - t_dict_region这两类配置在实际项目里非常实用,尤其对那种模型里有父子表关系的系统,强烈建议在建表初期就规划好绑定关系,否则后期数据都进去了再调整物理表结构,代价极高。
5.5 元数据加载慢与连接池耗尽
启动阶段ShardingSphere需要加载元数据,如果你的分片节点很多、表数量庞大,启动时间可能被拖得很长。团队在压测环境里遇到过启动耗时2分钟的情况,排查后发现是元数据加载时创建了大量数据库连接,而连接池又被业务接口占满,导致启动过程卡死。
解决办法有两个方向。第一个是调大HikariCP的maximum-pool-size,比如从默认10调到30,给元数据加载留出余量;第二个是预热数据源,启动完成后先执行几个简单查询,让所有分片节点建立连接,避免首次请求穿透到数据库层产生明显的RT尖刺。
ShardingSphere的sql-show参数在排查问题时非常有用,它会把改写前后的SQL打印到日志里。我在测试环境中始终开着它,但在生产环境会关闭,因为它会带来一定的日志IO损耗,而且敏感SQL可能被记录到日志文件里。
6. 从单体到分片:我看清的几个真相
如果让我总结个人在ShardingSphere项目里最强烈的体会,“先试点、后铺开”是首要原则。环境准备、依赖引入、分片策略规划、主从同步确认,每一步都要留足验证时间,千万别指望配置完就能无缝上线。我见过团队推倒重来的案例,原因就是上线前没有做完整的分片键覆盖度测试,结果大量后台统计SQL走全路由,数据库直接被打满。
第二个体会是分库分表从来不只是DBA或架构师的事,它需要业务研发深度参与。一个复杂系统的查询入口往往有几十个,每个查询是否带分片键,直接影响最终的路由效率。团队应该在设计阶段就建立一份“分片键使用检查清单”,所有SQL上线前都要过一遍。
最后说一下很多人纠结的“要不要自研”。如果你有专门的基础设施团队,丰富的数据库中间件经验,自研确实可以做到更贴合业务。但中小团队没有这个必要,ShardingSphere的能力边界足够覆盖绝大多数分片场景,而且社区反馈问题也快。真正拉开差距的,从来不是工具本身,而是你对自己业务数据访问模式的认知深度。