☰
从单表瓶颈到分库分表:ShardingSphere分片设计与配置实战
2026/10/20 14:56:09 网站建设 项目流程

生产环境里订单表到了几千万行,接口开始动不动慢查询,备份时间越来越长,这时候大多数团队都会想到分库分表。我自己维护的一个电商模拟项目,就是从订单表量变到质变被拖垮,最后选型落在了 ShardingSphere 上。这篇文章把分库分表的思考过程、核心概念和落地配置全部拆开讲,尽量让准备做数据库扩容的同学少走弯路。

1. 为什么要用ShardingSphere做分库分表

1.1 单表撑不住的真实场景

先说一个非常典型的场景:订单表日增十几万行,一年下来就是几千万行,三年之后稳稳破亿。

单表数据量到了一定规模,MySQL 的 B+ 树层级会变高,即使索引建得没问题,随机 IO 也会明显增加。你会发现有些统计 SQL 跑一次要几十秒,备份一个表要半小时,主从同步延迟越来越难控制。更让人头疼的是,某些开发同学喜欢在查询条件里不加分片键,这类 SQL 一旦上线,整个数据库连接池都会被拖住。

这不是 MySQL 本身不行,而是单机容量有天花板。分库分表的本质是把数据按照某个规则分散到多个数据库、多张物理表里,让每个节点承担一部分读写压力。

1.2 分库分表主流方案与选型对比

分库分表实现方式大致分三类:应用层硬编码、代理层中间件、嵌入式中间件。

方案优点缺点
应用层自己拆分不引入额外组件,路由逻辑可控代码侵入严重,扩容和改规则成本高
代理层中间件(独立部署)对业务几乎透明,支持跨语言需要维护独立集群,多一跳网络开销
嵌入式中间件(与业务同进程)性能好,配置灵活,无额外部署只适合Java应用,规则升级需要跟着应用上线

自己做硬编码听起来简单,但真正做起来很坑。你需要在 DAO 层写各种switch和if,查询条件稍微变一下就得改代码,尤其是后来要加读写分离、数据加密、影子库这些能力时,自己写的代码基本撑不住。

代理层独立部署的好处是业务方不用改代码,但多一层代理意味着多一次网络转发,链路变长。而且代理层的并发能力和缓冲池配置也需要额外调优,小团队未必扛得住。

1.3 为什么我最终选了ShardingSphere

选 ShardingSphere 的原因很直接:它有 JDBC 和 Proxy 两种形态。JDBC 模式直接嵌在应用里,性能损耗小,Spring Boot 项目接入非常顺手;Proxy 模式独立部署,给非 Java 团队或者其他中间件使用。这样我可以先用 JDBC 模式跑通核心业务,将来团队规模大了再上 Proxy。

ShardingSphere 不只能做分库分表,还把分布式主键、读写分离、数据加密、影子库、分布式事务这些都纳入了同一套配置体系。也就是说,原来你得接好几个中间件才能完成的事情,现在一个框架可以统一管理。它支持的数据库类型也多,MySQL、PostgreSQL、openGauss 等都在覆盖范围内,切换成本相对低。

当然也要说实话,ShardingSphere 的配置学习曲线并不平缓,版本之间的配置格式有差异,网上很多老教程还停留在 4.x,直接抄到 5.x 会报错。动手之前一定要先看清当前版本的文档格式。

2. 动手前的核心概念与分片设计

2.1 逻辑表、物理表、分片键:先懂这几个词

分库分表的第一步不是配中间件,而是把概念理清楚。逻辑表是你在 SQL 里写的那个表名,比如t_order。物理表是真正落在数据库里的表,比如t_order_0、t_order_1。ShardingSphere 的职责就是把对t_order的操作路由到对应物理表上。

分片键是路由的依据,也是整个分片设计里最重要的字段。SELECT * FROM t_order WHERE user_id = 1024能不能高效执行,完全取决于user_id是不是分片键,以及分片算法能不能准确落到少数物理表上。

分片键的选择标准其实就三条:分布足够均匀、业务上高频查询、取值几乎不变。用订单表举例,user_id通常是最好的分片键,因为大部分查询都是按用户找订单。如果按order_id分片,虽然订单号本身均匀,但业务侧不知道订单号对应哪个用户时就没法快速定位分片,只能全路由。

