
第一次接触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_1t_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?serverTimezoneUTCuseSSLfalse username: root password: root ds1: url: jdbc:mysql://127.0.0.1:3307/demo_ds1?serverTimezoneUTCuseSSLfalse username: root password: root注意两个细节。一是连接串里的serverTimezoneUTC必填否则连接MySQL 8.x的时候会报时区错误二是useSSLfalse内网环境没必要做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里定义逻辑数据源把写库和读库的关系告诉ProxydatabaseName: 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 BYProxy需要把每个分片查到的数据全部拉到中间件内存排序后再返回前端。如果单页数据量太大内存会直接飙高。我遇到过一个案例一张表分片后没做任何优化前端做分页查询每页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.xProxy内置的驱动版本必须匹配否则会出现Unable to load authentication plugin caching_sha2_password之类的报错。解决办法是把用户认证方式改成mysql_native_password或者升级Proxy到新版。PostgreSQL场景一般没这个问题驱动内置得比较完善。6.2 查询报错类逻辑库不存在、表不存在启动成功后客户端连不上最常见的原因是连接时没带逻辑库名。如果连接串写的是jdbc:mysql://127.0.0.1:3307/而不是/sharding_dbProxy不知道你要用哪个逻辑库直接报错。这和原生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你会发现很多问题不需要靠文档看一眼配置就能想明白原因。