
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里字符串哈希用的是CRC32ShardingSphere支持自定义策略生产环境我建议直接写一个统一的哈希算法避免依赖特定数据库实现。时间范围分片适合日志类场景但要注意热点问题。比如按月分表当月的数据永远集中在最新一张表上读写的热点并没有被分散只是把历史数据隔离出去而已。如果业务对近期数据访问极频繁这就不是一个好的分片方案更适合做归档。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?useSSLfalseserverTimezoneAsia/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?useSSLfalseserverTimezoneAsia/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?useSSLfalse 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?useSSLfalse 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?useSSLfalse 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?useSSLfalse 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 groupIdcn.com.kingbase/groupId artifactIdkingbase8/artifactId version8.6.0/version /dependency数据源配置里driver-class-name要写成com.kingbase8.Driverjdbc-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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;物理表的结构完全一致只是表名不同。不必纠结建表脚本在多个节点上重复执行的问题可以写一个简单的Shell脚本循环执行或者用Flyway这类迁移工具配合多数据源配置。数据初始化阶段我建议一定在表里插入几条带特定user_id的测试数据比如user_id1、user_id2、user_id3各一条方便后面验证分片结果。不要在这里偷懒等分片配置出了问题再去排查数据分布会浪费很多时间。4.2 项目依赖与启动类配置Spring Boot项目引入ShardingSphereMaven依赖如下dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-core-spring-boot-starter/artifactId version5.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); ListOrder selectByUserId(Param(userId) Long userId); Order selectByOrderId(Param(orderId) Long orderId); }XML文件如下insert idinsert INSERT INTO t_order (order_id, user_id, amount, status, create_time) VALUES (#{orderId}, #{userId}, #{amount}, #{status}, #{createTime}) /insert select idselectByUserId resultTypeOrder SELECT * FROM t_order WHERE user_id #{userId} /select select idselectByOrderId resultTypeOrder 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 ListOrder 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 100ShardingSphere无法定位分片范围只能全路由也就是对四个物理表都执行一次查询再把结果集合合并。这种操作的性能损耗极大所以业务侧要时刻保持“查询尽量带分片键”的自觉。全路由偶尔做一次全表扫描没有问题但不能成为常态。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的能力边界足够覆盖绝大多数分片场景而且社区反馈问题也快。真正拉开差距的从来不是工具本身而是你对自己业务数据访问模式的认知深度。