2.2 分片算法怎么选:取模、哈希、范围

ShardingSphere 提供多种内置分片算法,最常用的有取模、哈希取模和范围分片。

算法特点适用场景
MOD简单直接,按字段值取模分片键本身已经比较均匀
HASH_MOD先做哈希再取模,打散连续值user_id、手机号这类连续但可能扎堆的字段
RANGE按区间划分,比如按时间或ID段经常做范围查询,但容易产生热点

取模算法最简单,但有个问题:如果你的分片键是自增的订单号,老数据集中在低段,新数据段需要经过哈希才能均匀,否则会出现局部热点。

哈希取模算法会先把分片键做一次哈希,再对分片数量取模,能明显改善分布均匀性。代价是同样的字段值经过两次计算,路由多一点点开销,不过这点开销通常可以忽略。

范围分片很适合时间维度,比如按月份拆表,但缺点是数据量会随时间偏移,老表可能不活跃,新表压力很大。所以选算法之前,先想清楚业务访问模式,再说性能。

2.3 容量规划:分几个库几张表

很多人第一次分库分表容易犯“拍脑袋”的毛病。我建议用一道公式估算:目标数据量除以单表可承受行数,得到最少物理表数。

假设未来三年订单量大概 1.2 亿行,单表希望压在 500 万行左右,那至少需要 24 张表。为了给突发增长留空间,我一般按 32 张表规划。如果每个库放 8 张表,就是 4 个库。这样每个库大约承载 3000 万行,压力可控。

再有,库不是越多越好。每个库会占用连接数、文件句柄,备份和监控也要多一份成本。初期库太多,团队运维根本忙不过来。我个人更建议先从 4 库 8 表或者 2 库 8 表开始,等业务量真的上来了再做扩容动作。

分片函数必须提前设计好。如果你用user_id分片,库路由可以做成user_id % 4,表路由做成(user_id / 4) % 8,这样用户会均匀落在 4 个库的 8 张表里。如果库和表都用同一个%2,虽然能跑通,但会有一半物理表长期空着,容量规划要从一开始就避开这种浪费。

2.4 广播表和绑定表的设计

分库分表后,很多原来简单的表关系会变得复杂。像省市区、订单状态这些字典数据,量很小却到处要关联查询。这时候不要分片,直接让每个库都保存一份完整数据,也就是广播表。ShardingSphere 里配置广播表后,写操作会同步到所有库,读操作只查本地库,省去跨库 JOIN。

绑定表则是解决分片表之间的 JOIN 问题。t_order和t_order_item都按user_id分片,如果两张表的绑定关系没有声明,中间件就只能拆成笛卡尔积去查,性能很难看。声明绑定表后,ShardingSphere 会强制它们保持相同分片键和分片策略,JOIN 时只访问同一个分片,效率能提升很多。

一句话总结:像订单主表和明细表这种天生一起查的,要设计成绑定表;像配置表、字典表这种全局共享的,要设计成广播表。

3. 环境搭建与基础配置实战

3.1 引入依赖:ShardingSphere-JDBC 直接嵌进应用

我用的是 Spring Boot 项目,直接引入 ShardingSphere 的 JDBC Starter。版本最好和你的 Spring Boot 版本对齐,建议先去官方文档确认兼容矩阵。

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

接入后,你的业务代码不需要改 SQL,数据源会由 ShardingSphere 统一管理。需要注意,它接管的是数据源,原来配置在spring.datasource下面的连接信息要全部挪到spring.shardingsphere.datasource下面。

3.2 最小分片配置:把订单表按 user_id 拆开

先准备两个数据库ds0、ds1,每库里建两张表t_order_0、t_order_1。DDL 可以这样写:

CREATE TABLE t_order_0 ( order_id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, amount DECIMAL(12,2) NOT NULL, status VARCHAR(20), create_time DATETIME, KEY idx_user_id (user_id) );

