☰
ShardingSphere分库分表+读写分离实战:从选型到踩坑全解析
2026/10/2 14:28:20 网站建设 项目流程

把"分库分表"这个词放到技术社区里一搜,你会发现两拨完全不同的声音:一拨人把ShardingSphere当成数据库的万能解药,库表还没拆就先想着怎么"螺旋升天";另一拨人恰恰相反,被跨库事务和分布式ID折磨过之后,逢人就劝退。我自己的态度一直没变过——分库分表不是为了炫技,而是当单库真的撑不住的时候,最可控的一条出路。

这篇文章我准备用一个接近真实业务的例子,把ShardingSphere从选型到落地、从分库分表到读写分离的完整链路捋一遍。适合两类人看:一类是数据量已经到了千万级、每天被慢查询和锁等待折磨的后端开发;另一类是虽然项目还在早期,但想提前把数据架构的基本盘打好的架构师。全程用ShardingSphere 5.x,配置以Spring Boot + JDBC模式为主,该说清楚原理的地方我尽量说透,该给配置的地方直接贴可用的YAML,坑也会单独拉出来讲。

1. 先聊明白:你的业务到底需不需要分库分表

很多人一上来就把分库分表挂在嘴边,但真实情况是,大部分系统连单库的优化都没做完。我见过最典型的场景:一张订单表3000万行,索引建得不差,但业务方每天都在凌晨跑全量统计,把CPU吃满,白天接口跟着遭殃。这种问题靠分库分表是解决不了的,它属于"计算与存储没有分层"的问题,应该先去考虑汇总表、离线数仓或分析型数据库。

所以第一步不是配置ShardingSphere,而是先做一场业务体检。对照下面这几种情况判断一下:

  • 单表数据量超过5000万行,并且持续增长,即使加了索引,带分页和排序的查询仍然频繁超时;
  • 单库写入TPS长期高于3000,主库的binlog同步和磁盘IO已经吃紧;
  • 你已经在靠归档表、冷热分离来维持核心表的体积,但归档逻辑越来越难维护;
  • 团队对"再买一台更高的机器"已经没有预期,因为单机配置已经堆到很高。

上面任何一条踩中,那么分库分表才是一个值得考虑的方向。而读写分离则更多是"读多写少"的典型场景:一个订单系统80%以上的请求在查询,主库明明压力不大,但从库可以分流大量的报表、详情和后台查询。需要注意的是,读写分离和分库分表是两个正交的能力,你可以只做读写分离,也可以先做分库分表再叠加读写分离,优先级上我更建议先把分库分表的路由规则搞定,再考虑从库分流,因为分片之后的主从结构天然会变得更复杂。

说回选择了ShardingSphere之后的预期管理。一个拆成4库12表的订单系统,如果连表查询都带着分片键,读写性能会有明显提升;但如果后台运营想根据用户名、时间范围"裸查"全库,这类查询会路由到所有分片然后归并,性能反而不如单库。这不是中间件的问题,而是分布式查询的代价。你得在架构设计上承认这一点,并给运营、报表类查询单独规划通道。

2. ShardingSphere的架构与选型:JDBC模式还是Proxy模式

ShardingSphere的前身是Sharding-JDBC,很多人从1.x开始就在用了。现在它是Apache顶级项目,能力也早就超出了分库分表本身:读写分离、数据加密、影子库、分布式事务等模块都包含在内。理解这一点对于选型很重要,因为你后面可能不只是想要分片能力,还想要一个能统一处理数据安全、流量治理的方案。

当前主要两种接入形态。

JDBC模式:相当于一个增强版的DataSource,嵌入在你的应用进程里。应用拿到的连接就是ShardingSphere包装过的逻辑连接,SQL发出去之后由它解析、路由、改写、归并。对业务代码几乎零侵入,Spring Boot项目引入依赖、写配置即可。额外开销多发生在SQL解析和连接路由这一层,整体性能损耗可控。它只支持Java,因为JVM内运行的原理决定了它天然和Java生态深度绑定。

Proxy模式:一个独立部署的代理服务,对外兼容MySQL或PostgreSQL协议。你的应用不用做任何代码改动,把连接地址改成代理地址就行,多语言客户端(比如PHP、Go)也能直接使用。代价是多一层网络转发,延迟比JDBC模式略高,而且运维上要单独维护Proxy集群的高可用。

