
开头可以直接从问题切入。很多团队把分库分表当成“终极大招”以为上了 ShardingSphere 就能解决所有性能问题但实际上分库分表是一个一旦做了就很难回头的架构决策。MySQL 在单库单表数据量达到千万级、亿级之后索引维护成本、写入锁竞争、备份恢复时间都会显著恶化这时候分库分表确实是绕不开的路。但问题在于很多人还没想清楚“为什么要分”“怎么分”“分完以后怎么收场”就匆匆引入了 ShardingSphere最后不是被分布式事务坑就是被跨库查询折磨。这篇文章我会基于 ShardingSphere MySQL 这套组合把从决策、选型、配置到上线后排查的完整链路讲清楚。适合三类人看一是业务数据量已经上来、正在评估是否需要分库分表的后端开发二是已经决定接入 ShardingSphere 但不太清楚配置细节的工程团队三是想了解分库分表落地后有哪些坑的架构师和 DBA。1. 先搞清楚一件事你的数据真的需要分库分表吗分库分表不是性能优化的第一步而是很多手段都用尽之后才考虑的路。我在现实里见过太多团队订单表才几百万行就急着拆库拆表结果系统复杂度直线上升业务迭代效率被拖垮。所以这一章先泼冷水帮大家理清决策逻辑。1.1 单库单表的真实瓶颈在哪里MySQL 的 InnoDB 引擎使用 B 树作为索引结构单表数据量涨到一定程度后主要瓶颈并不是“查询变慢”这么简单。先说几个被低估的点索引层级加深B 树索引在数据量小的时候可能只有 23 层量级到了千万甚至上亿后层数增加每次索引查找的随机 IO 次数变多延迟不再是毫秒级而是上升一个量级。写入竞争加剧特别是使用自增主键和频繁更新时热点页争抢会严重影响并发性能。一个表的写入瓶颈远早于查询瓶颈出现。备份和恢复的窗口变长单表 100GB 和单表 10GB 的物理备份时间完全不是一个概念。即使业务能忍DBA 也忍不了每周备份超过两个小时。Blob/Text 大字段的拖累如果有大字段数据页的利用率会下降行溢出还会带来额外的 IO全表扫描会成为灾难。一般来说当单表行数超过 2000 万行InnoDB 官方建议附近或者单表物理文件超过 50GB 时就该认真评估分库分表了但重点不是这个绝对数字而是数据库的写入能力和核心查询延迟是否已经明显劣化。1.2 先做这些事能拖延就拖延很多人一查慢 SQL 就归咎于“数据量太大”实际上有三种情况远比分库分表优先级高索引设计问题。我见过一张 500 万行的订单表查询条件里有 user_id 但索引建的是 order_no导致每次用户查订单都全表扫描。这种情况回到一条ALTER TABLE ADD INDEX就能解决。未使用覆盖索引。大量SELECT *导致回表成本高把高频查询的字段组合成覆盖索引性能提升非常明显。架构层面的读写分离。把读流量从主库拆走往往能解决 80% 的“数据库扛不住”问题。按我的经验MySQL 单库在合理索引和读写分离的前提下撑到日均千万级读写是可行的。分库分表应该是最后手段而不是最先想到的方案。1.3 什么时候才必须分对四个典型信号的判断如果你遇到了下面四个信号中的两三个那就不要犹豫分库分表值得投入单表数据量已达到亿级并且持续上涨即便做了分区Partition和归档核心热表仍在膨胀。主库写入成为明确瓶颈读写分离和硬件升级如换 SSD、加内存都解决不了。单个数据库实例的连接数被打满应用侧连接池一再调大也无济于事或者主从延迟已经无法控制。业务有明确的多租户或区域性隔离需求比如按用户维度天然可以水平拆分。判断逻辑很简单如果瓶颈来自单表索引深度、单实例并发上限、备份窗口而不是慢 SQL 和索引问题那就果断走分库分表。2. ShardingSphere 核心概念先把这几个术语吃透再去碰配置ShardingSphere 是目前国内使用最广泛的分库分表中间件Apache 顶级项目。它有两个大方向ShardingSphere-JDBC 和 ShardingSphere-Proxy。很多人一上来就看配置结果连“逻辑表”和“实际表”都没分清自然配置得一团糟。2.1 两种接入模式JDBC 和 Proxy到底选哪个这两种模式差别相当大直接决定你的部署架构和应用改造成本。对比维度ShardingSphere-JDBCShardingSphere-Proxy部署方式以 jar 包形式集成在应用内作为增强版 JDBC 驱动独立部署的数据库代理服务应用直接连接代理支持的语言Java 应用原生支持任何支持 MySQL/SQL 协议的客户端网络开销无中间层应用直连数据库多一跳增加少量网络延迟运维复杂度低跟随应用发布高需要单独维护代理集群适合场景Java 技术栈为主、追求性能和低延迟异构语言、不想改应用的团队或者需要综合管理多个数据库我在实际项目中绝大多数 Java 团队都会优先选择 ShardingSphere-JDBC因为它不走额外网络层性能和稳定性都更好把控。但如果你是 PHP、Go 或者其他语言团队不想大规模改代码那就老实选 Proxy。不过要提醒一句Proxy 本身是有状态的服务连接数、内存消耗都要额外规划别把它当成无状态网关来看待。2.2 逻辑表、实际表、分片键配置文件的四个核心元素逻辑表/逻辑库你在 SQL 里写的表名例如orderShardingSphere 会根据配置把它路由到真实表。配置里逻辑表名就是你业务代码中的名字业务层几乎不用改 SQL。实际表/物理表数据库中真实存在的表例如order_0、order_1、order_2。分片键决定一条数据落到哪张表的字段通常是订单号、用户 ID 等分布均匀的字段。分片算法根据分片键计算出目标表 / 目标库的规则。举一个简单的例子如果业务代码里写的是INSERT INTO order (user_id, order_no, amount) VALUES (?)那么 ShardingSphere 拿到这条 SQL 后会根据user_id字段做哈希取模决定把它路由到order_0还是order_1等物理表。2.3 分片算法选型哈希取模、范围分片还是按时间分片这一步选错后面会非常痛苦。我给的参考逻辑其实很朴素按键取模Mod例如user_id % 16数据分布最均匀适合大多数核心交易场景。但只要分片数量一变化扩容数据迁移几乎是强制性的。哈希取模 一致性哈希桶数固定后变动代价低适合对扩容要求高的场景但一致性哈希在数据分布均匀性上不如简单取模而且实现复杂度高。范围分片Range比如按order_id范围切分查询条件天生带边界适合日志类或流水类数据但是容易产生热点分片——最新写入的都是最后一段老分片又闲置。按时间分片Interpolation本质上是范围分片的一种非常合适流水型业务如交易流水、日志配合定期归档效果很好。经验法则是核心在线交易表优先按业务主维度做哈希取模日志流水表优先按时间分片如果你知道未来扩容方向从一开始就多预留分片数量而不是先建 4 个分片、下个月再加到 16 个。3. 基于 MySQL 的分库分表实战配置从零跑通一个订单表下面这部分我尽量按一个完整可运行的路径来写。我以 ShardingSphere-JDBC 5.x 版本为例因为 4.x 和 5.x 的配置结构差异比较大5.x 是当前主线版本社区资料也最全。3.1 环境准备先准备好三个数据库实例为了体现“分库分表”我会准备两个数据库实例或同一个 MySQL 实例下的两个库命名为order_db_0和order_db_1。每个库内建 4 张订单表t_order_0、t_order_1、t_order_2、t_order_3。这样组合起来就是 2 库 × 4 表 8 个分片。建表 SQL 如下我用的是 MySQL 8.0引擎 InnoDB字符集 utf8mb4CREATE DATABASE IF NOT EXISTS order_db_0 DEFAULT CHARACTER SET utf8mb4; CREATE DATABASE IF NOT EXISTS order_db_1 DEFAULT CHARACTER SET utf8mb4; -- 分别在两个库里执行 CREATE TABLE IF NOT EXISTS t_order_0 ( id BIGINT NOT NULL, order_no VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL, amount DECIMAL(12,2) NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意一个细节分片表如果同时含id和order_no两个唯一性字段常规做法是把order_no也建为唯一索引但在分布式分片环境下这个唯一性约束的维护成本很高而且一旦 ShardingSphere 按user_id路由order_no这样的唯一索引只能保证片内唯一无法保证全局唯一。这也就是为什么很多分库分表方案会把订单号设计成“全局唯一但业务查询主要靠 user_id”的原因。3.2 引入依赖并搭建 Spring Boot 工程如果你在 Spring Boot 中使用 ShardingSphere-JDBC核心依赖只需要一个dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-core-spring-boot-starter/artifactId version5.4.1/version /dependency我建议用 5.3.x 或 5.4.x 这种比较稳定的版本5.5 之后部分 API 有调整社区资料匹配度需要自己先确认一下。另外提醒一点ShardingSphere 对数据源连接池并没有绑定要求HikariCP、Druid 都可以但不要把 ShardingSphere 和连接池的配置搞混它们各管一层。3.3 核心 YAML 配置分库分表规则的完整拆解ShardingSphere 5.x 的配置可以用 YAML也可以用 Java API还可以把配置放到 nacos 这类配置中心。对大多数项目YAML 是最直观的。下面是我常用的最小可运行配置spring: shardingsphere: datasource: names: db0, db1 db0: 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?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 db1: 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?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 rules: sharding: tables: t_order: actualDataNodes: db${0..1}.t_order_${0..3} databaseStrategy: standard: shardingColumn: user_id shardingAlgorithmName: db_mod tableStrategy: standard: shardingColumn: user_id shardingAlgorithmName: table_mod keyGenerateStrategy: column: id keyGeneratorName: snowflake shardingAlgorithms: db_mod: type: MOD props: sharding-count: 2 table_mod: type: MOD props: sharding-count: 4 keyGenerators: snowflake: type: SNOWFLAKE props: worker-id: 1 props: sql-show: true这里面有几个非常容易踩坑的地方actualDataNodes: db${0..1}.t_order_${0..3}是 ShardingSphere 的表达式语法${0..1}表示 0 到 1 的整数序列10 分片就是${0..9}不要写成${0-9}。分库的算法是user_id % 2分表算法是user_id % 4。由于库数 2、表数 4同一个user_id会先路由到库再路由到表组合之后是 8 个物理分片。keyGenerateStrategy不是必须的但建议配置。如果让 ShardingSphere 生成雪花 ID 作为主键你业务代码里的id就不用手动赋值。配置完启动应用后可以把sql-show打开你会看到每个 SQL 的路由结果。这一步价值很大——如果路由结果不符合预期先别怀疑代码去看路由日志。3.4 写一个标准的插入和查询示例验证路由是否正确先定义一个简单的 Mapper。这里不做 ORM 框架对比直接用 MyBatis-Plus 风格的写法来演示Mapper public interface OrderMapper { void insert(Order order); Order selectByUserIdAndId(Param(userId) Long userId, Param(id) Long id); }对应的 XMLinsert idinsert INSERT INTO t_order (id, order_no, user_id, amount, status, create_time) VALUES (#{id}, #{orderNo}, #{userId}, #{amount}, #{status}, #{createTime}) /insert select idselectByUserIdAndId resultTypecom.example.Order SELECT id, order_no, user_id, amount, status, create_time FROM t_order WHERE id #{id} AND user_id #{userId} /select这里面的核心要点是查询条件必须带分片键user_id。如果 SQL 里没有分片键ShardingSphere 会做全库全表路由广播查询对 8 个分片同时发起 SQL然后在内存里合并结果。这种查询在小数据量下不算问题一旦单表行数很大广播查询就是性能灾难。带分片键的查询比如WHERE user_id 10086 AND id 100ShardingSphere 计算出user_id % 2 0定位到 db0再计算user_id % 4 2定位到t_order_2所以最终只执行一次 SQL。这也直接回答了很多人问的“分库分表之后慢查询怎么办”——尽量让绝大多数查询都命中分片键。4. 分库分表后的四大难题从分布式 ID 到扩容迁移分库分表的门槛不在“分”而在“分完之后的日常运维”。4.1 分布式 ID 生成为什么不能再用自增主键单库单表时代AUTO_INCREMENT是最省心的主键生成方式。一旦分表多个分片各自生成自增 ID很大概率会出现重复主键。即便你通过设置不同表的自增起始值比如 table0 从 1 开始table1 从 1000001 开始将来扩容时还是会撞车。常用的替代方案有四种雪花算法SnowflakeShardingSphere 内置支持64 位 Long 型由时间戳、工作机器 ID、序列号组成。优点是生成速度快、趋势递增、无中心化依赖缺点是强依赖系统时钟如果服务器时钟回拨可能出现 ID 重复。ShardingSphere 官方对时钟回拨有容忍参数但生产环境务必配置 NTP 并监控时钟漂移。Redis INCR/INCRBY用 Redis 自增生成分布式 ID实现简单但 Redis 本身成为新的单点且每生成一个 ID 都多一次网络调用。可以用批量获取的方式优化。数据库号段模式建一张id_generator表每次取一个号段比如每次取 1000 个 ID缓存到内存。性能好ID 趋势递增但没有雪花算法那么“无所不在”。UUID唯一性没问题但作为主键会导致 B 树索引页分裂严重性能很差不推荐用作数据库主键。我个人的选择如果是 Java 技术栈且数据量中等直接用 ShardingSphere 内置的雪花算法如果对 ID 的“单调趋势递增”有强要求比如用于冷热归档判断建议自研或引入号段模式。4.2 分布式事务XA、BASE、还是最终一致性这是分库分表之后最容易引发“架构回退”的老大难。大部分简单的跨分片写操作可以通过 ShardingSphere 的分布式事务能力处理ShardingSphere 支持 LOCAL、XA、BASESeata 集成三种事务模式。XA 事务Atomikos/Narayana具备强一致性但加锁范围大性能代价高适合并发量低但一致性要求极高的场景。Seata 的 AT 模式属于最终一致性方案性能比 XA 好适合绝大多数交易类业务但需要额外部署 Seata Server。最容易被忽视的其实是“把事务边界压缩在单分片内”。如果业务能按用户维度聚合操作比如“一次操作只涉及同一个 user_id 的订单数据”就可以确保 SQL 路由到同一个分片从而继续使用本地事务。以普通电商交易为例创建订单和扣库存往往是两个服务、两个数据库。这类场景靠分布式事务来硬撑不如把扣库存设计成异步消息 本地事务表利用最终一致性保证。当你发现“所有操作都难以避免跨分片事务”时就要反问自己的拆分维度是不是选错了。4.3 跨分片 JOIN 和分页排序不用怕但要有方法先说 JOIN。分库分表之后JOIN 的代价极高因为两张表如果分片键不一致数据可能分散在不同库中中间的 JOIN 就需要跨库拉取数据到内存中完成这是典型的“不可控操作”。实践上一般有三种应对冗余字段在订单表里冗余商品名、价格等字段避免关联商品表。宽表设计通过消息队列异步构建聚合宽表让查询直接落到单表。应用层聚合先用第一个查询拿到结果集再逐个查询关联数据最后在应用内存里组装。再说分页。LIMIT 100000, 10这种常规写法在分库分表下会产生一个陷阱每个分片都会先查 100000 条数据然后 ShardingSphere 在内存里取总量前 100010 条再丢弃前 100000 条。这意味着深分页时每个分片都要传输大结果集性能损耗成倍放大。正确的思路是用“上一页最后一条记录的某个字段”做条件游标分页比如WHERE id ? ORDER BY id LIMIT 10每个分片只需要返回大于 ID 的最小 10 条。如果业务必须支持任意跳页则把排序字段和查询字段都落到同一个索引中尽量让 ShardingSphere 的归并器处理的过程简单一些。但归根结底B2C 后台管理系统这类浅分页场景问题不大用户体验页深分页基本无解只能给“搜索条件收窄”的限制。4.4 扩容与数据迁移从一开始就要想好的事水平扩容是分库分表里最痛苦的一件事。假设一开始是 2 库 4 表业务涨到需要 4 库 8 表那么已经写入的数据不可能原地不动必须做数据迁移。常见迁移方案有停机迁移在低峰期停服用离线工具如 DataX将老分片的数据按新规则路由到新分片上。数据量大时迁移时间长停机窗口不可控。双写 追平在业务层把新的读写按新旧两套规则同时执行成功写入新库后再把历史数据做批量追平。这种方式不要求停机但对业务的侵入性大。采用“分片数预分裂”的取模策略比如一开始库表总数为 16只部署 4 个分片其他 12 个分片预留但先不创建。扩容时只需把老数据按新规则搬到预置的分片即可。这种方法限制了前期成本但给后期留了余地。从架构终极解法看如果业务增长极快且无法预估可以考虑不依赖分片数的路由策略比如一致性哈希它允许最小化迁移数据量。但如前所述一致性哈希在数据均匀性上不如取模且 SQL 路由规则复杂度更高需要工程能力更强的团队来驾驭。5. 生产环境里的性能调优与踩坑记录真实排障经验这一章我写一些平时很难从官方文档直接看到的体会都是一些内部复盘下来的教训。5.1 连接数、连接池与 SQL 路由开销分库分表之后连接数不是乘以 1而是乘以分片数。假设应用连接池设置 maxPoolSize 为 50那么同一时间点理论上可能向 2 个库发起 100 条连接。连接池参数的评估必须把分片数算进去否则很容易出现“应用这边连接池没满但数据库 max_connections 被打满”的情况。另一个容易忽略的点是ShardingSphere-JDBC 是嵌入在应用进程里的路由和归并运算都会消耗 JVM 堆内存。尤其是大型报表类的 SQL如果路由到多个分片再在内存里排序堆内存压力极大。所以重 SQL 应该尽量避免过 ShardingSphere要么把报表需求挪到 ClickHouse 之类的大数据组件要么在业务侧做定制化聚合。5.2 表结构变更和索引调整别指望一条 ALTER TABLE单表时代新增一个索引就是一条ALTER TABLE ADD INDEX的事而且还能用ONLINE选项降低锁表时间。分库分表之后同样的变更需要同时在多个分片库上执行。我的建议是把所有的 DDL 变更脚本做成模板利用脚本循环在每个分片执行。要注意的是执行 DDL 时一些分片可能处于高负载状态建议用 gh-ost 或 pt-online-schema-change 这类工具逐台执行避免对在线业务造成明显影响。还有一个细节要注意ShardingSphere 的配置信息里如果有字段映射的调整需要同步检查配置中心里的逻辑表定义不然可能出现 SQL 和实际表结构不一致的报错。5.3 常见报错与排障思路从日志中快速定位分片问题我整理了几个高频问题基本覆盖了大部分人第一次接入 ShardingSphere 时会遇到的坑现象可能原因排查方向SQL 路由报错Cannot find datasource in sharding rule配置里的数据源名称和actualDataNodes表达式不一致检查${0..1}是否对应names列表里的 db0、db1插入数据成功但查询查不到分片键前后类型不一致例如一个是 String一个是 Long检查 Java 字段类型和 SQL 参数类型统一分片键类型分页查询结果缺失或重复多个分片数据合并后排序不稳定确保 ORDER BY 字段唯一或者在排序字段后追加主键批量插入报错批量 SQL 里各条数据的分片目标不同将批量插入按分片键分组重写或使用insert ... values拆批事务不生效跨分片操作仍在使用 Local 事务模式需要显式开启 XA 或 Seata BASE 事务另外日志里的Actual SQL: db0 ::: select * from t_order_0 ...这类内容极其有用不要只盯着异常栈先看 ShardingSphere 打出的 Actual SQL 是否路由到了预想的分片。如果路由错误问题和配置相关如果路由正确但执行慢再往 MySQL 执行计划方向查。5.4 三个容易忽视的配置细节下划线 vs 驼峰命名ShardingSphere 对 SQL 解析是严格按字段名匹配的如果你的实体类字段用的是驼峰SQL 里写的是下划线务必确保 MyBatis 的map-underscore-to-camel-case配置生效否则分片键提取不到会直接发起全路由。默认props里的sql-show在生产环境要关掉因为每条 SQL 都会打印路由信息高并发下日志量非常可怕。ShardingSphere 5.x 默认使用logic database概念如果项目里用了多数据源框架如 dynamic-datasource两套机制会冲突需要先想清楚哪一层负责路由。写在最后一个经历多次分库分表迁移后的体会分库分表技术本身并不难理解真正的成本在于它对整个研发流程的影响SQL 书写规范、索引规范、事务设计、上线发布、数据订正、备份恢复所有环节都要跟着改变。我见过太多团队把 ShardingSphere 当做一个“连接池增强插件”来引入结果上线后才发现所有离线统计任务都在做广播查询ETL 任务直接把生产库打挂。我个人这几年带队做分库分表的习惯是先做容量评估再定分片键最后才选中间件。如果可能我会把分片键的候选字段都列出来逐个用线上真实数据跑分布均匀度测试只保留那个数据分布最均匀、业务查询覆盖度最高的字段。至于 ShardingSphere它在规则表达、联邦查询、分布式事务接入等能力上确实很成熟5.x 的性能也比 4.x 有明显提升是目前 Java 技术栈里分库分表方案的首选。如果你还处在“要不要分库分表”的犹豫期我多说一句能不拆就不拆能晚拆就晚拆但一旦决定拆就按生产标准把数据迁移、监控、回滚方案一起做进去。分库分表不是银弹但做好了它能替你挡住未来一到两年的数据增长压力。