然后配置 ShardingSphere。下面这个配置是最小可用版本,故意把取模都写成%2,方便理解路由逻辑:

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: 123456 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: 123456 rules: sharding: tables: t_order: actual-data-nodes: ds$->{0..1}.t_order_$->{0..1} database-strategy: standard: sharding-column: user_id sharding-algorithm-name: db_inline table-strategy: standard: sharding-column: user_id sharding-algorithm-name: table_inline key-generate-strategy: column: order_id key-generator-name: snowflake sharding-algorithms: db_inline: type: INLINE props: algorithm-expression: ds$->{user_id % 2} table_inline: type: INLINE props: algorithm-expression: t_order_$->{user_id % 2} key-generators: snowflake: type: SNOWFLAKE props: worker-id: 1 props: sql-show: true

提示:这个最小配置能跑通,但user_id % 2会让一半物理表空置。比如偶数用户只落在ds0.t_order_0,奇数用户只落在ds1.t_order_1。生产环境建议用自定义均匀算法,或者把表达式改成user_id % 4这类逻辑,让每个节点都承载数据。

配置里actual-data-nodes用的是$->{0..1},表示枚举范围。algorithm-expression则是根据字段值动态计算。两套语法不要搞混,否则启动阶段就会报解析错误。

3.3 写入先过主库:读写分离配置

分库分表通常会和读写分离一起上。ShardingSphere 的读写分离配置是独立规则,需要把数据源拆成写库和读库两组。

rules: readwrite-splitting: >@Transactional public void createOrder(Order order) { orderMapper.insert(order); }

对应 SQL:

