news 2026/10/1 11:33:31

MySQL + ShardingSphere 分库分表实践:原理、配置与踩坑记录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL + ShardingSphere 分库分表实践:原理、配置与踩坑记录

分库分表这四个字,听起来挺吓人,实际上就是把原本存在一张表里的数据,按照某种规则拆到多个库、多张表里去。这几年我接手过的项目里,因为数据量暴涨、单表撑不住而被迫搞分库分表的,不在少数。用 MySQL 做底层存储,再用 ShardingSphere 做中间层来承接分片逻辑,是目前 Java 技术栈里比较省心的一套组合。这篇内容就是基于 MySQL + ShardingSphere 的分库分表落地记录,把核心原理、配置细节、实操步骤和踩过的坑一次讲清楚,给打算搞或者正在搞分库分表的同学做个参考。

1. 分库分表到底解决什么问题:单库单表的容量天花板

1.1 单表为什么扛不住:从 B+ 树到锁竞争

先聊一个最基础的问题:为什么一张表的数据量大了之后,读写会变慢?

MySQL 的 InnoDB 引擎用的是 B+ 树索引结构,数据量越大,B+ 树的层数就越高。三层 B+ 树能存储的记录数,大概也就是百万到千万级别,因为普通非叶子节点的索引项占用空间有限。一旦数据量涨到几千万甚至上亿,索引深度增加,每次查询要走更多磁盘 IO,性能自然往下掉。

这还只是单次查询的情况。更麻烦的是写入竞争。一张表同一时刻只能有一个写锁(或者行锁但范围也会扩大),当业务流量大起来,insert、update 在这些行锁上排队,延迟就会飙升。再加上 binlog 同步、主从复制延迟、备份恢复时间变长等一系列连锁反应,单库单表的架构很快就到了瓶颈。

很多人会把分区表和分库分表搞混。分区表解决的也是大表问题,但它还是在同一个 MySQL 实例里,只是把数据按照一定规则放在不同的物理分区文件中。分区的本质依然受限于单机 CPU、内存、磁盘 IO 的上限,而且分区键的选择受限、全局唯一索引也不太好处理。分库分表不一样,它是把数据物理地分散到多个库、多个表甚至多台服务器上,从根本上分摊压力。

1.2 分库分表带来哪些能力,又引入哪些问题

分库分表的核心收益有三点:第一,单表数据量可控,索引层级不再失控,查询性能稳定;第二,写入被分散到多张表、多个库,锁竞争大幅降低;第三,资源可以横向扩展,扛不住的时候加机器就行,而不是把单机配置拉到顶。

但收益和代价是并存的。分库分表之后,原本一条 SQL 就能完成的事情,现在可能要被拆成多条 SQL 再合并结果。跨库 JOIN 基本废了,分布式事务成本变高,分页排序要做二次聚合,全局主键不能再用数据库自增,这些全是新的麻烦。

所以我一直有个观点:分库分表不是银弹,它是在单表确实扛不住的情况下才做的技术决策。如果你的数据量还在百万级别,索引建得合理、慢查询都优化过,那老老实实先把单表优化做好,比盲目上分库分表要靠谱得多。

判断是否要分库分表的几个信号,我这里整理一下:

  • 单表行数超过两千万,且查询性能明显劣化。
  • 写入并发高,行锁竞争导致业务超时。
  • 数据增长趋势明确,未来一年内可能翻倍。
  • 单实例的磁盘、CPU、内存已经出现周期性高峰。

如果命中两条以上,就可以认真考虑分库分表了。如果只是偶尔一条 SQL 慢,那大概率是索引问题,别把分库分表当退路,该优化的先优化。

2. ShardingSphere 核心概念与选型思路

2.1 两种形态:ShardingSphere-JDBC 和 ShardingSphere-Proxy 怎么选

ShardingSphere 是 Apache 下的一个开源生态,目前最常用的组件是 ShardingSphere-JDBC 和 ShardingSphere-Proxy。

