第一次接触ShardingProxy的人,通常是被一句话吸引过来的:它能把你拆散在多个数据库节点上的表,重新拼成一个逻辑上的“普通数据库”,业务方不用改一行SQL就能继续按单库的习惯去读写。做数据库中间件这行久了会发现,很多团队从单库单表演进到分库分表后,卡住的往往不是拆分规则本身,而是“怎么让一百多个应用实例无缝切换到新架构”。ShardingProxy正好是解决这个接入问题的关键一环。这篇文章我从头讲透它的原理、配置、实战部署和排坑经验,全部基于真实环境踩过的坑总结出来,适合正在做分库分表架构选型的开发、DBA,以及被拆分后的查询路由搞得头疼的同学参考。
1. 先搞清楚ShardingProxy到底解决了什么问题
1.1 从单库单表到分库分表:问题从哪来
先聊一个最常见的演进路径。系统刚开始上线时一张订单表就够用,后来业务量涨了,单表千万级、上亿级,索引深了,写并发高了,数据库的CPU和IO开始报警。于是DBA把订单表按时间或者按订单号拆成多张表,再往后单库也扛不住,干脆把不同的表散到多个数据库实例上。这个动作做完,存储压力确实缓解了,但应用端的麻烦才刚刚开始:原来一条INSERT语句现在要自己算该去哪张表,原来一个JOIN要跨多个库手工拼数据,原来一个事务现在要面对跨库事务的难题。
如果这些逻辑全部写死在业务代码里,那每接入一个需求都要重新改一遍SQL拼接逻辑,而且不同服务的分片规则不一致之后,团队维护成本会迅速失控。市面上解决这个问题的方式无非三种:第一种是在应用层封装数据访问层,把分片路由做在代码里;第二种是引入数据库中间件,让SQL先经过中间件再路由到后端节点;第三种是把整个数据库直接换成一个原生分布式数据库。ShardingProxy属于第二种,也是我这些年见过在兼容性、改造成本和稳定性之间平衡得比较好的方案。
1.2 ShardingProxy与ShardingSphere生态的关系
很多人会把ShardingSphere和ShardingProxy搞混。简单说,ShardingSphere是一套完整的数据库生态,下面两条核心产品线:一条是ShardingJDBC,做成轻量级Java客户端,以jar包形式嵌入应用;另一条就是ShardingProxy,做成独立部署的中间件服务,对客户端而言它就是一个标准数据库服务端。
两者的定位差异很直观。ShardingJDBC像是给每个应用请了一位贴身管家,它跟着应用进程走,路由规则直接内置在JVM里,性能损耗低,但每个应用都要引入对应的依赖、配置分片规则。ShardingProxy则像是楼下的前台总机,你只管把SQL交给它,它负责转给背后各个数据库节点,业务侧完全感知不到分库分表的存在。
这种差异决定了它们的适用场景完全不同。新写一个微服务,明确规定从零开始,我倾向于ShardingJDBC,因为侵入可控、链路短;但如果要接的是存量系统,几十个服务每个都改JDBC连接方式成本太高,或者对接的是异构语言(比如PHP、Python、Go),没办法直接复用Java端的ShardingJDBC,那ShardingProxy几乎是唯一现实的选择。我实际遇到过几个项目,数据库表都拆好了,但业务团队有Java有Go还有写存储过程的,最后统一收口到ShardingProxy,所有客户端只改一个连接地址,问题立刻变简单。
1.3 为什么选Proxy而不是JDBC模式
每次做技术选型,团队里都会有人问:既然ShardingJDBC性能更好,为什么还要引入一个单独的中间层?这个问题我一般从三个维度来回答。
从运维视角看,ShardingJDBC的分片规则散落在每个应用进程里,规则要调整时必须发版重启,几十上百个实例同时变更是个灾难;ShardingProxy把规则集中在服务端,DBA改配置文件后重启单点即可,控制力强得多。从团队协作看,分片规则本质上是一种“数据基础设施约定”,放在Proxy层由DBA或基础架构组统一维护,比指望每个业务开发都理解取模算法要可靠。从生态兼容看,Proxy透明地实现了MySQL和PostgreSQL网络协议,意味着任何能用这两种协议连接数据库的客户端工具、监控系统、BI软件都能直接复用。
当然Proxy也有代价。多一跳网络就意味着额外的几毫秒延迟,加上中间件的CPU和内存开销,对延迟极端敏感的纯交易场景需要压测确认。我的真实感受是:如果你做的是内部系统、报表平台、ToB应用,或者是存量系统改造,Proxy的优势远大于这点性能损耗;如果每个接口对单次查询延迟都有硬性要求,且团队全是Java且愿意接受强侵入改造,再认真考虑ShardingJDBC。没有银弹,适合的才是对的。
2. 部署前的关键概念:逻辑库、物理库、绑定表与分片算法
2.1 逻辑库、逻辑表与物理节点
刚接触ShardingProxy时最容易糊涂的,就是配置里那一堆“逻辑”和“物理”的概念。我用最简单的话解释:逻辑库是客户端看到的库,逻辑表是客户端看到的表,物理库和物理表才是真正存在数据库里的东西。
比如你在Proxy里定义一个逻辑库叫sharding_db,客户端执行USE sharding_db,然后查询SELECT * FROM t_order WHERE order_id = 1001。Proxy拿到这条SQL后,先根据order_id的分片算法算出这条数据在物理表t_order_1上,再根据库分片规则算出它在物理库ds0上,于是真实执行的是SELECT * FROM ds0.t_order_1 WHERE order_id = 1001。整个过程对客户端完全透明。
这个“逻辑和物理分离”的设计,价值在于它解耦了业务代码和存储拓扑。今天订单表拆成4张,明天拆成8张,只需要改Proxy的配置,业务SQL里的表名永远叫t_order。我在项目里经常开玩笑说,分片规则是DBA和中间件之间的秘密,业务开发不该也不需要知道。配置里的actualDataNodes就是用来告诉Proxy“这实这些物理表散落在哪里”的字段,格式是ds0.t_order_0, ds0.t_order_1...,也可以用行表达式简写成ds$->{0..1}.t_order_$->{0..3}。
2.2 分片策略:标准、复合与行表达式的实际使用
分片策略是整个配置的重头戏,它决定了SQL会被路由到哪几张物理表。常见的类型有三种:标准分片、复合分片和HINT分片。
标准分片是使用最多的场景,它针对一个分片键做路由,比如order_id对4取模或者按月份分表。配置时通过shardingColumn指定分片键,在shardingAlgorithms里定义算法类型,INLINE是最常用的行表达式算法,比如t_order_$->{order_id % 4}表达的就是按订单号对4取模落到4张表中。复合分片适合需要用两个字段一起决定路由的场景,比如先按用户ID分库,再按订单时间分表,这时候需要实现一个自定义的复合算法类。HINT分片则是在SQL里完全不带分片键时强制指定走哪个节点,适合一些特殊的管理查询。
选算法时我的建议是:能用取模就用取模,模数按未来两年数据增长预估再留出一倍余量。按时间分片适合归档类数据,比如按月分表然后定期把历史表迁移到冷存储;按用户维度分片适合C端应用,因为绝大部分查询都是围绕某个用户展开的。记住一个要点:分片键的选择本质上是和业务查询模式强绑定的,不是DBA随便拍脑袋定的,最忌讳的就是选了一个业务上根本不作为查询条件的字段,结果每条SQL都变成全路由广播。
2.3 绑定表与广播表:被低估的两个配置
分库分表后,跨节点JOIN是最让人头疼的问题。比如t_order和t_order_item都按order_id分片,如果分片规则完全一致,那么应该把两个表配置成绑定表。绑定表的意思是:两个表在路由时会强制使用相同的分片路由结果,t_order查了ds0.t_order_1,t_order_item也只会去ds0.t_order_item_1查,JOIN变成了单库内的普通JOIN,不需要中间件做内存聚合。
广播表则适合那些数据量很小、几乎每个分片节点都需要用到的表,比如字典表、配置表、省份城市表。这类表在每个物理库里都存一份全量数据,Proxy在写入时会自动广播到所有节点,查询时任选一个节点即可。配置方式是在rules里用!BROADCAST声明两张表,之后应用完全感知不到它的存在。
这两个配置看起来不起眼,但在实际并发场景里作用极大。我看到过太多团队没配绑定表,结果一次本来很简单的联表查询被拆成几十次子查询,中间件内存被打爆,然后怀疑Proxy性能不行的案例。归根结底,中间件不是万能加速器,它只是在SQL路由上帮你做了正确的事,数据分布设计不合理,再好的中间件也救不回来。
3. 实战部署:从下载到跑通第一条分片查询
3.1 环境准备与安装包选择
说再多原理,不如亲手跑一次。这里我用一个模拟订单场景来做实战演示:两个物理库ds0和ds1,每个库里有4张订单表t_order_0到t_order_3,通过Proxy对外暴露成一个逻辑库sharding_db。
操作系统准备一台Linux(我这边测试用的是CentOS 7,其他主流发行版没区别),提前装好JDK 8+。因为ShardingProxy本身是JVM进程,所有数据库驱动已经内置,不需要额外装客户端。从ShardingSphere官网下载页找最新稳定版二进制包,如果本地没有就直接用内网镜像,包名大致是apache-shardingsphere-xxx-bin.tar.gz。这里提供一个选版本的原则:不追求最新,选已经发布超过半年的稳定版本,社区的issue和坑基本都被踩平了,文档也最齐。
解压后目录结构很清晰:bin目录是启停脚本,conf目录放所有配置,lib目录是各种依赖jar包。整个中间件的设计思路是“配置驱动”,你要改的一切都在conf下完成。我第一次部署时最不适应的一点是:它没有一个交互式控制台让你敲命令建库建表,一切都要通过YAML配置来声明,改完重启进程生效。理解了这个模式之后,反而觉得它适合自动化运维,配完直接走配置管理流程发布即可。
3.2 配置server.yaml:账号权限与全局参数
conf/server.yaml是Proxy自身的基础配置,相当于这个“虚拟数据库”的全局总控。最核心的是用户认证信息,这里决定了客户端用哪个账号密码能连上来。
rules: - !AUTHORITY users: - root@%:root - sharding@%:sharding provider: type: ALL_PERMISSIONS props: max-connections-size-per-query: 1 sql-show: true check-table-metadata-enabled: falseroot@%:root的意思是:用户名root,允许任意主机连接,密码是root。%表示来源IP不限,生产环境建议改成具体网段。sql-show: true会把每条经过Proxy的SQL打印到日志里,排障时非常好用,但生产环境建议关掉,否则日志量会非常可观。max-connections-size-per-query表示一次查询最多允许在多少物理库上同时执行,1表示每个查询路由到单库执行,如果路由到多库则串行;这个参数直接关系到并发和连接占用,后面性能章节再展开讲。
需要注意一个容易踩的坑:如果下面想配合读写分离,还需要在server.yaml里配置!TRANSACTION类型的全局事务规则;如果不想用分布式事务,保持默认即可。启动前可以先用bin/start.bat这个脚本在Windows环境做快速验证,线上还是用bin/start.sh。
3.3 配置config-sharding.yaml:定义数据源和分片规则
重头戏在conf/config-sharding.yaml,这里要定义逻辑库、物理数据源、分片规则三大部分。先看数据源配置:
databaseName: sharding_db dataSources: ds0: url: jdbc:mysql://127.0.0.1:3306/demo_ds0?serverTimezone=UTC&useSSL=false username: root password: root ds1: url: jdbc:mysql://127.0.0.1:3307/demo_ds1?serverTimezone=UTC&useSSL=false username: root password: root注意两个细节。一是连接串里的serverTimezone=UTC必填,否则连接MySQL 8.x的时候会报时区错误;二是useSSL=false,内网环境没必要做SSL握手,加了这个参数能省掉一层握手耗时。模拟环境里我会把ds0和ds1配置成两个真正的MySQL实例,如果只有一台机器,可以用两个不同端口模拟。
接着是分片规则,这是整个配置的灵魂:
rules: - !SHARDING tables: t_order: actualDataNodes: ds$->{0..1}.t_order_$->{0..3} tableStrategy: standard: shardingColumn: order_id shardingAlgorithmName: t_order_inline keyGenerateStrategy: column: order_id keyGeneratorName: snowflake shardingAlgorithms: t_order_inline: type: INLINE props: algorithm-expression: t_order_$->{order_id % 4} ds_inline: type: INLINE props: algorithm-expression: ds$->{user_id % 2} defaultDatabaseStrategy: standard: shardingColumn: user_id shardingAlgorithmName: ds_inline这里解释一下每一行的作用。actualDataNodes声明了t_order这个逻辑表对应的物理表位置,ds$->{0..1}表示ds0和ds1两个库,t_order_$->{0..3}表示每个库里的4张物理表,一共8张表。tableStrategy配置表级别的分片策略,按order_id对4取模决定数据落在哪张物理表。defaultDatabaseStrategy配置库级别的分片策略,按user_id对2取模决定数据落在哪个物理库。
有个设计细节值得留意:库分片键我选了user_id而不是order_id。因为订单场景里绝大部分查询都是“查某个用户的订单”,用user_id做库分片能让同一用户的所有订单都落在同一个物理库,后续查用户订单列表时只需要路由到一个库,不需要跨库聚合。这是从业务查询模式反推分片键的典型案例,远比“随便找个字段取模”要可靠。
3.4 启动、建表、验证查询
配置写完后,先确认两个物理库里已经建好对应的物理表。ShardingProxy不会帮你自动建表,需要你提前在ds0和ds1中分别执行建表SQL,把所有t_order_0到t_order_3都建出来。很多新手在这里栽跟头:Proxy配置的是8张实际表,结果物理库里只有一张t_order,一查就报“表不存在”。
启动进程:
cd /opt/shardingsphere/bin ./start.sh tail -f ../logs/stdout.log看到类似ShardingSphere-Proxy memory mode started successfully的日志说明启动成功。默认监听3307端口,这个设计是故意的,就是为了避开MySQL默认的3306端口,防止连错。如果用防火墙记得放行3307。然后用任意MySQL客户端验证:
mysql -h127.0.0.1 -P3307 -uroot -proot进入后执行USE sharding_db,再执行一条插入和一条查询。如果开启了sql-show,你会在日志里看到Proxy自动改写后的SQL,比如INSERT INTO t_order ...被改写成对ds0下的t_order_1执行,这就是透明路由生效的证据。
我第一次跑通时最深的感受是:从外部看,它就像一个安装完就能用的MySQL,完全不需要业务方理解后台的复杂度。这种“低门槛接入”才是Proxy产品设计的核心价值。
4. 读写分离与分布式事务:让Proxy更贴近生产
4.1 读写分离配置与主从延迟问题
真实的业务系统基本不会只用一台数据库,主从架构是标配。ShardingProxy在同一个配置体系里支持读写分离。在config-readwrite-splitting.yaml里定义逻辑数据源,把写库和读库的关系告诉Proxy:
databaseName: readwrite_splitting_db dataSources: write_ds: url: jdbc:mysql://127.0.0.1:3306/write_db username: root password: root read_ds_0: url: jdbc:mysql://127.0.0.1:3307/read_db username: root password: root rules: - !READWRITE_SPLITTING dataSources: readwrite_ds: writeDataSourceName: write_ds readDataSourceNames: - read_ds_0 loadBalancerName: round_robin loadBalancers: round_robin: type: ROUND_ROBIN使用的时候,这个逻辑数据源readwrite_ds本身也可以继续参与分片配置,也就是说读写分离和分库分表可以叠加使用。生产环境中常见的问题是“主从延迟”:写完主库立刻去查,数据还没同步到从库,读到旧数据。解决方案通常是在Proxy上层加一个强制路由提示,或者业务层对刚写完的数据做特殊处理。ShardingProxy可以通过@Slave选主库强制走写库执行,具体实现依赖SQL解析器的注释透传能力。我的建议是:对一致性要求极高的场景,不要依赖读写分离,走写库最稳妥;报表、列表这类读多写少的业务才适合把只读流量打到从库。
4.2 分布式事务类型与取舍
分库分表之后,跨库事务是个绕不开的坎。原来在单库里的一条事务如果涉及到两个物理库,普通的数据库事务已经无能为力了。ShardingProxy提供两种方案:XA事务和Seata柔性事务。
XA是强一致的典型代表,它的思路是事务管理器统一协调所有分片节点,先预提交再统一提交,保证要么全成功要么全失败。配置方式在server.yaml里声明事务类型为XA即可。XA的优点是对业务透明,接入成本低,缺点是并发性能损耗比较大,因为事务期间所有节点都要持有锁等待协调者决定最终结果。
Seata柔性事务则走的是最终一致路线,业务上先做本地事务,通过undo_log记录反向操作,遇到异常时反向补偿回滚。Seata方案并发能力更强,但需要额外部署Seata服务端,同时业务代码中需要引入相应的注解配合。
我个人的选型经验是:金融交易、账户类、库存类这些强一致场景,上XA,宁可性能差点也不能出错;积分、日志、通知这类可以容忍短暂不一致的场景,上Seata,吞吐量高得多。最理想的情况是:尽量从业务设计上避免跨库事务,比如把强关联的表按照相同分片键放在同一物理库,事务就退化为单库事务,这是最高效的“分布式事务优化方案”。
4.3 多分片键与自定义算法扩展
标准取模算法覆盖了大多数场景,但总有些业务需要更复杂的路由逻辑。比如订单系统一天的数据量差异非常大,大促期间可能是平时的几十倍,单纯取模会导致每个分片数据不均衡。这时候可以自定义分片算法。
ShardingProxy的扩展机制基于SPI,要做的是写一个Java类实现对应的分片算法接口,打成jar包放到lib目录,然后重启Proxy。比如我可以实现一个结合“日期+用户ID末位”的复合分片算法,每天的流量先按日分表,再按用户取模散到不同库,既能应对大促扩容单日表,又能让单日内的数据分布均匀。
这里要重点提醒:自定义算法一旦上线,后续想改路由逻辑会极其痛苦,因为老数据已经按照旧规则分布了。所以自定义算法在正式启用前,务必做充分的容量估算和回放测试。任何分片算法上线后都做“只读增加、不轻易修改”的原则。
5. 性能调优与连接管理心得
5.1 连接数与并发参数:来自一场压测的教训
之前帮一个团队做过一次压测,场景是模拟1000个并发用户同时查订单。上线前测试环境一切正常,一上生产就频繁报Too many connections。查下来才发现问题出在Proxy和后端数据库的连接数量匹配上。
ShardingProxy本身面向每一个客户端连接,它还会根据SQL路由结果向后端数据库发起连接。假如前端有100个连接,每一条SQL同时路由到4个分片,那么后端瞬间要建立400个连接。如果不限制max-connections-size-per-query,后端数据库的连接池直接被击穿。
我建议压测前把max-connections-size-per-query设成1,让每个查询路由到多个分片时串行执行,这样后端连接数始终被控制在合理范围。当然这会牺牲一些查询速度,所以这个参数应该在理解了“并发查询数×单查询节点数”的关系后,根据后端数据库能承受的连接总量来折中设置。后端MySQL的max_connections也要同步调大,同时Proxy的JVM堆内存建议至少给4-8G,具体取决于路由聚合时的数据量。
5.2 SQL解析开销与日志陷阱
很多人问Proxy会不会成为性能瓶颈,答案是:它确实有开销,但比大多数人想象的小得多。SQL解析、路由计算、结果合并这些操作都是在JVM内存中完成的,单核处理几万条简单SQL没什么问题。真正的性能杀手有两个:一个是大结果集跨分片合并,另一个是日志。
跨分片聚合排序这种SQL,比如LIMIT + ORDER BY,Proxy需要把每个分片查到的数据全部拉到中间件内存,排序后再返回前端。如果单页数据量太大,内存会直接飙高。我遇到过一个案例,一张表分片后没做任何优化,前端做分页查询,每页100条,但Proxy内存持续不释放,原因是Proxy为了排序要把所有符合条件的数据都加载进来。解决办法是:业务SQL加上明确的分片键条件,让路由收敛到单分片;或者用NoSQL思路做宽表冗余,避免跨分片聚合。
日志问题说起来很基础但很多人会中招:sql-show: true在生产环境跑了一周,发现日志文件几个GB,磁盘满,系统卡死。教训就是:这个参数只适合联调和测试环境,生产必须关掉,实在要监控可以通过接入Prometheus和JMX监控指标来做。
5.3 减少不必要的路由广播
可能遇到的另一个性能问题,是SQL里明明带了分片键但Proxy还是做了全路由。有一次排查慢查询发现,SQL里的分片键被包在函数里了,比如WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2024-01-01',虽然create_time是分片键,但经过函数处理后,Proxy无法提取分片键值,只能广播到所有分片。
这不是Proxy的bug,而是SQL规范化带来的天然限制。想让路由精准命中分片,SQL里要“裸用”分片键,中间不要套函数、不要做类型转换。这是我多年实战排障中反复验证过的规律,属于典型的“中间件替你干活,但你得遵守它的规则”。
6. 常见问题与排查技巧实录
6.1 启动失败类:端口、配置、驱动
端口被占用是最常见的启动失败原因。ShardingProxy默认3307端口,如果被其他进程占了,启动日志会报Address already in use。检查端口占用后,要么杀掉旧进程,要么修改server.yaml里的port配置。
配置语法错误比端口问题更隐蔽。YAML文件对缩进极其敏感,尤其是rules节点下的- !SHARDING这样的标签,少一个空格都会导致启动失败。我习惯在本地用支持YAML校验的编辑器先格式化一遍再上传,能省下大量排错时间。
MySQL驱动不兼容是另一个高频问题。如果连的是MySQL 8.x,Proxy内置的驱动版本必须匹配,否则会出现Unable to load authentication plugin 'caching_sha2_password'之类的报错。解决办法是把用户认证方式改成mysql_native_password,或者升级Proxy到新版。PostgreSQL场景一般没这个问题,驱动内置得比较完善。
6.2 查询报错类:逻辑库不存在、表不存在
启动成功后客户端连不上,最常见的原因是连接时没带逻辑库名。如果连接串写的是jdbc:mysql://127.0.0.1:3307/而不是/sharding_db,Proxy不知道你要用哪个逻辑库,直接报错。这和原生MySQL不太一样,因为Proxy可以同时挂载多个逻辑库,必须明确指定。
查询时报“表不存在”,先确认物理库里是否真的建了对应的物理表。Proxy只负责路由,不负责帮你建表。我见过不止一次:配置里写了t_order_$->{0..3},但物理库只建了t_order_0一张表,业务一查就炸。建议写一个初始化脚本,把所有分片表一次性建好,顺便做一次遍历校验。
6.3 独家避坑经验:配置生命周期与灰度切换
最后分享一个我特别想强调的经验:ShardingProxy的配置变更,至少在默认内存模式下,是需要重启进程才生效的。重启意味着所有在线连接会断开,对生产业务来说这是不可接受的。所以我的建议是把Proxy纳入配置管理平台,变更时间窗选在业务低峰,同时准备好回滚版本。
如果项目对可用性要求极高,可以考虑ShardingSphere后续版本里支持的集群模式,把配置放到注册中心统一管理,配合管理端做动态生效。不过引入这个方案本身也增加了部署复杂度,建议在资源允许的前提下再上。
另外,接入Proxy之前,务必先在测试环境做一轮全量SQL兼容性测试,重点覆盖子查询、多表JOIN、批量INSERT、分页排序这四类SQL。它们是最容易触发路由异常和结果合并bug的场景。把这个测试做扎实,上线后至少能躲掉80%的坑。
6.4 我常用的排障路径
遇到问题我会按照“连接层→路由层→执行层→合并层”的顺序来定位。连接层看端口、账号、防火墙;路由层开sql-show看改写后的SQL是否落在预期分片;执行层去物理库手动执行改写后的SQL,确认物理库本身没问题;合并层看返回结果集是否和预期一致。这套路径帮我解决过几十个疑难问题,比乱猜高效得多。
写到最后想补充的一句话
ShardingProxy不是那种用完就能立刻感受到“快”的组件,它的价值是让分库分表后的系统依然像一个普通数据库一样可被消费。我在多个项目里体会最深的一点是:中间件只是工具,真正决定架构成败的,是你对业务查询模式的理解,以及是否提前规划好分片键、绑定表、广播表和读写分离的组合策略。先把这些基础打牢,再上手Proxy,你会发现很多问题不需要靠文档,看一眼配置就能想明白原因。