INSERT INTO t_order (user_id, amount, status, create_time) VALUES (#{userId}, #{amount}, #{status}, #{createTime});

ShardingSphere 会在执行前把生成的order_id填充进去。规模小的场景用默认雪花配置就够了,唯一要注意的是多个应用实例的worker-id不能重复,否则高并发下可能出现重复 ID。

4. 核心操作:分片查询与写入的底层逻辑

4.1 一条带分片键的SQL是怎么执行的

ShardingSphere 对一条 SQL 的处理分五步:解析、路由、改写、执行、归并。

比如执行:

SELECT * FROM t_order WHERE user_id = 1024;

先把 SQL 解析成语法树,识别出逻辑表t_order和分片键条件user_id = 1024。路由阶段根据user_id % 2判定应该走ds0还是ds1,同时算出表名。改写阶段把逻辑表名换成物理表名。最终真正发到 MySQL 的 SQL 可能是:

SELECT * FROM t_order_0 WHERE user_id = 1024;

执行就很简单了,查完结果返回给客户端,不用做归并,因为只打了一个节点。

如果你往t_order里插入数据,步骤也类似。ShardingSphere 根据分片键定位物理表,自动完成路由和改写,应用层完全无感知。

4.2 不带分片键的查询为什么会全路由

最怕的是下面这类 SQL:

SELECT * FROM t_order WHERE status = 'PAID' ORDER BY create_time DESC LIMIT 20;

条件里没有user_id,ShardingSphere 不知道数据在哪张表,只能广播到所有分片节点执行。这在数据量小的时候问题不大,但是分了几十个节点后,所有库表都要扫一遍,慢查询几乎是必然的。

排查问题时,我习惯先看执行计划里有没有actual SQL。如果一条查询最后真实执行的 SQL 数量远超预期,就说明它触发了全路由。线上系统一定要限制这类查询,最好让分片键条件成为必填项,或者把查询场景提前改造成常用字段冗余。

4.3 绑定表和广播表的实战作用

绑定表和广播表不仅要在设计阶段想清楚,配置落地也很关键。绑定表规则写在分片规则下一级:

rules: sharding: binding-tables: - t_order,t_order_item broadcast-tables: - t_dict_region

这里我故意用binding-tables把t_order和t_order_item绑定起来,并且两张表都用同一个分片键user_id。这样 JOIN 查询时,中间件知道它们的数据在同一个物理节点,不会产生笛卡尔积。

广播表则是把t_dict_region在每个库里都放一份。ShardingSphere 执行插入、更新和删除时会对所有广播表广播写入,查询时随便查哪一份都行。用广播表时千万注意不能把大表设成广播表,否则每个写操作都要同步到所有库,代价非常大。

4.4 排序、分组、分页的归并代价

分片之后再写ORDER BY和LIMIT,不能直接按单库思维去优化。比如:

SELECT * FROM t_order WHERE user_id = 1024 ORDER BY create_time DESC LIMIT 10000, 20;

MySQL 单库里可以高效跳过 10000 行,但分片下每个节点都要先查足够多的数据,再交回 ShardingSphere 统一排序,最后取第 10000 行之后的 20 条。偏移量越大,内存消耗越夸张,性能崩得很快。

深分页场景建议改成游标分页或者 keyset 分页:

SELECT * FROM t_order WHERE user_id = 1024 AND create_time < #{lastCreateTime} ORDER BY create_time DESC LIMIT 20;

让下一次查询以上一次的create_time作为边界,而不是跳过一个很大的偏移量。这个方法在分库分表环境下比传统LIMIT offset稳定很多。

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

5.1 分片键选错了:一个典型教训

我见过一个模拟项目,最初为了图省事用order_id做分片键。理由是订单号自增且分布均匀,听起来没问题。但业务侧所有核心查询都按user_id查用户的订单列表,结果就是每条查询都触发全路由,数据库一压测就崩溃。

后来把分片键改成user_id,同时保留order_id作为普通索引,查询性能立刻恢复。如果你已经在线上踩了分片键选错的坑,也不要慌,尽早评估迁移方案,拖得越久改造成本越高。

5.2 跨库事务:别把分布式事务当本地事务用

分片之后如果一次业务操作要更新两个库的数据,原来的本地事务就失效了。比如扣除用户余额和创建订单分布在两个库,用@Transactional并不能保证跨库原子性。

ShardingSphere 有 XA 和 BASE 两种分布式事务方案。XA 强一致,但锁时长、性能开销都比较大;BASE 最终一致,适合对时效要求不高的场景。我更推荐用本地消息表或者事务消息来做最终一致,尽量从设计上避免跨库事务。

把跨库写操作拆成多个独立步骤,同库操作放一个事务,跨库操作用异步消息对齐状态。这样至少不会为了强一致把整个系统的吞吐量拖垮。

5.3 扩容与数据迁移:不可能真正无缝

取模分片的扩容比加表要麻烦得多,因为老数据的路由规则已经写进物理表了。扩容不只是加几个节点,还要把已有数据按新规则重新分布。

我建议的迁移顺序是:先准备新集群,然后做全量数据同步,接着开增量同步追平 binlog,确认数据一致后再切换路由规则,最后灰度放流量。整个过程必须有回滚预案,而且一定要在切换前做数据校验。

如果业务允许,也可以采用“双写”方案:应用写入新老两套数据库,等老库完全不再需要时再下线。双写会增加业务代码复杂度,不是首选。

5.4 用SQL日志快速定位路由结果

调试分片规则最实用的就是打开sql-show。在配置的props里加上:

props: sql-show: true

执行一条 SQL 后,控制台会打印逻辑 SQL 和真实 SQL。比如逻辑 SQL 是查t_order,真实 SQL 会显示成t_order_0,并且标注路由到了哪个数据源。看到这里,你就知道分片规则是否如预期了。

如果某个 SQL 刷出一长串不同物理表的真实 SQL,说明它是全路由,需要尽快优化。这个技巧排查问题非常直观,我每次调分片规则都会先打开它,确认无误后再关掉。

5.5 常见问题速查表

现象可能原因解决建议
各分片数据量差距大分片键本身偏斜,或算法选错换 HASH_MOD,或检查分片键是否均匀
不带分片键的查询很慢触发全库表路由强制在查询中带分片键,或用冗余表/搜索引擎
刚写入的数据查不到主从延迟导致读从库事务内读主库,或改造读接口接受延迟
分布式主键重复worker-id 冲突或时钟回拨检查每个实例的 worker-id 配置,同步服务器时间
扩容后部分数据查不到旧路由规则和新规则不兼容迁移后做数据校验,灰度切换规则
JOIN 查询非常慢绑定表未配置,产生笛卡尔积相同分片键的表配置 binding-tables

最后说一句大实话。分库分表这套东西,能晚做就晚做,但真要上线千万级数据之前,必须把分片键、容量规划、查询场景这几点讨论清楚。我见过太多团队以为接上中间件就万事大吉,最后被全路由、分布式事务和扩容问题轮番折腾。真到动手那天,先把最小配置跑通,再开 SQL 日志把核心查询都核对一遍,最后才谈性能和监控。如果让我重新做一次,我会先花两周时间把业务查询场景全部梳理出来,再决定表的数量和分片算法,而不是一上来就改代码。

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

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

立即咨询