ShardingSphere-JDBC 是一个轻量级 Java 框架,以 jar 包的形式存在,直接在应用层完成分片路由、SQL 改写、结果归并,应用通过 DataSource 连接就可以使用,代码里基本无感知。因为它在应用进程内执行,所以性能损耗比较小,部署也简单,业务方只需要引入依赖、配置规则、重启应用就能生效。

ShardingSphere-Proxy 则是一个独立部署的代理服务,对应用来说就是一个普通的 MySQL 数据库。应用不需要改造,连上代理就行。Proxy 的优点是支持异构语言、隔离性强、 DBA 可以直接用 SQL 操作,适合团队里有多个语言栈、不方便改应用的场景。缺点是多了一层网络转发,性能有一些损耗,而且需要额外部署和维护一套服务。

我个人的经验是:如果是 Java 技术栈统一的新项目,优先选 ShardingSphere-JDBC,因为它跟 Spring Boot 集成很顺滑,架构简单直接,排查问题也容易。如果你们是多语言混用,或者 DBA 想直接上手管理分片,那考虑 Proxy,但要对性能损耗有心理准备。

两种形态的对比,列个表格更直观:

对比项ShardingSphere-JDBCShardingSphere-Proxy
部署方式应用内集成 jar独立部署代理服务
应用改造需要引入依赖、改数据源配置客户端零改造,当 MySQL 直连
性能损耗较低(进程内计算)较高(多一层网络转发)
支持语言Java任意 MySQL 客户端
运维复杂度低,随应用发布中,需要独立运维
适合场景新项目、Java 统一技术栈存量系统改造、多语言混用

2.2 分片设计:逻辑表、数据节点、分片键和分片算法

理解 ShardingSphere,必须先理解几个核心概念。

第一个是逻辑表。比如你定义了t_order作为逻辑表,它在真实库里并不存在,实际存在的是t_order_0、t_order_1、t_order_2、t_order_3这些物理表。业务 SQL 写的是逻辑表名,ShardingSphere 在运行时把 SQL 改写成真实表的 SQL,这个过程叫 SQL 改写。

第二个是数据节点。数据节点是逻辑表对应真实物理表的位置,用表达式描述,比如ds$->{0..1}.t_order_$->{0..3},表示分布在ds0、ds1两个库上,每个库各 4 张分片表,总共 8 张表。这个写法是 ShardingSphere 特有的$->{...}行表达式,初始化时会自动展开成实际节点列表。

第三个是分片键。分片键用来决定一条记录最终落到哪个物理表,比如订单表用order_id做分片键,用户表用user_id做分片键。分片键的选择极其重要,它直接决定了查询能否精确定位到唯一分片。选分片键的原则是:主查询条件里的高频字段,而且要保证数据分布均匀。

第四个是分片算法。最常见的分片算法有三种:

  • 取模法:order_id % 4,简单粗暴,但数据规模增大需要扩容时,取模的基数一变,大量数据要迁移。
  • 哈希取模:先把分片键做哈希,再取模,可以让字符串类型的分片键分布更均匀。
  • 范围分片:比如按时间段、按区域分片,适合有明显分区维度的数据,但对访问热点不敏感的数据容易歪斜。

选哪种算法,取决于业务模型。订单表、流水表这类数据增长快、又常按主键查询的,用取模或哈希取模很顺手。时间维度明显的日志数据,用范围分片更自然。

另外还有两个容易被忽略的概念:绑定表和广播表。绑定表是指分片规则一致的一组表,比如t_order和t_order_detail都按order_id分片,关联查询时 ShardingSphere 会把两个表的路由结果绑定到同一分片,避免笛卡尔积式的跨库 JOIN。广播表则是所有库都保有一份相同数据的表,比如配置表、字典表,适合这种全量数据需要随时 JOIN 的场景。

3. 实操:基于 ShardingSphere 的分库分表落地

3.1 环境准备:版本匹配是第一道坎

