
以前我在业务线做后台开发时最怕听到的一句话就是“这个表又要扩容了”。单表数据量过了千万级之后索引再优化也就是那点收益只能上分库分表而分库分表一上SQL怎么拆、事务怎么管、主库切换时应用怎么感知全成了新问题。兜兜转转一大圈团队最后把目光落在了分布式数据库代理上——把数据访问层的复杂度从业务代码里剥出来下沉到独立的一层去处理。这篇文章就围绕分布式数据库代理聊聊它在架构里的位置、核心的工作方式、选型对比以及我实际部署和维护过程中踩过的坑。如果你正准备从单库走向分布式或者已经在用代理层但总感觉哪里不对这篇应该能给你一些参考。好先说明白一点很多人一搜“分布式数据库代理”搜到的是nginx反向代理、Java动态代理、内网穿透代理这些东西。这里的“代理”指的是应用与后端数据库集群之间的一个中间层它会伪装成一个标准数据库实例比如MySQL协议接收应用发来的SQL再把SQL路由到背后真实的一个或多个数据库节点上执行。你可以把它理解为一个数据库网关前端统一入口后端动态寻址。1. 数据库代理到底在解决什么问题1.1 拓扑变了客户端不该跟着改先看一个很常见的演进路径。业务初期就是一个单体MySQL应用配置一个数据源就够。访问量上来后把主从复制搭起来主库写、从库读应用层就需要维护两套地址再往后单表数据涨到瓶颈做了分库分表应用不仅要维护多组地址还得自己判断哪条SQL该发往哪个分片。如果这个判断逻辑写在每个服务的DAO层里带来的问题非常直观每个服务都要重复实现一套路由规则版本迭代时改动面积大新人接手时学习成本高。分布式数据库代理的定位是把这层逻辑集中收编。它对外暴露一个逻辑数据源应用只需要配置一个地址背后连接多少个物理库、哪些表分布在哪个分片、读写怎么分离都不需要业务方感知。路由规则只在代理层维护一份改分片方案时不需要动业务代码。1.2 三类典型场景的价值我把实际中用到代理的场景归纳成三类方便分类讨论。读写分离场景。一个主库挂两三个只读从库最常见。代理层收到SQL后根据语句类型决定走主库还是从库。好处是主从切换时只要代理层的配置跟着调整应用完全无感知。否则主库宕机时你需要在应用配置中心手动切换数据源运气不好还要发版本运维压力很大。分库分表场景。单表数据量到了一定规模后代理层根据分片键把数据均匀分布到多个物理库表。比如订单表按user_id哈希分片同样的SQL会按不同用户路由到不同分片。这个场景最依赖代理的SQL解析和路由能力。异构访问场景。某些代理还支持把SQL转成不同数据库方言让应用透明访问不同类型的后端。不过这块实际用得少我通常不建议在这种场景下强上代理。1.3 代理不是银弹得先想清楚边界这里要泼一盆冷水。代理层能解决寻址和路由问题但解决不了所有分布式问题。跨分片事务仍然是难点分布式事务的代价比单机事务高一个数量级跨分片查询的归并也有性能损耗尤其翻页查询在深分页场景下会非常吃力代理层自己是单点需要做高可用部署。这些都是设计阶段就要接受的“代价清单”。2. 代理层的工作拆解一条SQL从进入代理到执行完这一节我们拆一个核心问题一条SQL进入代理后内部到底发生了什么。2.1 连接管理与会话保持代理首先要解决连接管理的问题。应用发起的每条查询本质上是在代理上建立一个前端连接代理再向后端数据库建立真正的连接来执行SQL。前后端连接池相互独立前端连接数量可以很多后端连接被池化复用这样就避免了应用直连数据库时“连接数膨胀”压垮数据库的问题。这里有个细节容易被忽略后端连接的复用必须考虑会话上下文。比如LAST_INSERT_ID()、USER VARIABLES这类会话级状态如果随机复用连接就可能读到别的会话的状态。成熟的代理会做会话级绑定或者把相关标记从SQL里解析剥离后单独处理。我见过有人用轻量级代理做读写分离上线后发现有诡异的“数据串号”现象查到最后就是因为后端连接池复用时没有隔离会话状态。2.2 SQL解析从文本到路由决策代理收到SQL文本后的第一步不是直接转发而是解析。这一步大体分三层词法分析把SQL文本拆成Token序列识别出关键字、表名、字段、常量。语法分析按照SQL语法规则生成抽象语法树AST让代理知道这条语句的结构。语义理解与路由结合分片配置识别分片键的位置和值计算目标分片。以一条简单SQL为例INSERT INTO t_order(order_id, user_id, amount) VALUES (10001, 9527, 99.00);这里定义了分片键是user_id代理会解析出它的值是9527再做哈希取模计算分片目标 hash(user_id) % 分片数量假设4个分片库、每个库两张表9527可能路由到ds_2库的t_order_1表。这里的分片策略不只哈希一种还有按时间范围分片、按枚举值分片、按取模范围混合分片等不同策略的适用范围差别很大后面第4节会展开。2.3 读写分离的判定逻辑读写分离的判定看起来是“SELECT走从库其他走主库”但实际规则比这复杂。关键点在于事务内的一致性一旦客户端开启了事务事务内的所有操作都必须走同一个数据源。如果你的事务里有“先写后读”的操作读却走了从库由于主从复制存在延迟可能会读到旧数据。更准确的说法是代理会维护一个连接上下文当前连接处于事务状态时读写都绑定到主库连接上非事务状态下SELECT可以走从库但也得注意事务隔离级别和复制延迟的容忍度。有些代理还支持配置“基于延迟的从库权重”比如从库延迟超过阈值时临时摘除避免读请求打到滞后的节点上。这个小设计在生产里非常实用。2.4 结果归并多分片查询的“二次加工”单条SQL如果路由到多个分片代理还要做结果集归并。比如SELECT user_id, SUM(amount) FROM t_order WHERE user_id IN (9527, 9528, 9529) GROUP BY user_id;这条SQL会路由到多个分片每个分片各自执行分组聚合返回部分结果。代理需要把多份结果合并后再做一次分组和汇总才能返回正确的数据。归并的代价在“分页排序”场景下最明显SELECT * FROM t_order ORDER BY gmt_create DESC LIMIT 10 OFFSET 10000;每个分片都需要把前10010条数据传给代理代理排序后取第10001到10010条。随着OFFSET加大传输和排序的成本急剧上升。这就是代理层深分页性能瓶颈的本质原因后面第6节会讲针对性方案。2.5 一个类比帮你理解代理的内部角色可以把代理想象成餐厅的前台接待员。客人应用不需要知道后厨有几位厨师数据库节点、哪个厨师擅长什么菜数据分布只要告诉接待员“我要点菜”就行。接待员负责安排座位分配连接、把单子分给对口的厨师SQL路由、等菜好了再汇总到一桌结果归并。但接待员不是厨师如果客人点了一桌需要六个厨师同时协作的菜跨分片事务出菜速度和协调成本就上去了。3. 主流分布式数据库代理选型对比这一节聊选型。市面上的代理方案很多但设计取向差异很大。我挑四个有代表性的来对比ShardingSphere、Vitess、ProxySQL、MyCat。3.1 四款方案的定位差异先放一张对比表后面逐个补充说明维度ShardingSphereVitessProxySQLMyCat所属社区Apache基金会CNCF独立开源国内社区对外协议MySQL/PostgreSQLMySQLMySQLMySQL分库分表支持功能最全支持核心场景不支持支持读写分离支持支持支持轻量高效支持分布式事务XA BASE事务两阶段 MapReduce风格不支持部分支持部署方式接入SDK或独立Proxy独立集群独立进程独立进程运维复杂度中高低中适合场景Java业务体系、分片需求明确K8s大规模场景读写分离和连接管理为主传统分库分表迁移3.2 ShardingSphere功能最全的“瑞士军刀”ShardingSphere有两个形态ShardingSphere-JDBC是嵌入应用内的SDKShardingSphere-Proxy是独立部署的代理进程。JDBC形态没有网络开销性能更好但侵入性强每个接入服务的代码都要改数据源配置Proxy形态对应用透明应用不用改代码但多一层网络转发。我的实际体会是小团队、新项目、需要精细控制路由逻辑的优先考虑JDBC形态已有存量系统、不方便改代码的或者有多语言业务需要统一走数据层的用Proxy形态。两个形态的配置规则是同一套规范可以平滑迁移这也是它社区活跃度一直很高的原因之一。3.3 VitessK8s生态下的重型选手Vitess起源于YouTube后来捐给CNCF。它是为超大规模场景设计的和Kubernetes结合得非常紧密支持动态分片、自动resharding能在线迁移数据。能力确实强但随之而来的是学习成本高、组件多部署运维需要专业的团队。如果业务体量还没到海量级别上Vitess的投入产出比不高。有句话很扎心Vitess不是解决你当前问题的方案而是解决你未来几年问题的方案但前提是你能撑到未来几年。3.4 ProxySQL读写分离场景的轻骑兵ProxySQL的定位完全不同。它不做分片专注在高性能读写分离、连接复用、查询路由和流量控制上。配置基于SQLite支持在线修改所有配置项包括查询规则可以基于正则匹配改写SQL非常灵活。如果需求只是把多个从库流量均匀分流、或者给指定账号设置不同的路由策略ProxySQL是我最推荐的选择。它轻、快、无状态运维上几乎没有心智负担。注意它解决不了分库分表问题别在分片需求下硬选它。3.5 MyCat和它的衍生版本MyCat是国产开源的老牌分库分表中间件早年很多传统企业项目用它做水平拆分。它的配置方式是定义逻辑库、逻辑表到物理库表的映射关系有专门的XML配置。MyCat的历史意义不小但社区活跃度这些年明显下滑新项目不太建议从零选它。如果在维护老系统碰到了它也别慌核心原理仍然是SQL解析路由排查问题的思路和ShardingSphere这类产品是相通的。3.6 我的选型决策思路我自己的选型决策顺序大致是这样先问业务是否必须做分片。业务还没到单表千万级只是读写压力大优先ProxySQL或主从分离解决别为未来过度设计。确定要分片的看团队基础设施。已经有K8s、有基础设施团队、业务增速猛可以评估Vitess传统虚拟机部署、Java技术栈为主选ShardingSphere更顺手。再看团队对“侵入式改造”的接受度。能改业务代码、想追求性能选ShardingSphere-JDBC不能改代码、要平滑接入选ShardingSphere-Proxy或MyCat系。最后考虑长期维护成本。开源项目要看社区活跃度和版本迭代频率这决定了你踩坑时有没有参考资料。4. 落地部署一主两从环境下的读写分离和分表配置实战抽象的架构讲完了进入实操环节。我用ShardingSphere-Proxy 5.x版本在一主两从的MySQL环境上把读写分离和分库分表的配置完整走一遍。4.1 环境准备准备三台MySQL节点节点IP示例角色ds_primary192.168.1.11主库负责写入ds_replica_0192.168.1.12从库负责读ds_replica_1192.168.1.13从库负责读三节点间做主从复制这里不做展开只强调一句主从复制必须确认relay log状态正常。很多代理层的问题表象在查询结果不对根因在主从延迟或复制中断。代理层只负责路由不负责修复数据一致性。ShardingSphere-Proxy解压后核心配置目录是conf主要关注两个文件server.yaml配置全局参数config-sharding.yaml配置数据源和分片规则。4.2 全局配置逻辑认证与SQL开关server.yaml里首先要定义逻辑库的认证信息。这里有一个容易混淆的点逻辑库的用户名密码是给应用连接代理用的不需要和物理库的账号一致。rules: - !AUTHORITY users: - root%:root - sharding%:sharding provider: type: ALL_PERMITTED然后开启SQL相关配置方便后面验证路由props: sql-show: true sql-comment-parse-enabled: truesql-show: true是调试阶段必开的项。每一条SQL路由到哪个数据源、哪个表都会在日志中打印排查路由错误时靠它定位。4.3 数据源配置逻辑数据源到物理节点映射config-sharding.yaml里先配置后端物理数据源dataSources: ds_primary: url: jdbc:mysql://192.168.1.11:3306/demo_ds?serverTimezoneUTCuseSSLfalse username: root password: secret connectionTimeoutMilliseconds: 30000 ds_replica_0: url: jdbc:mysql://192.168.1.12:3306/demo_ds?serverTimezoneUTCuseSSLfalse username: root password: secret connectionTimeoutMilliseconds: 30000 ds_replica_1: url: jdbc:mysql://192.168.1.13:3306/demo_ds?serverTimezoneUTCuseSSLfalse username: root password: secret connectionTimeoutMilliseconds: 30000然后定义逻辑数据源write_query_ds主库负责写入从库用负载均衡算法读取。rules: - !READWRITE_SPLITTING dataSources: write_query_ds: writeDataSourceName: ds_primary readDataSourceNames: - ds_replica_0 - ds_replica_1 loadBalancerName: round_robin loadBalancers: round_robin: type: ROUND_ROBINROUND_ROBIN就是简单轮询。还有一个常用选项是RANDOM权重随机路由。生产环境我基本只用轮询因为随机对连接分布的控制力不如轮询直观。4.4 分片规则配置哈希分片与分片表在同一个文件里配置分片规则。假设t_order表按user_id分到4个库、每库2张表- !SHARDING tables: t_order: actualDataNodes: ds_primary.t_order_$-{0..1}, ds_replica_0.t_order_$-{0..1}, ds_replica_1.t_order_$-{0..1} databaseStrategy: standard: shardingColumn: user_id shardingAlgorithmName: db_hash tableStrategy: standard: shardingColumn: user_id shardingAlgorithmName: tbl_hash shardingAlgorithms: db_hash: type: HASH_MOD props: sharding-count: 3 tbl_hash: type: HASH_MOD props: sharding-count: 2这里有个细节需要特别说明actualDataNodes里写了三个数据源但实际上t_order的物理表是在主库ds_primary上。因为分片和读写分离是叠加工作的写入时路由到主库对应分片读取时路由到从库对应分片。这里的ds_replica_0.t_order_$-{0..1}中的表结构不一定要预先建好也牵出一个关键实践ShardingSphere-Proxy不会自动建表所有物理表都要你在后端数据库中手动初始化否则查询不到数据时第一个排查点往往就是这里。HASH_MOD即哈希取模。注意分片数和实际数据节点数量必须严格匹配比如db_hash的sharding-count: 3是因为有3个数据源tbl_hash的sharding-count: 2是因为每库2张表配错会导致路由结果落在不存在的节点上。4.5 启动与连通性验证启动代理进程bin/start.sh然后用MySQL客户端连接代理端口mysql -h127.0.0.1 -P3307 -uroot -proot执行一条查询SELECT * FROM t_order WHERE user_id 9527;回到代理日志你会看到类似这样的输出Actual SQL: ds_primary ::: SELECT * FROM t_order_1 WHERE user_id 9527这说明SQL被正确路由到了ds_primary上的t_order_1表。如果Actual SQL显示的是ds_replica_0或ds_replica_1说明读流量被正确分发了。用这个方式你可以快速验证读写分离和分片路由是否同时生效。5. 分布式事务的代理侧解法从XA到BASE分片规则配好之后紧接着撞上的就是分布式事务。这是分布式数据库代理绕不开的话题而且处理不好会直接翻车。5.1 代理层事务的三个处理等级我在实践中把事务处理分成三个等级按成本从低到高单分片事务。SQL路由后只落在一个分片上事务代价和单机事务几乎一致。这要求业务设计尽量把相关数据放在同一分片比如同一用户的订单都按user_id路由天然落在同一分片这是最优解。XA两阶段事务。当事务跨多个分片时代理作为协调者发起两阶段提交先让所有参与分片执行prepare全部成功后统一commit任一失败则统一rollback。ShardingSphere-Proxy实现了XA协议对使用MySQL/PostgreSQL的应用透明。代价是性能开销大锁定资源时间长响应时间波动明显。BASE柔性事务。核心思路是放弃强一致追求最终一致。典型实现是TCCTry-Confirm-Cancel和Saga。ShardingSphere这块的生产级能力一般实际项目里更多用Seata这类独立分布式事务框架代理只负责路由事务协调交给Seata来管。5.2 我的事务设计经验有几条经验值得单独拎出来尽量设计成单分片事务。如果事务里涉及的数据本来就有天然的聚合维度同用户、同商家一定要把分片键设计成这个维度事务成本就能压到最低。跨分片事务的并发量一定要压测。XA在低并发下的表现还能接受一旦并发上来数据库连接被长时间占用连接池很容易被打满。我见过一个系统上线前没注意这个问题压测时连接池直接爆掉最后不得不把所有跨分片事务改写成“单分片异步补偿”。排查事务相关问题时先看代理日志的事务标记。ShardingSphere-Proxy会在日志里打印begin transaction、commit、rollback的字样先确认事务边界是否按预期提交再去看数据库的锁等待状态顺序反了会浪费很多时间。5.3 一个实用的降级方案如果你必须跨分片修改数据但又不想背XA的重开销可以走“本地消息表异步任务”的老路子。即先在一个分片里写入主业务记录和消息表记录本地事务提交后由后台任务读取消息表再分发到其他分片执行后续更新。这个方案虽然不是强一致但如果业务能接受秒级至分钟级的最终一致运维复杂度会远低于XA。6. 性能与踩坑代理层运行期的调优和排错配置上线只是开始运行期的问题才是大头。我把这一节写成常见问题清单基本覆盖我这些年遇到的代理层高频坑。6.1 连接池参数不合理导致雪崩代理的线程模型和连接池参数直接决定瓶颈位置。ShardingSphere-Proxy基于Netty默认的接受工作线程数、后端连接池上限都需要按实际并发调整。后端连接池大小和数据库最大连接数之间的换算非常关键。假设MySQLmax_connections是1000代理后端池给每个逻辑库最大50个连接前端连接数可以放开到200个因为前端连接是轻量的、异步的。如果后端连接池配得过大一个代理节点就能把数据库连接吃光你就等着看数据库端的“too many connections”告警吧。调整思路先定数据库能扛的最大连接数再按代理节点数均分留出一部分给直接连库的运维操作。这里可以打个比方代理是数据库的门卫门卫再敬业也不能把大门全堵死自己人得有路走。6.2 深分页性能问题为什么命中索引还是很慢第2节提过深分页的归并开销。这里给出实际解法。方案一禁止深分页跳页只允许翻页。业务的“第5000页”本来就没多大意义改成“加载更多”可以避开大OFFSET。方案二把深分页转成基于游标的模式。用上一页最后一条记录的主键或排序字段替换成WHERE id last_id ORDER BY id LIMIT 20。因为路由条件和排序条件都清晰且每个分片只取20条性能会好很多。方案三如果必须要用OFFSET关注代理是否做了“结果下推”。某些SQL在支持分片键的条件下可以下推到单分片执行实际是不归并的。写SQL时尽量带全分片键让代理有机会把查询收敛到单分片。6.3 非分片键查询引发的“全路由风暴”这是分片场景下最常见的性能杀手。SELECT * FROM t_order WHERE order_no ORD123456; -- order_no不是分片键代理无法根据order_no定位分片只能广播到下挂所有分片执行然后统一归并。如果分片数量很多一次查询的代价相当于扇出到所有库。业务规模上来后这种查询会把代理和数据库同时打垮。解决方案是先按order_no反查出user_id再按user_id路由。做法是建一张“order_no到user_id”的映射表或者用ES存索引。总之没有分片键的查询一定要在业务层设计兜底方案不能指望代理帮你溶化所有查询。6.4 SQL改写与方言兼容的小坑代理的SQL解析能力虽强但总有边界。有些代理支持MySQL的INSERT ... ON DUPLICATE KEY UPDATE有些在改写时可能改写错。再比如SELECT *在分片归并排序时如果排序字段不包含在结果集里有些代理会在归并时抛出排序异常。我的习惯是上线前把业务里的SQL全量导出一遍在代理环境下逐一执行比对结果。市面上有一些SQL审核工具可以辅助但没有能完全取代真实验证。这个环节省不了。6.5 主从延迟导致的读脏数据读写分离场景下的读延迟是绕不开的。业务上如果无法接受可能读到延迟数据有两种做法一是把这类请求强制路由到主库通过代理的“同一线程或同一连接上下文内写后立即读走主库”的规则实现二是设置从库延迟阈值超过阈值就从负载均衡池中摘除该从库。ShardingSphere-Proxy的loadBalancer类型里有一个参考实现是基于延迟的但真正稳妥的还是在业务层做标记。比如“写了订单详情后5秒内该用户的订单查询必须走主库”这种业务规则比通用的代理规则容易控制和理解。6.6 故障排查的一般链路代理层排查问题我的顺序固定是看代理日志的Actual SQL确认SQL是否路由到了预期节点。看代理统计指标确认前端连接数、后端连接数、请求耗时是否有异常波动。看后端数据库的慢查询日志区分是SQL本身慢还是代理归并慢。看网络延迟确认代理到后端的链路是否正常。这套顺序基本能定位九成的问题。核心是先在代理日志处验证“SQL去哪儿了”这是分布式数据库代理和普通数据库最大的不同——你的SQL执行结果可能是多个节点结果的合并先知道每个子SQL跑了什么才能往下排查。7. 几条运营层面的建议技术之外还有一些代理层长期运营的实践心得顺带分享。留足监控面。代理是流量汇聚点对应的指标一定要先接入连接数、QPS、后端连接等待时长、SQL路由分布、归并结果集大小。这些指标平时不起眼真正出故障时就是定位的唯一线索。灰度接入。存量系统接入代理不要一把梭全部流量切过去。以读写分离为例可以先让一个业务的只读流量走代理观察一段时间再放开。分片场景更强调先在小库表上验证确认路由无误后再迁移存量数据。版本升级要谨慎。代理项目迭代快但不代表永远要追新版本。生产环境用稳定的Minor版本升级前先在测试环境跑DDL、压测、全量SQL回归。新版本最容易出的问题往往不在功能层面而在连接管理或SQL改写这类边角逻辑上。最后一点也是我觉得最根本的引入分布式数据库代理不是为了用技术复杂度来炫技而是为了让数据层和业务层之间有一层可控的缓冲。代理能帮你遮住物理拓扑的变动但遮不住数据模型本身的愚蠢设计。分片键选得好不好、事务边界划得清不清楚、深分页能不能在业务层规避这些才是真正决定分布式架构成败的东西。代理只是把这些问题集中到一个可管理的位置给了你一个相对干净的解决界面而已。就聊到这里吧。如果你正好在选型或者配置阶段希望这篇能帮你少走几步弯路。