总结成一个对比表格,方便决策:

对比项JDBC模式Proxy模式
部署形态应用内嵌,跟随服务部署独立进程,需单独维护
代码侵入低,换数据源即可几乎为零,改连接地址即可
性能损耗较低,无网络转发略高,多一跳
语言限制仅Java支持MySQL/PostgreSQL协议的所有客户端
运维成本低,不需要额外集群中高,需考虑Proxy自身高可用
适合场景Java微服务团队、对延迟敏感的业务多语言团队、不想改动应用的存量系统

如果你问我的建议,Java技术栈、业务又在快速发展期,我一般推荐JDBC模式。原因很直接:它和Spring Boot的集成最顺,出错时可以直接在应用日志里看到路由结果,排障心智负担小。Proxy适合那种客户端语言很杂、或者DBA团队想统一管控数据访问的场景,但你需要额外搭一套基础设施。另外提醒一句,5.x版本对Java版本是有要求的,基于Java 8或Java 11的项目用起来最稳。

3. 环境准备与依赖引入:从零搭建一个可运行的工程

我假设你已经有一组MySQL实例,至少两个库用于分片演示(比如order_db_0和order_db_1),每个库里预先建好三张结构完全相同的订单表。表结构我就不贴完整建表语句了,核心字段包括id、order_id、user_id、amount、status、create_time几个,索引按order_id建唯一索引,按user_id建普通索引。

Maven项目引入下面这些依赖:

<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.3.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency>

版本号你按自己的Spring Boot版本调整,5.3.x对Java 8和Spring Boot 2.x兼容性很好,如果项目已经在Spring Boot 3,建议用更新的5.4.x或以上。我见过一些老项目Starter版本和Spring Boot版本脱节,启动时报一堆DataSource初始化错误,那种问题查起来特别耗时。

接下来是最小可运行的分库分表配置。我先把读写分离放一边,只保留分库分表,这样拆开讲更容易理解:

spring: shardingsphere: mode: type: Standalone repository: type: File props: path: .sharding/ 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_db_0 username: root password: root ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/order_db_1 username: root password: root rules: sharding: tables: t_order: actual-data-nodes: ds$->{0..1}.t_order_$->{0..2} key-generate-strategy: column: id key-generator-name: snowflake database-strategy: standard: sharding-column: order_id sharding-algorithm-name: db-hash table-strategy: standard: sharding-column: order_id sharding-algorithm-name: table-hash binding-tables: - t_order, t_order_item sharding-algorithms: db-hash: type: HASH_MOD props: sharding-count: 2 table-hash: type: HASH_MOD props: sharding-count: 3 key-generators: snowflake: type: SNOWFLAKE props: worker-id: 1 props: sql-show: true

看着长,但拆开就是三块:datasource告诉中间件真实数据源有哪些,rules定义分片规则,props是全局参数。其中sql-show: true强烈建议在测试环境打开,它会在日志里打印每一次SQL的路由结果,比如Actual SQL: ds1 ::: select * from t_order_1,这个信息在你验证分片策略是否正确时是排查利器,能直接确认一条查询到底打到了哪个库哪张表。

启动工程后不要急着写业务代码,先写一个插入订单的接口和按order_id查询的接口,发几个请求,观察日志。你会看到插入时id字段被自动填上了雪花ID,order_id是业务传入的订单号,查询时SQL被路由到正确的分片。这里有个关键点:ShardingSphere不会替你自动建表,它只负责路由。表结构必须提前在每个真实库中建好,数据库连接的用户也需要有对应的读写权限。不少人第一次跑通报"表不存在",基本都是忘记了这一步。

4. 分库分表核心规则配置拆解:表、绑定表与分片算法

实际项目里分库分表的配置不会只有一张表,所以理解每个配置项背后的意图比照抄配置更重要。

分片键的选择是所有设计的前提。我用order_id做分片键,而不是数据库自增主键,原因很现实:业务上我们几乎所有的查询都是"根据订单号查详情、根据订单号查关联明细",订单号天然适合作为路由依据。如果按id分片,那么按order_id查询时ShardingSphere不知道去哪找数据,只能全库扫描,性能无法接受。分片键一定是从高频查询条件里选出来的,这一点要刻在脑子里。