先说环境选择。我这次用的是 MySQL 8.0.28、JDK 8、Spring Boot 2.7.x、ShardingSphere-JDBC 5.1.2。这套组合在项目里跑得很稳。如果你用的是 Spring Boot 3.x,需要对应选择 ShardingSphere 5.3+ 版本,因为 Spring Boot 3 基于 Jakarta EE,老的 starter 可能会不兼容。

Maven 依赖长这样:

<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.1.2</version> </dependency>

引入依赖之后,还需要注意 JDBC 驱动。MySQL 8 对应的驱动类名已经变成了com.mysql.cj.jdbc.Driver,不再是老的com.mysql.jdbc.Driver。如果是从 MySQL 5.x 迁移过来的项目,这个坑很容易踩到,启动的时候直接报无法加载驱动类。

连接 URL 这里有个细节。MySQL 8 默认开启了 SSL 认证,如果你本地的 MySQL 服务器没配 SSL 证书,连接时会报类似SSL connection error的错误。在开发环境里,我在 JDBC URL 上追加了关闭 SSL 的参数来解决:

jdbc:mysql://localhost:3306/db0?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8

注意,生产环境建议还是把 SSL 打开,别为了省事丢掉安全。开发环境关掉只是图个方便。

3.2 一份能跑起来的分库分表配置实例

这里结合我实际的项目场景:订单系统,单表数据增长太快,需要把订单表拆成 2 个库,每个库 4 张表,总共 8 张物理表。

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://localhost:3306/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://localhost:3306/ds1?useSSL=false&serverTimezone=Asia/Shanghai username: root password: root123 rules: sharding: tables: t_order: actual-data-nodes: ds$->{0..1}.t_order_$->{0..3} table-strategy: standard: sharding-column: order_id sharding-algorithm-name: order_inline key-generate-strategy: column: order_id key-generator-name: order_snowflake sharding-algorithms: order_inline: type: INLINE props: algorithm-expression: t_order_$->{order_id % 4} key-generators: order_snowflake: type: SNOWFLAKE props: sql-show: true

这段配置拆开来看:

  • actual-data-nodes定义了物理表的位置,ds$->{0..1}展开为ds0、ds1,t_order_$->{0..3}展开为t_order_0到t_order_3。
  • table-strategy指定了分片键是order_id,分片算法名称指向order_inline。
  • sharding-algorithms里定义的是 INLINE 算法,表达式为t_order_$->{order_id % 4}。这里有个容易踩的坑,如果我对两个库各 4 张表做表级分片,那么每个库内部都是order_id % 4。但如果库级也做了分片,表达式还会更复杂,后面细说。
  • key-generate-strategy配置了雪花算法生成分布式主键。因为订单表的主键不能再用 MySQL 自增,分库分表后各分片表自增 ID 会重复。ShardingSphere 的 SNOWFLAKE 会生成全局唯一主键。

库级分片和表级分片要区分清楚。我这里的配置只在表级别做了分片,两个库只是水平复制关系,所有数据按order_id % 4散落到每个库的 4 张表里。也就是说,ds0.t_order_0和ds1.t_order_0里都有数据,查询时路由结果分别落到两个库。如果你设计的场景是“库分片 + 表分片”两层,那actual-data-nodes和库级分片策略都要配套定义,配置复杂度会翻倍。

3.3 分片键和分片算法的选择,到底该怎么算清楚

分片算法不是随便写的,选错了后面扩容、查询全是坑。拿取模法来说,核心参数就是取模的基数,也就是分片总数。

我在上面示例中使用的是order_id % 4,意思是按订单 ID 的末两位落到 4 张表里。为什么是 4 而不是 8 或者 16?因为当前数据量评估后,每张表未来一年的数据量大约是 500 万行,这个体量对 MySQL 单表来说是舒服的区间。于是分片总数 = 预估总数据量 / 单表目标容量 = 2000 万 / 500 万 = 4 个分片。

这里要注意一个很经典的细节:取模分片的下标是从 0 开始的,order_id % 4的取值范围是 0、1、2、3,对应t_order_0到t_order_3,所以分片总数和物理表数量必须严格对应。

