我早年接手过一个乱成一锅粥的业务系统:单库快撑不住的时候,DBA 说瓶颈在 CPU,业务说慢在 SQL,架构师说迟早要分库分表,但是真要分,几十个微服务的数据源、事务、联表查询全部要改,光评估就要三个月。后来我们选了另一种做法——在应用和数据库之间加一层分布式数据库代理,把分库分表、读写分离这些事从业务代码里摘出去,让所有服务继续"假装"连了一个 MySQL。这篇文章想把我这几年的选型经验、搭建过程、生产环境踩过的坑一次讲透,适合那些数据量正在膨胀、又不想靠改代码来硬扛拆库的团队参考。
1. 代理层的本质:把"分布式"这三个字藏起来
1.1 没有代理的时候,你被分库分表折磨成什么样
很多人一听到分库分表,第一反应是业务代码里引入 ShardingSphere-JDBC 那种 SDK 数据源,由代码自己算路由、自己拼结果集。这个方案本身没问题,但它有一个绕不开的成本:所有接入方都得一起改造。以订单库拆成 2 个物理库为例,每个微服务都要引入同样的 SDK,换成新的数据源,重写一批 DAO,还要保证 SDK 版本一致,不然同一个分片规则解析出来的路由结果都对不上。
更麻烦的是团队边界问题。你负责订单服务,你同事负责支付服务,还有一个组管用户服务,每个组的技术栈和发布节奏不一样,推一次 SDK 升级就要挨个协调上线窗口。我见过有团队因为 SDK 版本不一致,最后线上出现了两个"分片规则版本",同一条订单数据被路由到不同分片,查不到单,排查了整整一天。这种事一旦发生,谁都不愿意再碰改造。
所以"代理层"这个思路才会重新被重视:既然改代码这么痛,那就改网络。把一整套分库分表逻辑放到中间层,业务方完全不感知。
1.2 代理在中间做了什么
分布式数据库代理本质上是一个中间层服务,对客户端伪装成一个单库,对后端数据库伪装成一个客户端。 它需要完整实现 MySQL 通讯协议(或者 PostgreSQL 协议),让应用程序拿普通的 JDBC 驱动就能连进来。你发的 SELECT 它照单全收,然后自己去做三件事:
- 路由:解析 SQL 里的分片键,决定把这句 SQL 发给哪个后端分片。
- 聚合:如果 SQL 涉及多个分片,比如全表查询、跨分片聚合,它会发一查多份,再把结果合并排序,最后返回一组"看起来就像单库算出来"的结果。
- 高可用:对后端节点做健康检查,发现主库挂了自动切换,客户端的连接不用断。
打一个生活化的比方,代理就像一个公司前台。访客(客户端)不需要知道 CEO 在几号会议室、财务在哪个工位,前台会自己判断该把人领到哪儿。访客要的只是"我有事找公司",至于内部怎么找,是前台的事。
1.3 中心化代理和 SDK 式方案,怎么选
市面上主流方案分两派:
| 维度 | 中心化代理(Proxy) | SDK 嵌入式 |
|---|---|---|
| 改造范围 | 业务代码零改动 | 每个接入方都要改数据源 |
| 部署位置 | 独立服务,独立运维 | 跟着应用进程走 |
| 扩展性 | 代理层可横向扩容 | 应用扩容即代理扩容 |
| 排查复杂度 | 多一跳网络,Tracing 要穿透 | 应用内调用,链路短 |
| 性能损耗 | 多一次网络往返、协议解析 | 本机解析,损耗较小 |
我的建议很直接:如果你的团队能接受改造,数据量还没到极致,优先用 SDK,它性能好、排错链路短。但如果你面对的是几十个存量服务、一堆老系统不敢动,或者跨团队根本推不动统一升级,那就上代理。代理不是万能药,它解决的是"存量系统的平滑演进"这个核心问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 连接管理、读写分离与分片路由:代理的三大看家本领
2.1 两段连接模型,先看懂再调优
代理天然存在两段连接:客户端到代理是一段,代理到后端数据库是另一段。很多人刚接触时不理解为什么代理自己也要一个连接池,结果压测时发现后端数据库连接数暴涨,一片茫然。
这是因为代理不是简单转发。如果客户端发来 1000 个连接,代理不可能给后端也建 1000 个连接,否则代理就没有存在的意义。它会在自己内部维护一个到后端的连接池,前端空闲连接可以复用后端连接,把数据库的并发连接数从"客户端数量"降为"阈值上限"。类似 Redis 连接池、HTTP 连接池的思路,只是这里代理要把两段池子都管好。
实际调优时,最关键的参数就是后端连接池的上限。设小了高并发下请求排队,设大了数据库先扛不住。我一般压测时先观察数据库端的连接曲线,再反过来调代理池大小,保证后端连接数约为压测并发数的 20% 到 30%,同时低于数据库 max_connections 的一半,留出运维操作余量。
2.2 读写分离,最基础也最容易出问题
代理的第二个看家本领是读写分离。配置简单,把主库和从库写进数据源组,SELECT 走从库、写操作走主库,一切看起来很美。但真正上生产之后,"主从延迟"会把你打得措手不及。
最典型的一个场景:用户在订单页提交订单,后端先 INSERT 主库,然后立刻跳转订单详情页,详情页的 SELECT 恰好被路由到从库,但从库还没有同步到这条订单记录,页面直接显示"订单不存在"。用户投诉你搞笑,你却查不出数据问题。
处理这个问题的常见手段有几种:
- 写后读强制走主库:利用代理的 Hint 机制,在 SQL 前加特殊注释强制走主库,比如
/* SHARDINGSPHERE_HINT: WRITE */。 - 主从延迟阈值控制:部分代理支持通过主从心跳延迟动态决策,延迟超过阈值就不再路由到该从库。
- 短时间容忍:对一致性要求不高的场景,干脆接受几毫秒延迟,不做特殊处理。
生产上我倾向第一和第三种结合。订单、支付这类强一致场景必须强制走主库,而列表页、计数类查询可以走从库接受轻微延迟。注意,强制走主库会放大主库压力,所以这类 Hint 要限制在必要的接口里,不要图省事全局开。
2.3 分片路由:分片键选不好,后期哭都来不及
分片路由是代理的核心能力,也是选型时最容易埋雷的部分。路由规则大体两类:哈希取模和范围分片。
- 哈希取模,比如
order_id % 2决定进 ds0 还是 ds1,优点是数据分布均匀,缺点是扩容翻倍增加时要重建数据。 - 范围分片,比如按时间按月分表,优点是扩容简单新起一张表就行,缺点是热点不均,月末表可能很小、月末最后一天可能狂暴。
选分片键时有一条铁律:分片键必须是你查询频次最高的等值条件。如果订单表天天按 user_id 查,你却按 order_id 分片,那每次查询都要把 SQL 广播到全部分片,性能比不分片还差。我见过一个团队按订单号做哈希分片,结果核心查询全是按用户查,代理层每个查询都变成全分片扫描,上线第三天连接池就被打爆。
分片键一旦公布,路由规则基本锁死。后续改分片键等于重新拆库。所以上线之前建议做两件事:第一,分析线上慢日志和业务 SQL,统计 WHERE 条件里出现频率最高的几个字段;第二,考虑组合分片或二级索引表,尽量避免频繁出现的非分片键查询。
2.4 绑定表、广播表,被低估的复杂度杀手
做了分片之后,跨分片 JOIN 是最大的性能黑洞。订单表和订单明细表如果都按 order_id 分片,而且分片数量一致,那两张表可以做成绑定表:同一个订单 ID 的两条记录永远落在同一个分片,JOIN 可以在单分片内完成,数据库仍然可以走索引。这个设计非常实用,但前提是你一开始就设计好,不然两张表的分片算法不一致,绑定表无从谈起。
另一类叫广播表(全局表),比如地区表、字典表。这类表数据量不大,但每个分片都存一份完整副本,代理在广播表上执行写入时会同步到所有分片,查询时则从本地分片读。注意,广播表会让写放大,一个 UPDATE 可能要影响 N 个分片,字典类表还好,如果是频繁更新的配置表,务必评估写放大的成本。
3. 代理选型实战对比:ProxySQL、MyCat、ShardingSphere-Proxy、Vitess
3.1 四个主流开源方案,别被生态宣传带偏
这四类我都实际接触过,先说结论:没有哪个最好,只有哪个更适合你当前的团队和架构。
| 方案 | 语言 | 核心能力 | 适合场景 | 注意点 |
|---|---|---|---|---|
| ProxySQL | C++ | 读写分离、连接池、SQL 防火墙、查询缓存 | MySQL 规模中等,重点是搞流量治理 | 本身不是分库分表引擎,分片要自己搞 |
| MyCat | Java | 分库分表、读写分离,历史久 | 传统 Java 团队,网上资料多 | 社区活跃度下降,SQL 兼容性问题要踩 |
| ShardingSphere-Proxy | Java | 分片、读写分离、数据加密、分布式事务 | 需要标准 SQL 解析、数据治理全面的团队 | 对 MySQL 协议实现较全,建议优先试 |
| Vitess | Go | 大规模分片、k8s 原生、跨机房 | 云原生大集群,能接受较高运维投入 | 不轻量,不适合中小团队直接上 |
ProxySQL 更像是一个"数据库流量网关",在读写分离、连接复用、SQL 限流这块做得很极致,性能也强。但它不做分片路由,如果你想分库分表,还得在前面再放一个分片层,架构会绕。
MyCat 是很多老 Java 项目里的常客,当年一票电商系统靠它拆库。你要是翻源码会发现它的分片规则、全局序列号、ER 分片这些概念都挺全,但近年更新节奏慢,遇到一些新的 MySQL 8 认证插件、JSON 函数时会有兼容问题,新项目我不太推荐。
ShardingSphere-Proxy 我放在压轴讲,因为它从 ShardingSphere-JDBC 演化而来,SQL 解析能力经过多年打磨,支持 MySQL、PostgreSQL、openGauss 多种协议,而且分片、读写分离、数据加密、影子库这些能力开箱即用。Apache 基金会项目,社区比较活跃,文档也全。它是目前"零代码改造上分片"这个场景下最省心的方案之一。
Vitess 是另一个量级的产品,它源于 YouTube 的数据库实践,在 Kubernetes 上运行,自带拓扑管理、自动重分片、备份恢复,适合上千节点的大规模场景。反过来,如果你的团队只有两三个人管数据库,Vitess 的运维成本会直接把你劝退。
3.2 我总结的选型决策路径
这些年每次给团队选代理,我基本走下面这条路:
- 先问需求:只是要读写分离和连接池,还是真的要分库分表。只要前者,ProxySQL 或 MaxScale 就够了,别把架构搞重。
- 再问规模:日均 QPS 在十万级以下、分片数量个位数,ShardingSphere-Proxy 很顺手;如果在百万级且强依赖 K8s,Vitess 可以认真考虑。
- 最后问团队:有没有人能持续维护这套中间层。代理是一个新的基础设施,线上出问题你必须有人看得懂日志、调得动参数,纯粹靠文档撑不起来。
如果你团队 Java 背景强,我推荐直接看 ShardingSphere-Proxy;如果是纯运维驱动、以 MySQL 为主,ProxySQL 会是更轻的选择。
4. 手把手搭一个分库分表代理:ShardingSphere-Proxy 5.4 实操
4.1 前置准备和部署
用 ShardingSphere-Proxy 搭代理,先准备一台 Linux 服务器,JDK 17 以上,MySQL 两个实例(ds0 和 ds1),再下载 ShardingSphere-Proxy 5.4.x 的二进制包。解压之后目录结构大概是这样:
code复制apache-shardingsphere-5.4.x-shardingsphere-proxy-bin/
├── bin/
│ ├── start.sh
│ └── stop.sh
├── conf/
│ ├── server.yaml
│ ├── config-sharding.yaml
│ └── config-readwrite-splitting.yaml
└── lib/
启动逻辑不复杂,真正决定成败的是 conf 下的几个 YAML 配置。我强烈建议先备份这些文件,改出问题还能快速回滚。
4.2 配置逻辑库和分片规则
先看 server.yaml,核心是授权信息:
yaml复制rules:
- !AUTHORITY
users:
- root@127.0.0.1:root
- sharding@127.0.0.1:sharding
provider:
type: ALL_PERMITTED
这里的用户名密码是客户端连代理时用的。注意,ALL_PERMITTED 表示对所有后端库自动放行,测试环境可以,生产环境要配合权限配置做到最小授权。
再看 config-sharding.yaml,这是分片核心:
yaml复制schemaName: sharding_order_db
dataSources:
ds0:
url: jdbc:mysql://192.168.1.10:3306/order_ds0?serverTimezone=Asia/Shanghai&useSSL=false
username: dba
password: dba_pass
ds1:
url: jdbc:mysql://192.168.1.11:3306/order_ds1?serverTimezone=Asia/Shanghai&useSSL=false
username: dba
password: dba_pass
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds${0..1}.t_order_${0..1}
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: t_order_inline_mod
keyGenerateStrategy:
column: order_id
keyGeneratorName: snowflake
bindingTables:
- t_order, t_order_item
broadcastTables:
- t_region
shardingAlgorithms:
t_order_inline_mod:
type: INLINE
props:
algorithm-expression: t_order_${order_id % 2}
keyGenerators:
snowflake:
type: SNOWFLAKE
这段配置的含义是:逻辑库名 sharding_order_db,后端两个物理库 ds0 和 ds1;逻辑表 t_order 映射到物理表 t_order_0 和 t_order_1,按 order_id % 2 决定数据落到哪一张表;同时配置了绑定表 t_order 和 t_order_item,以及广播表 t_region。主键用雪花算法生成,避免分片后 ID 冲突。
很多新手只配了分片键,忘了配主键生成策略。后果是应用自己传入的 ID 可能在多个分片里重复,后续做数据合并、跨系统联查的时候会撞车。能用代理自带的主键生成策略就别省。
4.3 启动、建表、验证路由
配置好之后,执行 bin/start.sh 启动,默认代理端口是 3307。用 MySQL 客户端连上去:
bash复制mysql -h127.0.0.1 -P3307 -uroot -proot
连上后你会看到逻辑库 sharding_order_db,这时可以建表:
sql复制CREATE TABLE t_order (
order_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2),
status INT,
create_time DATETIME
);
注意,这条 DDL 是通到后端的,代理会自动把它下发给所有物理表。然后插入几条订单:
sql复制INSERT INTO t_order (order_id, user_id, amount, status) VALUES (1, 101, 88.00, 1);
INSERT INTO t_order (order_id, user_id, amount, status) VALUES (2, 102, 99.00, 1);
因为 order_id % 2 的规则,order_id=1 会落到 t_order_1,order_id=2 落到 t_order_0。你不信的话,直接去后端库 order_ds0.t_order_1 和 order_ds1.t_order_0 里翻数据,就一目了然了。查询时按 order_id 过滤,代理会精准路由到对应分片;如果按 user_id 查,代理会把 SQL 广播到所有分片再合并结果,你在逻辑库上看到的数据聚合结果和单库效果是一致的。
这里有个细节:actualDataNodes 里如果表的数量是 t_order_${0..1},而你没有提前在后端创建物理表,启动后建表操作能否成功取决于代理版本和配置。我一般会先在两个后端库手动把物理表建好,再启代理,避免代理和数据库 DDL 权限的兼容性纠缠。
4.4 追加读写分离配置
如果还想让代理顺带做读写分离,再单独写一个 config-readwrite-splitting.yaml,并在启动时加载:
yaml复制schemaName: sharding_order_db
dataSources:
write_ds:
url: jdbc:mysql://192.168.1.10:3306/order_ds0
username: dba
password: dba_pass
read_ds_0:
url: jdbc:mysql://192.168.1.12:3306/order_ds0_replica
username: dba
password: dba_pass
rules:
- !READWRITE_SPLITTING
dataSources:
order_rw:
writeDataSourceName: write_ds
readDataSourceNames:
- read_ds_0
loadBalancerName: round_robin
loadBalancers:
round_robin:
type: ROUND_ROBIN
配完之后,同一个逻辑库里既做分片又做读写分离,写请求进主库、读请求进从库。这里要特别小心:读写分离的从库数据源必须和分片的数据源在逻辑上对应好,一旦从库的库名表名结构和主库不一致,路由之后会直接报"表不存在"。
5. 代理层的高可用:代理不能成为新的单点
5.1 代理节点横向扩展与流量接入
引入代理之后,最尴尬的事就是代理自己挂了。所以第一件事是让代理节点至少部署两个,前面用负载均衡把流量分过去。常见做法有两类:
- 四层负载均衡:LVS、Nginx Stream、云上 LB,直接把 MySQL 协议转发到代理节点。
- 域名 + 多 IP 轮询:客户端配置域名,DNS 轮询到多个代理节点。
如果团队有 Nginx 使用经验,用 Nginx Stream 模块做四层转发很快。一条核心配置如下:
nginx复制stream {
upstream db_proxy {
server 192.168.2.11:3307;
server 192.168.2.12:3307;
}
server {
listen 3306;
proxy_pass db_proxy;
proxy_timeout 60s;
}
}
这样客户端还是连 3306 端口,流量被分发到两台代理,任何一台宕机,另一台接管。注意,代理层本身如果是无状态的话(所有路由规则所有节点一致),扩容就是加节点、改负载均衡配置,不用迁移任何数据。这正是中心化代理比 SDK 好扩容的地方。
5.2 后端节点探活与故障转移
代理内部的健康检查往往被忽略。以 ShardingSphere-Proxy 为例,它有两种探活方式:一种是后端数据库连接失败时自动触发告警;另一种靠定时任务轮询。你可以结合数据库行数来验证,比如自定义一个探活 SQL,配置在 server.yaml 的 healthCheck 部分。
实际生产里我遇到最多的问题是:主库宕机后代理不会自动把写请求切到新主库。因为代理只认配置里给的数据源,主从切换是 DBA 或数据库高可用方案(如 MHA、Orchestrator)的事。代理和外部高可用组件要联动:外部切换主库后,代理侧要么通过配置热刷新发现新拓扑,要么由脚本触发代理数据源切换。如果你不提前设计这个联动,主库切换后代理还会继续往旧主库写,直接写入失败。
5.3 连接池参数:再好的架构也怕连接风暴
代理的连接池参数是上线前的必调项。主要关注这几个:
- 最大连接数:包括前端最大连接和后端连接池上限。
- 连接空闲超时:空闲连接超过时间要回收,避免后端堆积。
- 连接获取超时:请求等待连接的时间,超时直接报错,防止雪崩。
- 读写超时:和后端数据交互的超时,防止慢 SQL 拖垮线程。
一个典型的坑是:客户端连接池设置得很大,比如 200,代理后端连接池也设置 200,压测时数据库连接数直接冲到几千,数据库 OOM。正确做法是确保后端连接池上限远小于数据库 max_connections,我通常压测时用"数据库连接数曲线"倒推代理参数,而不是拍脑袋设。
5.4 一次"too many connections"的真实排查
有一回线上凌晨突然爆出一堆 too many connections,应用日志里全是连接失败。查代理指标,发现前端连接数正常,后端连接到数据库的活跃线程却被大量积压的 SQL 占满。
排查链路是这样的:
- 先看代理日志,发现大量异步线程在等待某几个慢 SQL。
- 再到数据库
SHOW PROCESSLIST,发现大量SELECT * FROM t_order WHERE user_id = ?的查询,扫描行数十几万。 - 顺藤摸瓜,发现这些 SQL 通过代理被广播到了所有分片,因为
user_id不是分片键,代理对非分片键查询必然全片扫描。 - 最终结论:不是代理有问题,是应用侧某一次数据对账任务写了一条不带分片键的查询。
这件事给我的教训很深:代理能不能扛住全片扫描是一回事,该不该让它扛是另一回事。 分片键缺失的查询,一旦数据量上来就会成为新的慢 SQL,需要尽早通过代理的慢查询日志或者 SQL 审计能力发现,把它挡在应用层。
6. 生产环境踩坑清单:SQL兼容性、分布式事务与迁移的坑
6.1 SQL兼容性:标准 SQL 没问题,方言函数要小心
代理的 SQL 解析引擎再强,也不可能覆盖所有数据库方言。我自己遇到过的典型问题包括:SELECT LAST_INSERT_ID() 在分片后语义变得模糊;自定义函数、存储过程、某些 MySQL 特有语法无法被解析;大事务里的 SET autocommit = 0 状态在多分片上难以统一维护。
应对策略是:上线前做一轮 SQL 兼容性扫描,把应用里所有 SQL 摘出来,在测试环境跑一遍,筛选出"代理解析失败"和"结果集行为不一致"两类,提前改造或绕过。某些低频的管理类 SQL 可以直接直连后端库执行,不一定要走代理。
6.2 跨分片查询:能不做就不做,非做不可要心里有数
跨分片 JOIN、跨分片 ORDER BY、跨分片分页,是代理性能的滑铁卢。当代理把一条 SQL 广播到多个分片后,每个分片只能算出局部结果,然后汇总排序。比如 LIMIT 10 OFFSET 10000,代理要把所有分片的 10010 条记录都拿回来排序,再截取目标页,数据量一大,内存直接被打满。
如果你的业务存在大量这类查询,有两个方向:一是从分片键设计上解决,让高频查询的过滤条件天然带上分片键;二是引入搜索引擎或分析型存储,把这类复杂查询引流到别的地方,而不是硬怼代理。记住,代理不是为了把所有 SQL 都变快,它只是让你在做数据架构升级时不用把代码全部推倒重来。
6.3 分布式事务:XA 和柔性事务的取舍
分库分表之后,原来单库上的本地事务会变成跨库事务。ShardingSphere-Proxy 支持 XA 两阶段提交和柔性事务(如 Seata)。
我实际测试下来的感受是:XA 强一致,但性能损耗明显,一个跨 2 个分片的事务延迟可能是单库事务的 2 到 3 倍。柔性事务性能好很多,但需要业务上实现最终一致性的补偿逻辑。这里没有银弹——核心交易链路如果量不大,可以咬牙上 XA 图一个省心;高并发场景,请老老实实做补偿设计,别指望事务框架帮你兜底所有副作用。
6.4 平滑迁移与回滚
最后聊迁移。理想流程是:先搭代理、建好逻辑库和分片规则,然后把应用的数据源连接地址从物理库改成代理,逐批灰度切换。灰度前,先做只读灰度,让一部分只读流量走代理验证 SQL 兼容性;确认没问题后,再把写流量切过来。
一定要准备的回滚手段有两种:一是保留原直连数据库的旧配置,出问题直接切回;二是确保代理侧配置是"增量可回退"的,新逻辑库失败可以快速把客户端指向旧库。我曾经见过团队把直连改成代理后顺手把旧连接地址删了,结果代理宕机时想回滚都回滚不了,只能原地修,非常被动。
写在最后的体会
代理这东西,本质上是一层"架构缓冲垫",它让团队在业务高速迭代时不用立刻承受拆库的阵痛,代价是多了一个需要运维的基础设施。我个人最大的体会是:别把它当成万能中间件去硬扛复杂 SQL,也别因为怕改造就完全回避它。 它最适合的场景始终是"存量系统需要平滑演进"和"多团队难以统一推动改造"这两种。
如果你现在正站在"要不要上分布式数据库代理"的十字路口,我建议先拿一个小业务线试水,跑通配置、压测、监控这一整套流程,再决定要不要全面铺开。代理的安装部署不难,难的是你愿不愿意花精力把路由规则、连接池参数、故障联动、SQL 兼容性这些细节一条条磨到位。磨到位之后,它确实能给你带来很长一段时间的安稳日子。
