分布式数据库代理,说白了一句话:让业务层像访问单库一样访问一个分布式数据库集群。这句话听起来简单,真正落地时才知道坑有多深——读写分离、分库分表、连接管理、分布式事务、跨节点查询,全都要在这一层解决。我最早接触这个方向是因为一个订单系统从单库演进到分片后,应用代码里到处都是路由逻辑,换个分片键恨不得改半个项目,后来才意识到,与其让每个业务团队自己处理这些琐碎细节,不如在数据库前面加一个统一的代理层。这篇文章把我这些年折腾分布式数据库代理的经验整理出来,包括它到底解决什么问题、核心功能怎么设计、方案怎么选、以及最常见的坑怎么排查,适合正在做分布式改造、或者准备引入中间件的团队参考。
1. 为什么需要分布式数据库代理
1.1 从单库到分布式,问题到底出在哪
很多团队刚做分布式改造时,第一反应是直接改应用。先做主从分离,读走从库、写走主库,于是每个 DAO 里多了一个“读方法”和一个“写方法”;然后做分库分表,按用户 ID 拆 16 张表,于是每个 SQL 都要拼表名、路由到指定数据源。这种方案在小规模下能跑,但业务一多就失控了:报表要跨库汇总,后台要按订单号查,运营要按时间范围扫,每个查询都得自己写合并逻辑。我见过一个项目,光 SQL 路由的工具类就有三千多行,而且每个业务团队各写各的,风格完全不一样。更麻烦的是,一旦要做主从切换或者增减分片,所有应用都要跟着改配置、发版本,运维成本瞬间被顶到一个离谱的高度。
1.2 代理层到底解决了什么问题
分布式数据库代理的核心思路,是把“如何访问多个数据库实例”这件事从业务应用里抽出来,放到一个独立的中间层去处理。业务应用仍然只看到一个逻辑上的数据库,发出去的 SQL 和以前一样是SELECT * FROM t_order WHERE order_id = ?,至于这个 SQL 应该去哪个物理库、哪张物理表,由代理层解析并路由。这样做有三大收益:第一,业务代码保持简单,团队不需要维护自己的路由工具;第二,数据节点变化对业务透明,扩分片、切主库这类操作只改代理配置;第三,可以把连接管理、读写分离、分布式事务等通用能力沉淀到一层,所有业务共享。代价则是多了一层网络跳转,以及代理本身可能成为新的性能瓶颈和故障点,所以选型和部署方式需要额外谨慎。
1.3 客户端模式与代理模式怎么选
严格来说,市面上解决这个问题有两种路线。一种叫客户端模式,比如 ShardingSphere-JDBC,它把路由和分片逻辑封装成 JDBC 驱动,应用直接依赖 Jar 包,不需要独立部署进程。优点是无额外网络开销、性能好,缺点是对应用侵入强,每个应用都要引入依赖并配置,语言栈也被绑定。另一种就是本文重点聊的代理模式,比如 ShardingSphere-Proxy、MyCat、Vitess,它们独立部署成一个服务,应用通过 MySQL 或 PostgreSQL 协议连接它,就像连一个普通数据库。代理模式对应用最友好,切换成本低,也更适合多语言团队,但需要单独维护集群、考虑高可用和性能消耗。我的建议是,如果是全新的 Java 项目而且团队掌控力强,客户端模式确实快;但绝大多数传统企业场景,业务系统是异构的,甚至还有第三方系统要连上来,代理模式几乎是唯一能落地的答案。
2. 核心功能拆解:一个代理该做什么
2.1 读写分离与流量调度
读写分离是代理最基础也最常用的功能。代理拿到一条 SQL,先判断它是SELECT还是写操作,然后决定发到主库还是从库。但这里有个容易被忽略的细节——事务内的读必须走主库。比如你开启一个事务,先UPDATE了一条记录,再SELECT这条记录,如果这个读被路由到从库,而主从延迟还没追上,你就读到旧数据了,这在业务上是不可接受的。所以好的代理会跟踪连接的事务状态:一旦客户端执行了BEGIN或者任何写操作,后续所有 SQL 都强制走主库,直到事务提交。另外,负载均衡策略也不能只看轮询。我实测下来,对延迟敏感的报表类查询,用ROUND_ROBIN问题不大;但遇到长事务或者大查询,最好按从库的实时负载动态调度,否则个别从库容易积压。还有一点,代理的读写分离和数据库本身的主从同步是两件事,代理只负责把流量分开,数据同步还得靠 MySQL 主从复制或 binlog 同步工具,别搞混。
2.2 分库分表路由的几种方式
分片是代理最核心的能力,常见有两种路由策略:哈希取模和范围分片。哈希取模就是拿分片键算一个 hash,再对分片数量取模,比如user_id % 16,优点是数据分布均匀,缺点是扩分片时要迁移数据;范围分片则是按时间或 ID 区间划分,比如按月分表,写起来直观,但容易产生热点,月末的订单全压在一张表上。这里我想多说一句,分片键的选择几乎决定了下半辈子的幸福程度。最理想的分片键是查询频率最高的等值条件,比如订单表按user_id分片,那么“查某个用户的订单”只需要路由到一张表,性能最好。如果业务经常按order_id查,那order_id也得能映射到user_id,通常的做法是订单号里冗余用户标识,或者建一张映射表。最怕的是那种“我两个字段都要高频查询”,又不愿意改造表结构的团队——最后要么做全库广播,要么只能引入额外的索引系统。代理生成 SQL 时,对大表一般不会自动做全节点查询,除非你明确知道自己在干什么。
2.3 连接管理:前端连接和后端连接是两回事
很多人第一次看代理的连接模型会懵:客户端连代理是一套连接,代理连真实数据库又是另一套连接,这两套连接是解耦的。举个例子,如果后端有 16 张表分布在 4 个库,一个查询可能同时涉及多个库,代理就需要同时占用多个后端连接来并行执行子查询。假设客户端的并发连接数是 200,代理池里的后端连接数不够,SQL 就会排队等待,表现就是应用侧连接池打满、接口变慢。所以设置代理的后端连接池时,不能只看客户端连接数,还要看单条 SQL 平均会扇出到几个节点。我一般按“后端连接数 = 预估并发数 × 平均扇出数”来估算,然后留 30% 余量。除了数量,连接的空闲回收和健康检查也关键,MySQL 的wait_timeout默认 8 小时,如果代理不主动保活,空闲连接很容易被数据库断开,第二天上班第一波流量就会出现大量连接错误。
2.4 高可用与故障转移
代理层承担了所有流量的入口,它自己挂了,业务就全挂了,所以高可用不是可选项。至少要做到两点:一是代理服务本身多实例部署,前面用负载均衡或 VIP 接入,实例间无状态;二是代理能够感知后端数据库节点的健康状态,自动摘除故障节点。这里有个细节,故障转移的粒度要能控制到“读”和“写”分开。某台从库磁盘满了或复制延迟过大,它应该只从读流量里摘除,不影响写;要是主库出问题,那就得触发主从切换,代理自动把写流量切到新的主库。我踩过的坑是,早期用一个简单的 TCP 探活来判断后端节点状态,结果数据库负载很高、还能 ping 通,代理照样把流量发过去,直接把数据库压死。后来改成执行轻量 SQL 探活,比如SELECT 1,并且连续失败 3 次才摘除,才算稳定下来。
3. 分布式事务与一致性处理
3.1 跨库事务为什么这么难
分库分表之后,原来单库里的一个事务可能跨了两个物理库,数据库的本地事务保证不了这种场景。比如一个下单操作,订单数据在order_db_0,库存数据在inventory_db_1,扣库存成功了但订单插入失败,数据就错了。很多人第一反应是上分布式事务框架,但我想先说清楚:分布式事务的本质是用“最终一致性”换取“跨库操作的可行性”,它不可能像单库事务那样既严格原子又高性能。两阶段提交(XA)看起来最“硬”,但它的同步阻塞和协调者单点问题,在高并发场景下很容易变成性能灾难,我见过一个团队用 XA 做下单接口,压测时 TPS 直接掉了 80%,后来还是拆了。
3.2 XA、TCC、本地消息表怎么选
我的经验是,方案选型要看业务对一致性的容忍度和操作的实时性要求。XA 适合小事务、低并发的强一致场景,比如跨库的账户扣款、配置更新,实现也简单,直接在代理层开启 XA 事务就行,代价是性能。TCC(Try-Confirm-Cancel)适合需要实时保证、但业务逻辑能拆成预留和确认两个阶段的场景,比如库存预占、优惠券锁定,它对业务侵入比较大,每个操作都要写三个方法,而且 Confirm 和 Cancel 必须保证幂等。本地消息表则是最实用的“兜底”方案——业务在主库执行本地事务,同时写一张消息表,然后通过异步任务把消息投递到其他库去执行,因为消息和业务在同一个本地事务里,所以不会丢。纯异步其实最适合大多数订单、积分、通知类场景,牺牲几十毫秒的可见性,换来的是系统简单可靠。代理层一般会提供 XA 支持,但 TCC 和本地消息表通常是业务自己实现,代理能帮忙做的是事务上下文透传和全局事务 ID 管理。
3.3 分布式锁不能替代分布式事务
还有一个常见误区,把分布式事务和分布式锁混为一谈。分布式锁保证的是“同一时间只有一个节点能操作某资源”,它解决不了“两个库的数据一起成功或一起失败”的问题。订单支付的回调,你用 Redis 分布式锁保证同一笔订单不被并发处理,这没错;但回调里要同时改订单表和扣减库存,如果第二个操作失败,锁也救不了你。所以分布式锁和分布式事务是互相配合的关系——锁用来防并发冲突,事务用来保证多节点操作的最终一致。不要指望“我加了锁就万事大吉”,锁释放时的一致性补偿机制才是更需要花力气设计的。
4. 主流方案选型对比:开源代理怎么挑
4.1 ShardingSphere-Proxy:目前最均衡的选择
ShardingSphere 是国内用得最多的分库分表中间件,演进到今天已经非常成熟。Proxy 模式基于 Netty 实现数据库协议,支持 MySQL 和 PostgreSQL,分片、读写分离、分布式事务、数据加密这些能力都内置了。我最看好它的一点是配置完全 YAML 化,规则清晰,团队上手快;而且它和 ShardingSphere-JDBC 共用同一套内核,以后想从 Proxy 迁移到客户端模式或者反过来,成本都可控。缺点是性能相比直连数据库有一定损耗,实测简单查询大概有个 5% 到 10% 的开销,可接受;但如果你对性能极其敏感,就要多做几轮压测再拍板。
4.2 MyCat/MyCat2:老牌中间件,但生态偏重
MyCat 很早就火了,它更像一个“数据库路由网关”,配置上有 schema.xml、rule.xml 一套体系。MyCat 2 重构过架构,支持了更多协议和分布式事务能力,但社区活跃度和代码现代化程度已经不如 ShardingSphere。我个人的感受是,MyCat 适合老团队、老项目,因为资料多,网上踩坑案例也多;但新项目我通常不建议从它起步,原因很简单——它的分片函数和 SQL 优化能力相对有限,复杂查询支持不够好,遇到问题更多要靠自己啃源码。
4.3 Vitess 和云数据库自带的 Proxy
Vitess 是 YouTube 开源的数据库集群方案,它不只是一个代理,而是一整套“分片数据库平台”,包含自动分片、动态重均衡、在线迁移等能力,在 Kubernetes 环境里部署体验很好,很多海外大厂在用。不过它的学习曲线陡峭,运维组件多,小团队没必要上来就搞这么重。另外,如果你用的是云数据库,比如阿里云、腾讯云的数据库产品,它们大多自带高可用和读写分离的接入地址,这种“托管的代理”最大的优点是免运维,缺点是定制能力弱——你没法自己写路由函数,也没法调整一些底层参数。我觉得它适合业务不复杂、不想养中间件专员的团队。
4.4 自研轻量代理的取舍
我也见过一些大厂自研数据库代理,因为业务特性太强,市面方案满足不了。自研的好处是深度可控,可以针对自己的业务定制路由和合并逻辑(比如索引数据自动路由);坏处是这几乎是一个“无底洞”——SQL 解析、协议适配、分布式事务、高可用、监控告警,每一项都是深水区。如果你认真评估后仍然决定自研,我的建议是不要从零开始,站在开源协议解析器的肩膀上,再结合实际场景做裁剪。但对绝大多数团队,我更推荐先选一款成熟的代理,踩坑过程中再决定要不要自研局部组件。
5. 实操实录:用 ShardingSphere-Proxy 落地读写分离 + 分片
5.1 场景设定与分片规划
先设定一个典型场景:订单库要支撑日千万级订单,规划 2 个物理库,每个库 8 张表,共 16 张订单表,按user_id哈希分片;同时每库部署一主一从,读走从库。分片数量选 16 而不是 2,是因为要考虑未来两三年业务增长,哈希取模的分片数一旦定下来,扩容时迁移数据非常痛苦。这个规划里,逻辑表名是t_order,物理表是ds_0.t_order_0到ds_1.t_order_15,代理负责把逻辑表的 SQL 路由到正确的物理表。
5.2 代理配置与启动步骤
ShardingSphere-Proxy 的配置分为两层:server.yaml是代理自身的配置,包括端口、权限、属性;config-sharding.yaml是数据源和分片规则。我贴一个简化但可运行的配置片段(基于 5.x 版本):
# config-sharding.yaml dataSources: ds_0: dataSourceClassName: com.zaxxer.hikari.HikariDataSource url: jdbc:mysql://192.168.1.10:3306/order_db_0 username: root password: change_me ds_1: dataSourceClassName: com.zaxxer.hikari.HikariDataSource url: jdbc:mysql://192.168.1.11:3306/order_db_1 username: root password: change_me rules: - !SHARDING tables: t_order: actualDataNodes: ds_${0..1}.t_order_${0..15} tableStrategy: standard: shardingColumn: user_id shardingAlgorithmName: t_order_hash keyGenerateStrategy: column: order_id keyGeneratorName: snowflake shardingAlgorithms: t_order_hash: type: HASH_MOD props: sharding-count: 16 keyGenerators: snowflake: type: SNOWFLAKE - !READWRITE_SPLITTING dataSources: ds_0: writeDataSourceName: ds_0_write readDataSourceNames: - ds_0_read_0 - ds_0_read_1 ds_1: writeDataSourceName: ds_1_write readDataSourceNames: - ds_1_read_0启动方式很简单,解压发行包后修改配置文件,执行bin/start.sh,默认监听3307端口。业务侧只改数据源地址,从原来的jdbc:mysql://数据库IP:3306/order_db改成jdbc:mysql://代理IP:3307/order_db,用户名密码用代理里配置的账密。这里我要提醒一句:actualDataNodes的写法决定了路由结果,ds_${0..1}和t_order_${0..15}的顺序和数量一定要和真实部署一致,不然启动时校验就会报错。
5.3 验证路由效果与性能观察
配置上线后,第一件事是验证路由对不对。直接用mysql客户端连上代理,执行PREVIEW SELECT * FROM t_order WHERE user_id = 123,代理会返回这条 SQL 实际被路由到了哪个数据源、哪个物理表。这是我最常用的排查命令,比看日志直观得多。再测一个不带分片键的查询,比如SELECT * FROM t_order WHERE order_id = 1,这会被广播到全部 16 张表,如果线上真的有人这么查,你就能立刻发现慢查询从哪里来。然后观察代理的监控指标:前端连接数、后端连接池活跃连接数、SQL 响应时间、路由到各节点的请求分布。有一次我压测发现某个分片明显比其他片慢,查下去是那个物理表的历史数据量比其他表大了三倍,索引维护成本高,这就是哈希不均匀的典型表现,需要回看分片键的取值分布。
6. 常见问题与排查技巧实录
6.1 连接池被占满,接口大面积超时
这是代理上线后最容易遇到的问题。表象是应用侧连接池报Connection is not available,代理侧后端连接数持续打满。排查思路是先分清是前端打满还是后端打满。如果前端连接数不高而后端满了,多半是 SQL 扇出太狠——一条 SQL 广播到 16 张表,瞬间占用 16 个后端连接,几条慢查询就能把连接池吃光。解决方向有三个:限制广播查询、优化慢 SQL、调大后端连接池并设置合理的排队超时。我习惯在代理前面加一层“访问控制”,把不带分片键的查询默认拦截或转发到专门的查询库,线上效果很好。如果你用的是 ShardingSphere-Proxy,可以打开 SQL 审计日志,统计哪些 SQL 的扇出数最高,针对性优化比盲目扩容有效得多。
6.2 路由结果和预期不符,数据查不到
这个坑几乎每个团队都会踩。最常见的原因是分片键值类型不一致——表结构里user_id是字符串,但代码传的是数字,哈希结果完全不同,路由到的表自然对不上。第二种是隐含的隐式转换,比如WHERE user_id = ?的?绑定参数是字符串,代理解析时按字符串算 hash,和建表时用数字算的不一致,也会路由错。建议在创建分片算法时,先拿同一批真实用户 ID 做一轮路由预演,把路由结果和预期物理表逐一比对。第三种原因是分片键被写在函数里,比如WHERE DATE(create_time) = '2024-01-01',代理解析不出来,只能全库扫描,这种 SQL 虽然不会查不到数据,但慢得让人怀疑人生。
6.3 分布式事务下数据不一致,怎么快速定位
用了 XA 或 TCC 之后,偶尔还是会出现“主库扣了钱,明细库没写入”的情况。不要慌,先看全局事务日志,确认事务状态是 commit 还是 rollback。XA 的典型问题是协调者崩溃后 prepared 状态的节点不知道该怎么办,这就需要事务恢复机制自动扫描并补偿。TCC 的问题更多出在 Confirm 和 Cancel 的幂等性上——如果不幂等,重复调用就会把库存扣成负数。我的经验是,TCC 的每个操作都要带全局事务 ID 和分支操作 ID,目标库要建一张“事务执行记录表”,同一事务 ID 的操作只执行一次,这是最朴素的幂等方案。另外,任何分布式事务方案都挡不住代码层面的 bug,日志里必须能看到完整的调用链,否则排查一个不一致问题可能要翻半天各个库的操作记录。
6.4 热点分片和数据倾斜
分库分表最怕的不是数据量大,而是数据不均匀。某个超级用户的订单量是普通用户的上千倍,按user_id哈希后,那个分片就成了热点。遇到这种情况,纯粹的哈希策略解决不了,需要在业务层面拆散大 Key——比如给超级用户增加一个“子账户”维度,让他的数据分散到多个分片;或者按时间和用户组合分片,把单用户的历史订单也摊开。这属于建模层面的优化,代理配置改不动。还有一类倾斜是“尾部效应”:新增分片后,老数据的迁移没跟上,查老数据时不时路由到不存在的地方。所以扩分片这个动作一定要有专门的迁移流程,用一致性校验工具核对每个分片的行数和对账数据,确认无误再切流量,千万别图省事直接改配置。
我个人做了几年数据库中间件相关的工作,最大的体会是,分布式数据库代理不是一个“装上去就能用”的工具,它更像是把原来分散在各处的脏活累活集中到一个地方,让团队能统一治理。它确实带来了新的运维复杂度,但换来的收益是业务侧极大的简化——新同学接手一个分库分表系统,不用再读那几千行路由代码,只需要理解“连代理、写逻辑 SQL”就够了。如果你正准备引入代理层,我的建议是先在非核心系统上跑一个月,重点观察路由准确性、连接池表现和慢查询分布,把这些基础问题解决了再推全网。毕竟,中间件越强大,你越要对它保持敬畏。