INLINE 表达式看似简单,但数据类型和写法有个隐藏问题。如果分片键是字符串类型,直接用%可能报错。比如用手机号分片,不要直接写mobile % 4,而是先取哈希,再取模。ShardingSphere 里的 HASH_MOD 算法就是干这个的:

sharding-algorithms: user_inline: type: HASH_MOD props: sharding-count: 4

HASH_MOD会把分片键先用 MD5 或一致性哈希处理,再对sharding-count取模,字符串类型的分布效果要好很多。

除了分片算法,分片键还有一个容易忽略的点:业务 SQL 如果不带分片键,ShardingSphere 会把这条 SQL 广播到所有数据节点去执行。听起来好像也能查到,但数据量一大,全表扫描的成本直接爆炸。所以在设计表结构时,就要把业务查询路径理清楚,让核心查询都强制带上分片键。比如订单表按order_id分片,那按用户查订单这种需求,就一定要配套维护一个“用户-订单映射表”,或者用 ES 这类搜索引擎来承接。

3.4 读写分离、分布式 ID 和分布式事务的配套方案

分库分表上线一段时间后,读压力也会跟着上来,这时候通常还要配上读写分离。ShardingSphere 的读写分离配置和分库分表规则是并列的,定义在主库和从库的数据源之上,指定负载均衡策略即可。

一个简化配置的思路如下:在datasource里配置write-ds和read-ds两组数据源,然后在rules下新增readwrite-splitting规则,把write-ds作为主库,read-ds作为从库,查询请求默认走从库。这个机制能有效分担查询压力,但要注意主从同步延迟问题,刚写入的数据如果立刻查,可能查不到。关键业务场景可以通过hint强制路由主库。

分布式 ID 方面,ShardingSphere 内置了 SNOWFLAKE 算法生成分布式主键。雪花算法的好处是全局唯一、趋势递增、性能高,生成的 ID 是 Long 类型。但有个使用细节:雪花算法生成的 ID 末尾有 4 个连续数字位,MySQL 的 BIGINT 可以承载,但如果你用 JavaScript 处理这类 ID,因为 JS 的 Number 精度只有 53 位,会丢失精度,所以前后端交互要用字符串方式传递 ID。

分布式事务是另一个大话题。ShardingSphere 支持三种事务模式:

  • Local Transaction:默认模式,只保证单个分片内的局部事务,跨分片不保证原子性。
  • XA 事务:基于两阶段提交协议,适合跨分片强一致性要求高的场景,缺点是性能损耗明显,事务时间长。
  • BASE 事务:集成 Seata,适合追求最终一致性的长事务场景,对业务侵入小,吞吐量更好。

我的建议是:核心账务类操作用 XA,业务链路过长、有消息补偿机制的用 Seata。不要把分布式事务当万能,尽量通过设计避免分布式事务,比如把同一用户的多笔操作路由到同一个分片上,这样大部分写操作其实都是本地事务,根本不需要跨分片协调。

4. 常见问题与排查技巧实录

4.1 启动报错、驱动不兼容和 SSL 连接的坑

先列我踩过的几个启动期问题。

第一个是Cannot load driver class: com.mysql.jdbc.Driver。原因就是 MySQL 8 的驱动类名变了,ShardingSphere 配置里的driver-class-name必须写com.mysql.cj.jdbc.Driver。这一点看似简单,但项目里如果多个数据源混用,很容易漏改。

第二个是 SSL 连接错误。MySQL 8 默认开了 SSL,如果你没有为服务器配置证书,连接时报错信息会很长,核心是SSL connection error或者Public Key Retrieval is not allowed。MySQL 8 的 caching_sha2_password 认证插件下,还会出现Public Key Retrieval is not allowed的报错,这也是很多人拿到新 MySQL 8 就连接报错的原因。解决方法是在 JDBC URL 加allowPublicKeyRetrieval=true。安全起见,开发环境可以useSSL=false,测试和生产还是建议正常配置 SSL。

