最近在团队内部做了一次关于Sharding-Sphere的技术分享,整理材料的时候翻了不少源码和官方文档,也把这两年在生产环境里实际用到的分库分表方案重新捋了一遍。这次就把分享内容展开写成一篇完整的实战笔记,从核心原理讲到落地配置,再到我踩过的坑,一次性说清楚。
先说清楚这篇笔记适合谁看:你正在做订单、交易、用户这类增长很快的业务数据,单库单表已经出现性能瓶颈,或者你只是听说过Sharding-Sphere、准备做技术选型,想搞明白它到底能做什么、不能做什么。这篇文章不是官方文档的复述,而是基于实际项目经验的完整梳理,涉及原理的部分我会尽量讲透,涉及部署的部分会给可直接抄的配置。
1. 从单库瓶颈说起:为什么需要Sharding-Sphere
1.1 数据库瓶颈的几种真实形态
大部分业务系统初期都是单库单表,数据量在千万级以下时,MySQL配合主从复制基本能扛住。但业务继续增长,你会陆续遇到几类问题。
第一类是单表数据量过大。以电商订单表为例,日订单量50万,一年就是1.8亿,单表超过亿级之后,B+树索引深度增加,写入时的页分裂和索引维护成本明显上升,即使查询走了索引,也会因为聚簇索引过大导致缓冲池命中率下降。你会发现明明SQL很简单,但响应时间就是不稳定。
第二类是写入吞吐瓶颈。单库的写入能力受限于磁盘IO和binlog复制开销,即使做了读写分离,写的压力仍然集中在一个主库上。遇到大促峰值,主库的写入连接数会直接打满。
第三类是单库的连接数和资源隔离问题。一个MySQL实例默认连接数上限通常几百到一两千,一旦多个业务共用一个实例,一个慢查询就能拖垮整个实例的所有业务。
这些问题用缓存、消息队列可以缓解一部分,但数据最终还是要落在数据库里。缓存只能挡读流量,对写瓶颈无能为力。真正能治本的方案,还是分库分表。
1.2 分库分表要解决什么,又会引入什么
分库分表的核心思想就一句话:把数据按照某种规则分散到多个数据库实例、多张表中,让每个分片的数据量和写入压力降下来。
具体做法分为两种:垂直拆分和水平拆分。垂直拆分是把字段多的表按业务域拆成多张表,比如把订单基本信息与订单明细分开,本质上还是单库,解决的是行宽和字段利用率问题。水平拆分是把同一张表的数据按分片键散到多张表,这是应对数据量增长的主要手段。Sharding-Sphere说的分片,主要指水平拆分。
但分库分表不是免费的午餐,它引入的复杂性相当可观:
- 跨分片查询:比如按订单号分片,但你想按用户ID查订单列表,会变成全分片路由的查询,性能骤降。
- 分布式事务:原来一个本地事务就能搞定的多表更新,现在要跨多个库处理。
- 全局主键:数据库自增主键在各个分片内会重复,需要全局唯一的ID生成方案。
- 数据迁移与扩容:分片数定了之后,再想扩容就是大工程。
很多团队倒不是死在分片后的性能上,而是死在这些引入的复杂性没有系统化解决上。这正是Sharding-Sphere这类中间件存在的核心价值:把分片的复杂性封装起来,对业务代码尽量透明。
1.3 Sharding-Sphere的定位与选型理由
Sharding-Sphere是Apache基金会下的顶级开源项目,它不是一个单独的框架,而是一套生态。核心由三部分组成:Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar,其中前两个是生产环境最常用的。
- Sharding-JDBC:以jar包形式运行在业务应用内,作为JDBC驱动层的增强,业务代码里还是用JDBC接口,但实际执行的SQL会被解析、改写,路由到真实的分片库表。它适合Java应用,部署简单,性能开销小。
- Sharding-Proxy:独立部署的代理服务,实现MySQL/PostgreSQL协议,业务方把它当普通数据库连就行,适合多语言场景或不想侵入应用的情况。
- Sharding-Sidecar:服务网格形态的数据库代理,目前成熟度相对前两个低一些。
选型时我建议记住一个判断标准:如果你的应用是Java,且可以接受引入依赖,就优先选Sharding-JDBC,它的性能损失最小、功能最全;如果有非Java应用、或者DBA需要直接看到分片后的SQL和执行情况,再考虑Sharding-Proxy。两者也可以结合,但没必要一开始就上Proxy。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Sharding-Sphere核心原理:分片引擎到底做了什么
2.1 一次查询背后经历的五步处理
Sharding-Sphere的核心是一个完整的SQL处理引擎,一次查询从进入到返回,要经历五个阶段:解析、路由、改写、执行、归并。理解了这五步,你基本就理解了Sharding-Sphere的所有行为逻辑。
先看SQL解析。这一阶段做的事情和数据库本身一样,把字符串SQL解析成抽象语法树。Sharding-Sphere使用Antlr解析SQL,生成包含表名、列名、条件值、排序字段、聚合函数等结构化信息的AST。这里有个很多人没注意的细节:解析过程不连接数据库,只根据SQL文本本身和配置的路由规则判断如何执行,所以解析性能非常高,一个简单SQL的解析耗时通常在毫秒级以下。
接着是路由。路由拿到AST里的表名和分片键条件,结合配置的分片算法,决定SQL应该发往哪些真实的库和表。路由结果分为三种:单播路由、多播路由和全路由。
- 单播路由:分片键条件能精确命中一个分片,比如
WHERE order_id = 1001且order_id是分片键,Sharding-Sphere计算出唯一分片,只发一条SQL。 - 多播路由:分片键条件是IN列表或范围,命中多个分片,比如
WHERE order_id IN (1001, 1002, 1003)。 - 全路由:SQL里没有分片键条件,不知道去哪个分片,只能所有分片都查一遍。这是最需要避免的情况。
路由之后是改写。改写有几个目的:把逻辑表名替换成真实表名,比如t_order改写成t_order_0;补充分片键条件;处理聚合函数和排序。举例来说,你写的SQL是SELECT * FROM t_order WHERE order_id = 1001,路由结果指向t_order_0,改写后变成SELECT * FROM t_order_0 WHERE order_id = 1001。
执行阶段相对简单,就是把改写后的真实SQL通过连接池发送到各个分片执行。Sharding-Sphere支持多种数据库连接池,比如HikariCP、Druid,也支持配置每个分片的独立连接池参数。
最后是归并。这是很多人忽略但极其重要的一步。各个分片返回结果后,Sharding-Sphere需要把这些结果集合并成你想要的结果。归并分为几种类型:
- 遍历归并:最简单的列表拼接。
- 排序归并:每个分片返回有序结果,归并时做多路归并排序,不需要全量加载到内存。
- 分组归并:分片各自分组后,再做一次整体分组聚合。
- 聚合归并:SUM、COUNT这类函数,需要把各分片的结果二次聚合。这里有个典型的改动:
SELECT COUNT(*) FROM t_order路由到4个分片后,Sharding-Sphere会把SQL改写成SELECT COUNT(*) FROM t_order_0、SELECT COUNT(*) FROM t_order_1等,然后在归并阶段把4个count值相加。 - 分页归并:分页参数会被改写,以保证最终结果正确,具体后面在分页问题里细说。
2.2 为什么要理解原理,而不是直接用配置
我见过不少团队把Sharding-Sphere当黑盒用,配置好分片规则就开始写代码,结果数据倾斜了不知道原因、查询变慢了不知道去哪排查。理解原理最大的价值,是让你在性能出问题时能快速定位问题发生在哪个环节。
举个例子:一条SQL突然查得很慢,如果你不了解路由机制,可能只会去看数据库慢日志。但你一分析就会发现,SQL被改写了,实际执行的分片数量和你想的不一样,慢的根本原因在路由阶段就决定了——你不是命中了一个分片,而是全路由扫了16个分片。这时候你该改的是查询条件或分片键,而不是加索引。
再举个例子:很多人不理解为什么Sharding-Sphere的文档反复强调分片键要尽量出现在SQL的WHERE条件里。原理上就是路由阶段依赖分片键条件来决定路由结果,没有分片键条件就只能全路由。这个行为从1.x到5.x版本都没变过,是最底层的设计约束。
2.3 核心组件和版本演进的一点说明
Sharding-Sphere从4.x升级到5.x,变化不小。5.x引入了可插拔的SPI架构,分片算法、分布式ID生成器、读写分离负载均衡等全部可以通过SPI扩展。如果你看网上的教程,会发现很多还是4.x的配置写法,直接搬到5.x会报错。建议新项目直接用5.x,4.x已经进入维护期。
5.x版本里,分片配置从之前的sharding.rule.tables等简化成了新的rules配置结构,并且支持YAML、Spring Boot Starter、Java API三种配置方式。我后面给的配置示例统一使用5.x的格式。
3. 分片策略与配置实战
3.1 分片键怎么选:这是最重要也最容易拍脑袋定的事
分片键的选择直接决定分片的均匀性和查询效率。很多团队随手选了主键或者业务单号,后续查询需求一多就发现问题了。
选分片键的核心原则有两条:
第一条,高频查询条件优先。绝大多数读写请求都是按什么条件查,分片键就选什么字段。电商场景下,订单号的查询需求通常大于按用户ID查的,但用户查询订单列表也极其高频,所以很多团队会把订单表按用户ID分片,同时把订单号与用户ID的映射关系缓存起来,查询时先拿用户ID算分片再查。
第二条,分片键的值域要足够大且分布均匀。比如按用户ID取模,前提是用户ID本身是均匀分布的数字。按地区分片虽然也能用,但地区和地区之间的订单量差异可能非常大,容易造成数据倾斜。
这里给一个典型的反面教材:把订单号直接作为分片键,然后订单号前缀包含业务类型,比如A开头走A类业务、B开头走B类业务,取模后业务类型不同天然就散开了,但如果某类业务量暴增,这类数据就会集中到少数几个分片。
3.2 四种分片策略,怎么选怎么配
Sharding-Sphere 5.x提供了四种分片策略:inline、standard、complex、hint。我把它们分别说清楚。
inline策略是最简单的行表达式分片,适合分片键值直接参与计算就能确定分片的情况。配置里用${表达式}来写,比如按user_id取模4分库、再按order_id取模4分表,可以这样写:
yaml复制rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_$->{0..1}.t_order_$->{0..1}
databaseStrategy:
inline:
shardingColumn: user_id
algorithmExpression: ds_$->{user_id % 2}
tableStrategy:
inline:
shardingColumn: order_id
algorithmExpression: t_order_$->{order_id % 2}
注意inline只支持=和IN操作,不支持范围查询。如果你在SQL里写了WHERE order_id BETWEEN 100 AND 200且order_id是分片键,inline策略下Sharding-Sphere会报错,因为无法对BETWEEN做精确路由。
standard策略支持自定义分片算法,并且支持范围查询。它有两个接口要实现:PreciseShardingAlgorithm处理精确值分片,RangeShardingAlgorithm处理范围分片。如果范围查询不需要支持,让RangeShardingAlgorithm抛出UnsupportedOperationException即可。这个策略适合需要把多个字段综合计算分片、或者分片算法比较复杂的情况。
complex策略用于多个分片键共同决定分片的场景,实现ComplexKeysShardingAlgorithm接口即可,分片键列表可以在参数里拿到。比如按user_id和tenant_id共同取模,你就可以在这个接口里自己做逻辑。
hint策略是强制路由,分片键不来自SQL,而是由业务上下文指定。这在分片键没落在SQL条件里时特别有用。举个例子,你的表是按tenant_id分片的,但业务查询在SQL里没有带上tenant_id,而是从登录用户上下文取的,这时候就能用HintManager手动指定分片值:
java复制try (HintManager hintManager = HintManager.getInstance()) {
hintManager.addDatabaseShardingValue("t_order", 123);
// 执行SQL,会强制路由到tenant_id=123对应的库
}
强制路由是双刃剑,能用就不用,因为会破坏Sharding-Sphere对业务透明这个核心价值,但确实有大场景需要它。
3.3 一致性哈希分片:解决扩容问题的经典方案
分片键取模实现简单,但有一个致命问题:分片数量一旦固定,后续扩容就非常痛苦。原来分4片,数据到80%了想扩到8片,需要对存量数据做全量重分布,那段时间基本不敢做线上变更。
一致性哈希就是为了解决这个问题设计的。它把整个哈希值空间组织成一个环形结构,数据节点和数据都映射到环上,数据落在顺时针方向遇到的第一个节点上。这样新增节点时,只有该节点逆时针方向到上一个节点之间的数据需要迁移,其他数据不受影响。
Sharding-Sphere没有内置一致性哈希的默认实现,但你可以通过StandardShardingAlgorithm自定义接口实现一个。这里给一个简单的实现思路:先对分片键做MurmurHash,得到哈希值,然后映射到一致性哈希环上。需要注意的是,分片键的值域要足够大且分布均匀,否则一致性哈希也会出现节点间数据不均的问题。
我自己的建议是:如果你的系统大概率会面临扩容,就别用纯取模,一开始就上一致性哈希。虽然实现上多写几行代码,但以后扩容的时候你就知道这件事值得。
3.4 分布式ID生成:为什么不能直接用数据库自增
分库分表之后,自增主键在各分片内各自增长,必然出现重复,所以必须使用全局唯一ID。
Sharding-Sphere内置了三种分布式ID生成方案:UUID、SNOWFLAKE和自定义。UUID实现简单但ID过长、无序,对索引不友好,生产环境不建议用。雪花算法是默认推荐方案,生成的ID是一个64位的Long,包含时间戳、工作机器ID和序列号,趋势递增且全局唯一。
雪花算法的核心配置是worker-id,每台机器需要配置不同的值,否则可能生成冲突。生产环境可以接入注册中心(如Zookeeper、Nacos)自动分配worker-id,避免手工管理。基于Sharding-Sphere的配置如下:
yaml复制rules:
- !SHARDING
keyGenerators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 1
tables:
t_order:
keyGenerateStrategy:
column: order_id
keyGeneratorName: snowflake
需要注意雪花算法的时钟回拨问题:如果机器时钟回拨,可能生成重复ID。Sharding-Sphere在检测到时钟回拨时会等待或抛异常,默认策略是抛异常。如果你对可用性要求很高,需要自己测试一下时钟回拨场景下的行为是否符合预期。
4. 分布式事务:分库之后最棘手的问题之一
4.1 本地事务的边界被打破了
分库之前,一个事务涉及的更新操作都在同一个数据库实例里,用数据库的本地事务就能保证原子性。分库之后,一个业务操作可能同时更新订单库和用户库,任何一个库提交失败,都不能简单地回滚另一个库。这是分布式事务要解决的问题。
Sharding-Sphere对分布式事务的支持经历了几个阶段。4.x时主推的是XA两阶段提交,强一致但性能损耗大。5.x之后更推荐BASE柔性事务,并内置了对Seata的集成。
4.2 XA事务:强一致方案的使用场景
XA事务基于两阶段提交协议:先准备(prepare),再提交(commit)或回滚(rollback)。只要所有参与的分支都prepare成功,最后就能commit成功。这个协议能保证强一致性,但缺点是协调者成为单点,网络分区时可能阻塞,性能也比较差。
在Sharding-Sphere里启用XA事务很简单,只需要引入shardingsphere-transaction-xa-core包,然后使用@ShardingSphereTransactionType(TransactionType.XA)注解标识事务:
java复制@ShardingSphereTransactionType(TransactionType.XA)
@Transactional
public void createOrder(OrderDO order) {
orderMapper.insert(order);
userMapper.decreaseBalance(order.getUserId(), order.getAmount());
}
这里我的实际经验是:XA适合少量跨库操作、对一致性要求极高、并发量不高的场景,比如后台订单审核、账户资金扣减等。如果你在核心交易链路里依赖XA,要接受它在高并发下的延迟损耗。
4.3 BASE事务与Seata集成
BASE柔性事务的核心思想是最终一致性,允许中间状态不一致,通过补偿来达到最终一致。Seata是业内最主流的实现方案,支持AT、TCC、Saga等模式。
Sharding-Sphere集成Seata时,通常采用AT模式。AT模式的做法是:先记录分支事务操作前后的数据快照(undo log和redo log),本地提交业务数据,然后通过全局锁保证并发隔离,最后异步协调各分支事务的状态。AT模式对业务代码侵入极小,但需要额外的undo_log表。
集成方式不复杂,加上seata依赖并配置好Seata服务端,业务上只需要使用@GlobalTransactional注解:
java复制@GlobalTransactional
public void createOrderWithSeata(OrderDO order) {
orderMapper.insert(order);
userMapper.decreaseBalance(order.getUserId(), order.getAmount());
}
这里我有一个明显的对比建议:如果单笔操作涉及的分片数量少(2-3个)、对一致性延迟容忍度高,用BASE事务;如果操作涉及的分片数量多、且要求强一致,XA更可控,但对性能要有心理准备。实际项目里,我见过最稳妥的做法是尽量通过业务设计避免跨分片事务,比如把订单和用户信息放在同一个分片,或者通过消息队列做异步对账。技术方案是兜底,不是首选。
5. 实操落地:电商订单库拆分案例
5.1 需求分析与拆分规划
这个案例背景是一个电商项目,订单表t_order单表数据量已经到了8000万,每天新增约30万,高峰期写入QPS约2000。我们决定做分库分表,预期目标是支撑未来两年数据量增长到3亿。
第一步是确定分片键。我们分析查询特征是:按订单ID精确查询(用户查看订单详情)、按用户ID查询订单列表(我的订单页)、按商家ID查询订单列表(商家后台管理)。这三类查询里,用户ID才能同时覆盖C端查询和部分B端场景,所以最终选择user_id作为分片键。
第二步是确定分片数量。考虑到当前数据量和未来增长,我们规划10个库、每个库100张表,总共1000个分片。实际计算过程是:目标3亿数据,单表控制在300万以内,那么至少需要100张表;为了后续扩容余量,再考虑物理库分布,定了10库100表的方案。这个数量对路由计算和连接管理压力都不大。
具体配置如下(YAML格式,基于Sharding-Sphere 5.x):
yaml复制dataSources:
ds_0:
url: jdbc:mysql://192.168.1.10:3306/ds_0?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: xxx
connectionTimeoutMilliseconds: 30000
ds_1:
url: jdbc:mysql://192.168.1.11:3306/ds_1?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: xxx
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..9}.t_order_${0..99}
tableStrategy:
complex:
shardingColumns: user_id, order_id
algorithmClassName: com.example.OrderShardingAlgorithm
keyGenerateStrategy:
column: order_id
keyGeneratorName: snowflake
keyGenerators:
snowflake:
type: SNOWFLAKE
props:
worker-id: ${worker_id}
我这里用了complex策略,而不是inline取模。原因是请求里有时候只有user_id,有时候只有order_id,这两种情况下路由逻辑是不一样的。我需要自行实现一个分片算法:当SQL条件里有user_id就用user_id取模,有order_id就用order_id取模,两个都有时优先用user_id。这种灵活性是inline策略给不了的。
5.2 数据迁移与双写方案
上线分库分表不是把应用配置改了就完事,存量数据必须从原单表搬过来,同时要保证新数据不丢。
我的做法是三步走:历史数据迁移、增量数据同步、切换线上流量。
历史数据迁移用离线工具,写脚本按主键范围分批读取原表数据,计算分片后插入目标分片表。这里要特别注意两点:一是迁移期间应用可能还在写库,需要先做增量同步;二是订单的关联数据,比如订单明细表,也必须按同样的分片规则一起迁,否则后续关联查询会全路由。
增量数据同步用消息队列或binlog监听。我们把业务应用先接上Sharding-Sphere写新表,同时监听原单表的binlog,把所有新写入的数据按规则同步到分片表。这个过程需要持续跑一段时间,直到数据追平。
切换流量时,先切换读流量,观察一段时间确认数据和性能正常,再切换写流量。如果出问题,可以随时回滚到原单表。这个切换过程会涉及一些业务开关配置,但对用户无感。
5.3 上线后性能测试结果
上线后我做了压测,重点看三个指标:完整查询响应时间、写入吞吐量、连接池占用。
单分片精确查询(SELECT * FROM t_order WHERE user_id = ? AND order_id = ?)的响应时间从之前的平均80ms降到平均12ms,主要原因就是单表数据量从8000万降到300万以内,索引的深度和缓冲池命中率都大大改善。
写入吞吐提升也很明显。之前单库写入峰值极限在2000 QPS左右,分片后因为写入分布到了10个库,每个库的实际写入只有200 QPS,余量非常大。压测期间我们测试了单库单表的写入极限是8000 QPS,所以整体容量变成了8000乘以10,当然这是理论值,实际还会受到应用层、连接池、网络等限制,但至少不再是数据库单点瓶颈。
连接池占用方面,如果应用配置了每个分片一个连接池,连接数会随着分片数量成倍增加。我们的业务需要连接数不多,10个库的连接能接受。但如果分片数量上百,连接管理就要小心,建议用Proxy模式或做连接池复用。
6. 常见问题与排查技巧实录
6.1 分页查询的深坑:为什么第100页变得特别慢
分页查询在分库分表下不是一个简单的问题。LIMIT 100000, 20这种写法,Sharding-Sphere会改写为在多个分片都执行LIMIT 100000, 20,然后在归并层取出所有分片的结果再做内存排序和分页。你只是要看第100页的20条数据,但实际从每个分片都拉了100020条数据到内存,性能自然崩。
解决思路有三种。第一种是业务上限制深分页,比如只允许查看前100页,这在大部分C端场景都能接受,因为用户几乎不会翻到100页之后。第二种是使用游标分页,用上一次查询返回的最后一条记录的ID作为下一次查询的起点,比如WHERE order_id < ? ORDER BY order_id DESC LIMIT 20。这种方式因为每次都走索引,且能精确路由到单分片,性能和稳定性都非常好。
第三种是用搜索引擎或缓存层,把需要深分页的数据同步到ES或Redis中,这部分数据不直接走分片。对B端后台管理系统的复杂筛选查询,我比较推荐这个方法,因为后台查询条件多、分片键经常不出现,强行在Sharding-Sphere上做全路由查询性能太差。
6.2 跨分片Join:能避免就避免
分片之后,跨分片Join查询基本是不可用的,因为数据分散在不同的物理库中,中间件确实支持广播表绑定,但这会大大增加执行时间和内存开销。
我见过最惨的例子是后台报表系统按订单ID关联用户表查询,两个表分片规则不一样,一个按user_id分、一个按order_id分,Join的时候所有分片都要参与,查询要等最慢的那个分片返回,整体响应时间长达数秒。
解决方案还是要结合业务来。简单场景用冗余字段,比如订单表里直接冗余用户昵称和头像,下单时查一次用户信息写入,查询时就不用再关联用户表。复杂场景就上ES或宽表,把Join动作前置到数据同步阶段,查询阶段只做单表查询。
6.3 广播表和绑定表的处理
有一类数据量小、几乎所有业务都会用到的表,比如地区表、字典表,它们不需要分片。Sharding-Sphere里可以配置为广播表,实际数据在每个分片都复制一份,查询时选择任意分片的副本即可。这类表在配置里加上broadcastTables声明。
还有一类表是分片规则完全相同、且经常关联查询的表,比如订单表和订单明细表,都按user_id分片。这种叫绑定表,配置绑定关系后,两个表关联查询时只需要在同一个分片内Join,避免跨分片。
配置示例如下:
yaml复制rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..9}.t_order_${0..99}
t_order_item:
actualDataNodes: ds_${0..9}.t_order_item_${0..99}
bindingTables:
- t_order, t_order_item
broadcastTables:
- t_dict_region
这里有个隐含要求:绑定表的实际数据节点必须一致,如果订单表是100张、订单明细表只建了10张,绑定就会出问题。
我不止一次在线上看到数据倾斜,查下来是分片键的值分布有问题。比如有个系统按商家ID分片,但有几个大商家的订单量占全站60%,这几个商家的数据集中到了少数几个分片。这种问题Sharding-Sphere解决不了,根子上是分片键选错了。大商户账户要单独扩容或单独分片,不能和普通商家混用一个分片规则。
如果真的出现了数据倾斜,临时排查方法是在Sharding-Sphere控制台或者通过执行计划查看各分片的数据量和查询次数,定位哪些分片负载高。长期的方案是改造分片算法,用一致性哈希结合权重分片,权重按业务规模动态调整。这个方案实现复杂度高,但对数据倾斜严重的场景很有效。
6.4 连接池配置与性能调优经验
Sharding-JDBC模式下,每个分片数据源都会创建一个独立连接池。如果你的应用配置了10个分片,就有10个连接池,每个连接池默认10个连接,总共100个连接。连接数不是瓶颈,但如果你同时又是多应用部署,每台机器都持有100个连接,DBA那边连接数压力会很大。
建议在配置连接池参数时,按实际并发量设置,不要照抄默认值。一个很重要的技巧是开启连接池的空闲回收,避免长时间占用数据库连接:
yaml复制dataSources:
ds_0:
dataSourceClassName: com.zaxxer.hikari.HikariDataSource
poolName: ds_0
minimumIdle: 5
maximumPoolSize: 20
idleTimeout: 300000
另外一个调优点是SQL预处理。Sharding-Sphere每次执行SQL都要经过解析,虽然解析性能不差,但高并发下解析开销会被放大。建议代码里尽量使用PreparedStatement,让Sharding-Sphere和数据库都能对SQL做缓存。
6.5 配置不一致导致的路由错误
有一种很隐蔽的故障:线上多个应用实例的Sharding-Sphere配置不一致,导致同样的SQL在不同实例上路由到不同分片。比如A实例配置了100张分片表,B实例配置了50张,分片算法取模结果一致但范围不一致,可能出现数据写了A实例的62号表,但查询路由到B实例时找不到这个表。
这类故障排查起来很耗时。建议把配置作为一个独立配置中心管理,多个应用实例共享同一份配置,不要各自维护YAML文件。我强烈建议用Apollo或Nacos统一管理Sharding-Sphere配置,配置变更走审批和灰度,避免线上随意修改。
再说一个很多人会忽略的问题:schema变更。分库分表后,如果要给分片表加字段,必须所有分片都加上,而且要通过工具批量执行。漏了任何一个分片,业务查询就会报字段不存在。上线的自动化脚本里务必要遍历所有分片。
7. 一些个人经验和最后的建议
如果让我给刚接触Sharding-Sphere的团队提几条最实在的建议,大概是这些。
第一,不要一上来就分库分表。单表数据量在5000万以内、读写QPS在5000以内,优化SQL和索引、做好缓存、上读写分离,基本都能解决问题。分库分表是最后的手段,不是炫技的工具,它会引入运维和开发上的额外复杂度。
第二,分片方案一定要和业务查询特征绑定。选分片键之前,先把所有查询场景列出来,哪些按订单号查、哪些按用户查,哪些查列表、哪些查详情,整理完后你才知道分片键怎么选、哪些字段需要冗余、哪些查询要走ES。跳过这一步直接配置,后续返工成本极高。
第三,上线前一定要做全量SQL Review。把所有涉及分片表的SQL过一遍,尤其是后台报表类SQL,很多都是无分片键的全路由查询。对于无法避免的全路由SQL,要么加缓存,要么改造成独立查询服务。
第四,压测不能只测正常请求。要专门测全路由情况下的兜底性能,比如没有分片键的列表查询、深分页查询、跨分片聚合查询,这些场景一旦被用户触发,可能就是事故。提前压测,知道最坏的性能边界,才能在线上做限流和熔断。
我自己在几个项目里用Sharding-Sphere,整体感受是它的设计思路清晰、文档全面,但使用起来需要你对数据分布、查询模式和运维规范有清晰的认知。它不是买了就能用的工具,更像是一个需要认真对待的基础设施。把它真正用好的团队,通常都不是因为它多智能,而是把分片相关的每一条规范和边界都落实到了位。希望这篇分享能帮你少走一些弯路。