库策略和表策略是两个独立的路由决策。数据库层决定这条SQL进ds0还是ds1,表层面决定进t_order_0还是t_order_2。两者可以使用相同或不同的分片键,数量也可以不同。比如我这里是2库3表,order_id经过db-hash算法得到0或1决定库,经过table-hash算法得到0、1、2决定表。最终一个订单会落在6种组合中的某一个,整体数据分布是均匀的。这里不要用"2库3表正好等于6张物理表"的思路去反推算法,它们各自取模,组合就是6种。

分片算法不是越多越好,选对才是核心。ShardingSphere自带不少算法,最常见的三个需要理解它们的边界:

算法类型支持SQL类型特点
MOD等值查询、IN数值直接取模,分布均匀,但对字符串分片键支持不好
HASH_MOD等值查询、IN先哈希再取模,对字符串和数值都友好,最常用
INLINE等值查询、IN基于行表达式,写法直观,但字符串分片键需要配置分区键和表达式,容易出错

我项目里默认优先HASH_MOD,因为订单号的形态可能是雪花ID的大整数,也可能是带业务前缀的字符串,HASH_MOD能统一处理。范围查询(比如create_time between)是分片算法的天然短板,没有哪个取模算法能优雅支持范围路由,这类查询最终都会走全分片扫描。如果业务确实有强范围查询需求,可以考虑INTERVAL时间分片,那是另一个话题,一般放在日志流水、交易流水这类数据上更合适。

绑定表是JOIN性能的关键。配置里我写了t_order, t_order_item。这两张表是主表和明细表的关系,它们的父子关系都用order_id关联。配置绑定表之后,ShardingSphere在路由时会确保"父表和子表进了同一个分片"——也就是说,一个订单的主记录和明细记录永远在同一个库的同一张关联物理表里。这样JOIN查询不需要跨库、跨表归并,而是退化成普通的物理表JOIN。这个配置看起来不起眼,但对带明细查询的订单系统来说,性能差距可以达到一个数量级。绑定表的核心约束是:关联字段必须是分片键,否则下推逻辑会失效。

分布式主键的生成方式值得多花一分钟。ShardingSphere默认内置了SNOWFLAKE算法来生成主键,它会根据worker-id做机器区分。真实生产环境里,每个应用实例都要配置不同的worker-id,否则不同实例可能生成相同的主键,这一条非常重要。如果你的服务超过1024个实例,或者对ID生成有更高的可控要求,建议提前换成号段模式(比如美团的Leaf思路),ShardingSphere支持通过自定义KeyGenerator接入这类方案。这个后面踩坑环节我再细说。

还有一条必须强调:不带分片键的SQL默认是全路由。比如select * from t_order where user_id = ?,因为user_id不是分片键,ShardingSphere会在所有分片执行同样的查询,再把结果归并起来。这条SQL在数据量大了之后一定慢,因为它是物理上六倍的查询开销。合理做法是给这类查询单独设计一个"路由索引表"(比如user_id到order_id的映射表),或者直接把它送进分析库,不让它在在线链路上裸奔。

5. 读写分离配置与实践:主从延迟问题的处理思路

读写分离的前提是MySQL主从复制已经正常。这里不展开主从搭建的细节,只强调两个基础设施层面的点:一是binlog格式建议用ROW,因为分片环境下多个库的结构一致性要求较高;二是从库的复制延迟要有监控,ShardingSphere只负责流量分发,不会替你解决主从延迟本身。

读写分离的配置逻辑也很清楚:把主库和从库组合成一个逻辑数据源,写入路由到主,读取路由到从。还是用order场景举例,假设现在有两个分片库order_db_0和order_db_1,每个分片下面各有一主一从,配置如下:

spring: shardingsphere: datasource: names: ds0, ds0_replica, ds1, ds1_replica ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/order_db_0 username: root password: root ds0_replica: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3307/order_db_0 username: root password: root ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/order_db_1 username: root password: root ds1_replica: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3307/order_db_1 username: root password: root rules: readwrite-splitting: >

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

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

立即咨询