第三个是行表达式解析出错。配置里写了ds$->{0..1},但加载时报表达式格式错误。这种问题多半是缩进或引号问题,YAML 里$->{...}的表达式不要加多余的引号,写完配置后先确认表达式能独立解析再启动。

4.2 不带分片键查询导致的全路由风暴

这是运行时最高危的问题。某个查询条件没有包含分片键,ShardingSphere 只能把 SQL 发给所有分片去执行,然后归并结果。在分片少的时候问题不大,一旦分片数到了 8 个、16 个,一个不带分片键的查询就是一次小型全库扫描,而且全部串行或并行执行,数据库压力瞬间飙升。

我遇到过一例:运营后台按订单状态查询订单列表,条件里只传了status,没传order_id,结果每次执行都走全路由,直接把从库打到了 100% CPU。排查方法很简单,开启sql-show: true,在日志里看 SQL 的路由结果,发现每条查询都路由到了全部 8 张表。

解决办法有几个:

  • 业务查询必须限定允许不带分片键的场景,后台低频率查询可以接受,但高频接口绝对不行。
  • 为高频的按用户查询场景,建立“用户-订单映射表”,先把分片键查出来,再带分片键去查订单表。
  • 使用绑定表或广播表减轻 JOIN 场景下的全局路由压力。

另外还可以用 ShardingSphere 的hint强制指定分片,这种手动控制路由的方式在特殊业务里很管用。

4.3 分页排序聚合查询的坑

分页问题在分库分表之后非常典型。假设执行SELECT * FROM t_order ORDER BY create_time LIMIT 100000, 10,ShardingSphere 的逻辑是:每个分片都取前 100010 条,然后在内存里归并排序,再取最终的 10 条。分片越多、偏移量越大,内存和 CPU 的浪费成倍增长。

我踩过最深的一次:运营后台翻页到第 5000 页,接口直接超时,数据库内存撑到接近上限。

应对策略有几个:

  • 限制翻页深度,加上截止条件,比如只能查最近 30 天、只能翻前 200 页。
  • 使用“延迟关联”技巧:先查询出主键 ID,再用主键回表查询详细信息,减少传输量。
  • 如果业务有实时翻页需求,建议在上层引入搜索引擎或者做宽表冗余,不要用 MySQL 分库分表去硬扛深度分页。

聚合查询也有类似的问题。COUNT(*)、SUM()这类操作会在每个分片执行一次,然后归并汇总。如果分片键字段本身和聚合维度不一致,结果可能不准。所以设计分片键时,也要把你最核心的聚合统计维度考虑进去,尽量让高频统计能在单个分片内完成。

4.4 扩容和数据迁移:为什么说配分片方案要想清楚后路

最后聊一下扩容。这是分库分表最难受的一个环节。

如果你用的是取模算法,分片总数从 4 扩到 8,所有数据的分布位置基本全部变化,比如原来order_id % 4落在t_order_1的记录,现在对 8 取模可能落在t_order_1、t_order_5等不同的表里。这意味着大量数据需要迁移,而且迁移过程中还要保证新旧数据保持一致。

可选的迁移方案有几种:

  • 停机迁移:维护时间段内停服,把数据按新规则重新灌入,简单但影响业务。
  • 双写迁移:新旧两套库同时写,迁移历史数据完成后切读流量,过程冗长但不停服。
  • 工具迁移:用 Canal 监听 binlog,把增量数据同步到新库,历史数据用批量任务搬完后再校验。

不管用哪种方案,提前规划分片策略都特别重要。如果业务还处于早期,数据量增长可控,我建议直接用一致性哈希思路,或者至少预留分片数的倍数关系(比如从 4 扩到 8 时,用 4 的倍数扩展),这样老的 4 张表映射到新的 8 张表时可以保持一类映射规律,迁移成本会小很多。

根据我个人的实际感受,分库分表不是一次性的改造,而是一个需要持续打磨的技术底座。最早可以先小规模试点,比如只拆两个库,每库拆两张表,把分片键、分布式 ID、事务边界、SQL 规范全跑通,积累几个月的真实数据和日志,再决定要不要扩大分片规模。

