1. 为什么我要认真啃一遍 ShardingSphere
最早接触分库分表,是在一个订单量刚过千万级的系统里。当时单表数据量逼近两千万行,查询开始出现肉眼可见的延迟,索引维护成本也越来越高。团队第一反应是“加索引、加缓存”,但治标不治本,写入瓶颈和单库连接数上限摆在那里。后来决定上分片方案,选型时对比了几种思路,最终落到ShardingSphere上。原因很直接:它不绑定具体数据库,支持 MySQL、PostgreSQL、Oracle 等多种后端,而且提供 JDBC 和 Proxy 两种接入形态,对已有代码的侵入性可控。
ShardingSphere 本质上是一套分布式数据库中间件生态,核心能力包括数据分片、读写分离、分布式事务、数据加密、影子库压测等。它解决的问题很明确:当单库单表成为瓶颈时,如何在不改业务 SQL 的前提下,把数据水平拆到多个库表,同时保持对应用层的透明。适合谁看?我认为有三类人值得花时间:一是正在做分库分表选型、纠结用中间件还是手写路由的开发者;二是已经用了 ShardingSphere 但只停留在“能跑”层面、想搞懂内部机制的工程师;三是需要评估分片方案对现有系统影响范围的技术负责人。
这篇文章不会只讲概念,我会把配置、参数计算、踩坑记录都摊开说。你如果跟着走一遍,至少能搞清楚:分片键怎么选、分片算法怎么配、分布式事务怎么取舍、以及那些文档里不会写的“实际跑起来才知道”的细节。
2. 核心概念拆解与方案选型逻辑
2.1 分片核心概念:逻辑表、真实表、分片键
ShardingSphere 的分片模型里有几个必须分清的术语,否则配置时很容易懵。
逻辑表是 SQL 里写的那个表名,比如t_order。真实表是物理库里实际存在的表,比如t_order_0、t_order_1。分片键是决定数据落到哪张表的字段,通常选业务查询里最常用的过滤条件,比如user_id或order_id。分片算法则是一段逻辑,输入分片键的值,输出目标库和表的编号。
我见过不少人一上来就把order_id当分片键,结果按用户维度查订单时全库扫描。这就是分片键选错的典型后果。分片键的选择直接决定了后续查询是“精准路由”还是“全路由”,性能差距可能是几十倍。
2.2 为什么选 ShardingSphere 而不是手写路由
手写分片路由不是不行,但成本在于:你要自己维护路由规则、处理跨库 JOIN、实现分布式主键、还要保证事务一致性。这些工作重复且容易出错。ShardingSphere 把这些能力封装成可配置的模块,你只需要声明规则,剩下的交给它。
另一个关键点是对 SQL 的解析能力。ShardingSphere 会解析 SQL 语法树,改写 SQL 中的表名,再路由到目标库执行,最后归并结果。这套流程如果自己写,光是 SQL 解析就够喝一壶。当然,代价是引入了一层中间件,有学习成本和一定的性能开销,这个后面会细说。
2.3 JDBC 与 Proxy 两种形态怎么选
ShardingSphere 提供两种接入方式,选型时我建议按团队情况来定。
| 对比维度 | ShardingSphere-JDBC | ShardingSphere-Proxy |
|---|---|---|
| 接入方式 | 作为 jar 包嵌入应用 | 独立部署,应用连代理 |
| 语言支持 | 仅 Java | 任意支持 MySQL 协议的语言 |
| 性能开销 | 较低,无网络中转 | 多一跳网络 |
| 运维成本 | 随应用发布 | 独立运维,可统一管理 |
| 适用场景 | Java 单体/微服务 | 多语言混合、DBA 统一管控 |
我的经验是:纯 Java 技术栈优先 JDBC,性能更好、排查方便;如果团队里有 Python、Go 服务也要接同一套分片规则,Proxy 更省事。两者可以混用,但初期不建议同时上,复杂度会翻倍。
3. 分片配置实操:从零搭一个可跑的分片方案
3.1 环境准备与依赖引入
先明确版本。ShardingSphere 5.x 和 4.x 的配置差异很大,5.x 引入了新的 YAML 配置体系和 DistSQL,我建议直接用 5.x。以 JDBC 形态为例,Maven 依赖大致如下:
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core</artifactId> <version>5.4.0</version> </dependency>数据库驱动按实际后端引入,比如 MySQL 用mysql-connector-j。注意版本兼容性,ShardingSphere 5.4 对 MySQL 8.x 支持较好,如果后端是 5.7,建议先在小环境验证。
3.2 分片规则配置:以订单表为例
假设我们要把t_order按user_id分成 2 个库、每个库 2 张表,共 4 个分片。配置核心是rules下的sharding部分:
rules: - !SHARDING tables: t_order: actualDataNodes: ds_${0..1}.t_order_${0..1} tableStrategy: standard: shardingColumn: user_id shardingAlgorithmName: order_inline databaseStrategy: standard: shardingColumn: user_id shardingAlgorithmName: db_inline shardingAlgorithms: order_inline: type: INLINE props: algorithm-expression: t_order_${user_id % 2} db_inline: type: INLINE props: algorithm-expression: ds_${user_id % 2}这里actualDataNodes用表达式描述了所有真实节点,INLINE算法用 Groovy 表达式做取模。user_id % 2决定库和表,这样同一个用户的订单会落在同一个库的同一张表,按用户查询时能精准路由。
3.3 分片键与算法选择的参数计算
分片数量不是拍脑袋定的。我的计算逻辑是:预估三年数据量 ÷ 单表舒适容量 = 分片数。单表舒适容量通常取 500 万到 1000 万行,超过后索引深度增加、DDL 变慢。假设订单表三年预计 8000 万行,按单表 800 万算,需要 10 个分片,取 2 的幂次方便取模,就是 8 或 16 个分片。
分片键的选择要满足两个条件:一是高基数,值分布均匀,避免数据倾斜;二是高频查询条件,保证大部分查询能带上分片键。订单场景里user_id通常比order_id更适合,因为用户查自己的订单是高频操作。
注意:如果业务里既有按用户查、又有按订单号查,可以考虑基因法或冗余分片键,把
user_id的后几位编码进order_id,这样按订单号也能路由到正确分片。这个技巧后面会展开。
4. 分布式主键、事务与读写分离的落地细节
4.1 分布式主键生成方案对比
分片后自增主键会冲突,必须换方案。ShardingSphere 内置了SNOWFLAKE和UUID两种。雪花算法生成 64 位长整型,趋势递增,适合做数据库主键;UUID 无序,作为主键会导致索引页分裂,一般不推荐。
rules: - !SHARDING keyGenerators: snowflake_gen: type: SNOWFLAKE props: worker-id: 1worker-id在集群里必须唯一,否则可能生成重复 ID。如果机器数量多,建议用注册中心统一分配,或者用机器 IP 末位做区分。我踩过的坑是:本地开发时多个实例用了同一个worker-id,压测时出现主键冲突,排查了半天。
4.2 分布式事务的取舍
跨库写入必然涉及事务。ShardingSphere 支持LOCAL、XA、BASE三种事务类型。LOCAL 是本地事务,跨库时不保证一致性;XA 基于两阶段提交,强一致但性能损耗大;BASE 是柔性事务,最终一致,适合对一致性要求不极端的场景。
我的建议是:能避免跨库写就避免。分片键选得好,大部分写操作会落在单库,用 LOCAL 就够了。如果确实有跨库写,优先考虑业务补偿,而不是硬上 XA。XA 在高并发下的锁等待和协调开销,实测下来对吞吐影响明显。
4.3 读写分离配置要点
读写分离和分片可以叠加使用。配置上先定义数据源,再声明读写分离规则:
rules: - !READWRITE_SPLITTING dataSources: ds_0: writeDataSourceName: write_ds_0 readDataSourceNames: - read_ds_0_0 - read_ds_0_1 loadBalancerName: round_robin主从延迟是绕不开的问题。写后立即读,可能读到旧数据。解决办法有两种:一是强制走主库,用 Hint 指定;二是业务上容忍短暂延迟。我一般对“写后立刻要读”的场景加 Hint,其他查询走从库。
5. 常见问题排查与避坑经验实录
5.1 全路由与慢查询排查
分片后最怕全路由。判断方法很简单:看 SQL 有没有带分片键。如果WHERE里没有分片键,ShardingSphere 会把 SQL 广播到所有分片,性能急剧下降。排查时打开 SQL 日志,观察实际执行了几个分片。
props: sql-show: true开启后控制台会打印逻辑 SQL 和真实 SQL,能直观看到路由结果。我遇到过一次按create_time范围查订单,没带user_id,结果扫了全部 16 个分片,响应时间从 20ms 涨到 800ms。后来加了冗余分片键才解决。
5.2 跨库 JOIN 与分页问题
跨库 JOIN 是分片方案的天然短板。ShardingSphere 支持 Binding Table 和 Broadcast Table 来优化关联查询,但本质仍是内存归并。我的原则是:核心业务表尽量按同一分片键分片,让关联查询能下推到单库。如果做不到,就在应用层组装数据,别指望中间件。
分页也是坑。LIMIT 10 OFFSET 100在分片场景下,每个分片都要取 110 条再归并,偏移量越大越慢。优化思路是用游标分页,或者限制最大翻页深度。
5.3 常见问题速查表
| 问题现象 | 可能原因 | 解决方向 |
|---|---|---|
| 主键冲突 | worker-id 重复 | 检查集群内 worker-id 唯一性 |
| 查询变慢 | 全路由 | 检查 SQL 是否带分片键 |
| 事务不生效 | 跨库用了 LOCAL | 改用 XA 或业务补偿 |
| 读不到刚写的数据 | 主从延迟 | 写后读加 Hint 走主库 |
| 启动报错 | 配置表达式错误 | 检查 actualDataNodes 语法 |
5.4 独家避坑技巧
第一个技巧:分片数尽量用 2 的幂次。取模运算在 2 的幂次下可以用位运算,效率更高,而且后续扩容时可以用翻倍策略,减少数据迁移量。
第二个技巧:上线前做一次全量数据迁移演练。分片方案上线意味着历史数据要重新分布,迁移过程中的数据一致性、停机时间都要提前验证。我见过没做演练直接上,结果迁移脚本跑了 6 小时,业务停了半天。
第三个技巧:保留分片键的原始字段。有些团队为了省空间,把分片键哈希后存储,结果排查问题时无法还原原始值,定位困难。建议原始字段和哈希值都保留。
6. 我对 ShardingSphere 的实际使用体会
用了一段时间后,我的整体感受是:ShardingSphere 把分片这件事的“标准动作”都封装好了,配置得当的话,业务代码几乎无感。但它不是银弹,分片键选错、跨库查询多、事务边界不清,这些问题中间件解决不了,得在架构设计阶段就想清楚。
最后分享一个小技巧:如果你还在评估阶段,可以先不接真实数据库,用 H2 内存库跑一遍分片配置,验证路由逻辑是否正确。这样成本最低,也能快速熟悉配置语法。等规则验证通过,再切到真实环境,能省下不少调试时间。