我在实际项目里最深的体会是:分库分表最大的难点从来不是 ShardingSphere 的配置怎么写,而是业务上的分片键设计是否合理。分片键一旦定下来,后面对查询路径、扩容策略、数据迁移的影响是深远的。另外,sql-show这个配置建议一直开着,至少在测试阶段千万不要关,它能直观看到每条 SQL 被路由到哪些分片,排查全路由、笛卡尔积关联、分布式聚合问题都靠它。

最后再分享一个小技巧:如果你刚接手一个没做过数据分层的项目,又评估下来确实需要分库分表,别一上来就追求 16 分片、64 分片这种大规格。先把分片总数设成 4 或者 8,物理机不够就用同一实例多个库先顶着,把链路、监控、告警、数据校验脚本全部准备好,等到业务量真的逼近了,再平滑扩容。能稳定跑起来的方案,比看起来酷炫的方案要值钱得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 11:33:17

网页版手写数字识别:从MNIST数据集到CNN推理的完整落地指南

简介&#xff1a;这份资源面向希望入门深度学习与Web交互的开发者&#xff0c;提供一套基于PyTorch的手写数字识别完整项目&#xff0c;涵盖从数据处理到网页端展示的全流程。包内共131个文件&#xff0c;以124张jpg图片构成分类数据集&#xff0c;另含3个Python脚本、3个txt说…

作者头像 李华
网站建设 2026/10/1 11:31:13

数据库第二次作业全攻略:从库表设计到死锁排查

先交代一点背景。数据库第二次作业&#xff0c;放在很多计算机相关专业的培养方案里&#xff0c;正好是从“会写SQL”过渡到“能把数据库用在真实系统里”的那道坎。第一次作业往往是建表、插入、简单查询&#xff0c;第二次作业就开始上强度了&#xff1a;外键约束、索引优化、…

作者头像 李华
网站建设 2026/10/1 11:31:04

Java程序员转战大模型应用团队:一个月真实感受与收藏必备学习资料

作者分享了从Java开发转向大模型应用团队的第一个月的真实体验。文章指出&#xff0c;虽然大模型应用开发不像网上说的那么“高大上”&#xff0c;但确实比传统业务开发更有意思。作者发现&#xff0c;转行并非等于从零开始&#xff0c;技术栈的快速更新和业务问题的解决更能体…

作者头像 李华
网站建设 2026/10/1 11:30:12

MySQL索引之魂:B+树如何用三层结构解决磁盘IO与查询性能难题

1. 从一条慢查询开始&#xff1a;为什么索引结构会成为数据库的命门做后端开发这些年&#xff0c;我见过太多“SQL优化三板斧”式的操作——加索引、改查询、跑EXPLAIN&#xff0c;好像只要把索引列加上就万事大吉。直到有一次&#xff0c;线上一个订单表到了千万级&#xff0c…

作者头像 李华
网站建设 2026/10/1 11:29:40

C++20 Concepts入门:用约束告别模板报错地狱

这套C的模板从入门到放弃&#xff0c;就卡在报错上。每次递归展开几十层&#xff0c;错误信息动辄几百行&#xff0c;看一眼就头大。C20的Concepts甩掉了这口最大的锅——它把对模板参数的约束直接提升成了语言一等公民&#xff0c;让编译器能明确告诉你“你要的int版本不存在&…

作者头像 李华
网站建设 2026/10/1 11:29:37

MySQL视图、存储过程与触发器:边界、代价与避坑指南

如果你经常跟MySQL打交道&#xff0c;一定绕不开视图、存储过程和触发器这三样东西。它们能把复杂的SQL拆成清晰的功能块&#xff0c;也能在你没想到的角落变成性能黑洞。我上一份工作维护的订单库里&#xff0c;几十个视图、七八个存储过程、外加一堆触发器&#xff0c;改一个…

作者头像 李华