☰
分布式数据库代理:从读写分离到分库分表的平滑演进实践
2026/10/10 3:40:59 网站建设 项目流程

做了几年后端,手底下管过几十套数据库实例之后,我越来越觉得“分布式数据库代理”这玩意儿是个被低估的角色。很多人一听这名字,第一反应是“中间件”“网关”“多了一层跳转”,潜意识里就觉得它只会拖慢性能,是个麻烦的组件。但我自己的真实体感是:如果你正处于单库扛不住流量、分库分表又不敢乱下手的阶段,一个设计得当的数据库代理,恰恰是花小钱办大事、让团队平稳过渡的关键基础设施。

这篇文章,我想从自己的实践视角,把“分布式数据库代理”这件事掰开揉碎地聊一遍。它到底是什么、能解决什么问题、技术选型的时候怎么权衡、部署的时候有哪些细节坑,以及我踩过的那些数据库代理相关的“经验教训”。内容会比较长,但我会尽量说人话,适合正在规划数据库架构、或者被线上库瓶颈折磨得睡不着觉的后端同学参考。

1. 先把痛点聊透:代理到底解决了什么问题

很多团队对数据库代理的第一反应是“我们业务量没那么大,用不着”。这句话对了一半,另一半的问题在于,等业务量真的大了,你根本来不及从容地设计代理层。我还是从实际痛点倒推,说说为什么需要这一层。

1.1 单库时代的四个典型困境

我见过太多中小团队,早期就是一台主库打天下。业务跑两三年,用户量上来之后,开始频繁出现四个问题。

第一个是连接数撑不住。应用的连接池配置往往跟着微服务数量线性膨胀,十几个服务、每个服务三五十个连接,主库的max_connections率先告急。DBA那边又不敢随便调高,因为每个连接背后都是内存和线程开销,OS 层面和 MySQL 内部的资源都会被拖垮。这个时候不是光靠把连接池调小能解决的,业务高峰期该报错还是报错。

第二个是读写比例严重失衡。大多数互联网业务,读请求和写请求能到 10:1 甚至更高。主库既要处理事务写入,又要扛住海量查询,明明是读的压力把 CPU 打满,写请求也跟着遭殃。很多人会想到做主从复制,但主从复制之后,应用层怎么路由?每个服务都自己判断“这条 SQL 是读还是写”,写个if判断把查询打到从库?如果只有一两个服务还能凑合,服务一多,这个逻辑会散落得到处都是,而且从库一旦切换,所有服务的配置都要同步联动,非常痛苦。

第三个是存储容量单一。表数据量过了几千万甚至上亿,单库单表的写入瓶颈先不说,单说磁盘容量就很尴尬。扩容需要停机窗口,需要拿数据导来导去,业务可等不了这么长时间。这时候你就需要一种更“柔性”的扩展手段,而不是简单粗暴地把机器换得更大。

第四个是故障处理不够快。主库宕机的时候,如果全靠人工去检查、去切换、去改连接配置,那这个故障窗口大概率是分钟级甚至小时级的。现代业务对可用性的要求通常都在 99.9% 以上,分钟级故障已经属于重大事故。

这四个困境,单靠数据库本身很难同时解决。堆硬件能撑一阵子,但解决不了路由的灵活性问题;自己做连接管理,又容易在各业务线里复制出大量重复代码。所以我后来倾向于在应用和真实数据库之间,插入一个独立的“代理层”,让专业的东西干专业的活。

1.2 代理在这个场景里扮演什么角色

把数据库代理说成“数据库前面的反向代理”,可能更容易理解。它和应用之间走标准的数据库协议,看起来像一个数据库;它和后面的真实数据库节点之间,管理着真实的连接池和流量分发。应用不再需要关心后面挂了几台数据库、哪台是主、哪台是从,只需要连接这个代理地址就行。

代理层承担的几个核心职责大概是这样的:

  • 连接管理:统一收敛所有应用对数据库的连接,通过连接复用和池化,把后端数据库承受的连接数压力降下来。
  • 读写分离:解析客户端发来的 SQL,判断是SELECT还是写入类语句,把读流量分流到只读节点,写流量固定在主节点。
  • 数据分片路由:当数据量超出单库承载能力时,按照分片键(比如用户 ID、订单 ID)把数据分散到多个数据库实例,并对跨分片查询做聚合处理。
  • 访问控制与安全:在代理层做库表权限的二次控制,屏蔽某些高危 SQL(比如不带WHERE条件的DELETE),形成一道安全兜底。
  • 故障转移与高可用:代理节点会持续探测后端实例的健康状态,一旦主库发生切换,由代理层更新拓扑,业务侧在极短的时间内无感知地重新建立连接。

这些职责背后,本质上都在做一件事:把“数据库集群的复杂性”从业务代码里剥离出去,让应用层的同学只面对一个逻辑上的、统一的数据入口。

1.3 哪些业务适合引入代理

不是所有业务都需要代理。我自己的判断标准很简单:如果只有一两个服务、单库性能还很充裕,那确实不用引,因为引入一层就意味着多一层运维成本和网络开销。但出现下面这些情况,就可以认真考虑了:

  • 服务数量多、技术栈杂,统一数据库访问入口的需求非常迫切。
  • 读多写少,想快速实现读写分离,但又不想在每个服务的代码里各自维护数据源路由逻辑。
  • 数据库流量持续上涨,希望在不改动业务代码的情况下,通过加只读节点横向扩展读能力。
  • 准备做分库分表,但是团队人力有限,不想在应用层引入复杂的 SDK 改造,希望通过透明代理方式先把分片路由跑起来。

一句话总结:代理适合那些希望“架构演进平滑一点、业务代码改动少一点、运维手段统一一点”的团队。

2. 核心设计思路与架构拆解

确定要用代理之后,下一个问题就是:这套系统到底该怎么设计?我见过不少团队一上来就兴奋地定方案,结果过一阵子就被各种边缘情况折磨。这里分享几个核心设计思路,都是我实测下来觉得好用的。

2.1 路由层的核心职责

代理最核心的能力,是对 SQL 的解析和路由。

SQL 解析并不是要把 SQL 变成一棵完全语义化的 AST,那太重了。更务实的做法是做到“够用级别”的解析:判断语句类型、提取涉及的表名、找到分片键的值、识别是否带事务、判断是否只读。真正在线上跑的时候,代理的解析性能直接决定了你能扛多大流量。

举个例子,用户表user_info按照user_id分成了 16 个库。应用发来一条SELECT * FROM user_info WHERE user_id = 123456,代理需要从WHERE条件里精确提取出user_id = 123456,然后通过哈希函数或取模规则算出它应该落在哪个分片上,最后把 SQL 原样转发到目标分片执行。如果 SQL 里的查询条件不带分片键呢?比如SELECT * FROM user_info WHERE nick_name = 'foo',代理就得对所有分片发起查询,然后做结果合并,这种全路由查询性能是相对差的,需要在路由规则设计阶段尽量规避。

这里有一个容易被忽略的点:路由规则的选择,必须和业务查询特征强绑定。如果业务大多数查询都是按用户维度走的,那用user_id做分片键肯定没问题;但如果你时不时还要按订单维度、按商家维度去查,就需要在分片键之外设计二级索引或者额外维护映射表。说白了,分片路由不是 DBA 拍脑袋定的,而是业务方和 DBA 一起对着慢查询日志一点点捋出来的。

2.2 读写分离与一致性怎么权衡

读写分离看着简单,实际上坑很深。最大的坑是主从延迟带来的数据不一致。

主库写入了一条记录,从库因为复制延迟还没拿到数据,你的查询请求恰好被路由到了从库,结果查不到。这种问题在写后立即读的场景下尤其明显。比如用户下单后立刻跳转订单详情页,如果这个详情查询被路由到了从库,用户就可能看到“订单不存在”的诡异报错。

一个比较实用的处理方式,是“写后读”场景强制走主库。怎么判断?常见做法是让应用在关键写操作之后,在请求头上带一个ROUTE_TO_PRIMARY的标记,代理看到这个标记就把接下来的一两条查询强制路由到主库;或者在事务内部使用的连接,随后的查询继续沿用主库连接。这个逻辑听起来简单,但是很多团队在实现时才意识到:他们连“写后读”的上下文标识都没有,只能一刀切全部走主库,导致只读节点形同虚设。所以早期设计方案的时候,一定要给代理留出这种细粒度路由控制的接口,别让业务侧只能靠数据库账号、只能靠 IP 白名单来区分。

还有个细节,就是主从延迟监控。只靠SELECT NOW()这种手段,测到的延迟往往不够准确,最好在代理层对复制心跳表做高频率采样,一旦延迟超过阈值,就暂停将查询分发给对应从库。这个策略我后来一直沿用,能挡掉不少偶发的一致性事故。

2.3 分片规则与扩容策略

分片是分布式数据库代理里最“硬核”的部分。分片规则设计得好,三年不用折腾;设计得不好,扩容的时候哭都来不及。

常见的分片模式无非三种:范围分片、哈希分片、基于目录表的分片。范围分片(比如按创建时间范围、按 ID 区间)的好处是查询局部性好,方便批量扫描,但容易产生热点。哈希分片(比如对分片键取哈希再取模)能打散数据流量,但扩分片的时候要重新分布数据。目录表分片就是额外维护一张路由表,灵活性高,但多一次查询、也要求目录表自身的高可用。

我个人在大多数场景下更倾向于哈希分片,尤其是对用户维度、业务方维度的切分。这里有个很关键的实操点:不要直接用user_id % 16这种方式,因为一旦后续要把 16 个分片扩到 32 个,user_id的取模结果会全部变化,数据迁移量几乎等于重来。更稳妥的做法是引入虚拟分片(vBucket)概念,例如把 16 个物理分片映射到 1024 个逻辑分片,每条数据先通过哈希落到某个逻辑分片,再由逻辑分片与物理分片的映射表决定最终位置。这样扩容时只需要调整部分逻辑分片的物理映射,迁移的数据量可以控制在很小的比例。

这一点强烈建议在一开始就考虑进去。因为一旦上线跑起来,线上数据量大到一定程度,任何“重分布”的成本都高得吓人。宁可前期设计复杂一点,也别给自己埋“全量迁移”的雷。

2.4 高可用与故障转移的几种做法

代理本身不能成为单点。否则数据库高可用做得再好,代理一挂,应用照样全挂。

业界常用的做法是代理节点多活部署,前面挂一层负载均衡(比如 LVS、负载均衡器或者域名解析轮询)。代理节点之间要做到对后端数据库拓扑状态的共享,通常通过一个强一致性的协调组件(比如 etcd 或类似服务)来维护。主库发生切换时,所有代理节点需要几乎同时感知到新主库的地址,并立刻把写流量切到新主库。

这里值得说的是,故障转移不是“检测到主库失联”就算完事,还要考虑数据一致性的边界。比如主库和从库之间复制延迟比较大,主库突然宕机,新提升的从库可能缺了最后几秒的 binlog,那这几秒的写入就需要确认是否需要补偿。代理层不能假装没看见,直接切过去就完了。一个稳妥的做法是:在做主从切换决策之前,先比较各节点的日志位点,尽量让新主库具备最新的数据;同时,对于确实丢失的数据,通过告警机制暴露出来,让业务方核对补偿。

还有一个很多团队会忽略的点:代理层的探活和健康检查,不能只看“进程活着”,也不能只看“端口能连上”,要看数据库真正能不能执行查询。我见过有代理配置了 TCP 层面的探活,后端数据库已经处于假死状态了,代理还继续把流量往它那儿送,结果业务侧一堆超时。健康检查最好是执行一条轻量的SELECT 1,并且加一点随机延迟,避免所有代理节点同时发起探测、把一台濒危数据库给探得彻底崩溃。

3. 工具选型:没有银弹,只有取舍

分布式数据库代理的方案,市面上其实分成好几条路线。每条路线都有明确的设计哲学和适用边界。我自己接触过不少团队,也看过他们在不同路线之间的摇摆,这里说一说我的选型判断。

3.1 透明代理型:SQL解析与转发

透明代理型是最“正统”的数据库代理形态。它独立部署,对应用完全透明,应用不需要改代码、不需要引入 SDK,只要把数据库连接地址指向代理即可。它和数据库协议深度打交道,能解析 SQL、做读写分离、分片路由、结果集合并。

这类方案的优势是对业务侵入极小,数据库连接串改一下,大多数代码零改动,适合存量业务、技术栈复杂的场景。劣势也比较明显:因为要做 SQL 解析,对复杂查询、多表 JOIN、子查询的支持需要持续打磨;而且多了一层网络转发,性能上会有一些损耗,需要做好连接复用和内核态优化。

如果是这种情况,我建议在选型时重点考察它的 SQL 兼容度、分片聚合能力、以及维护社区的活跃度。拿不太准的时候,可以用你线上最复杂的那批 SQL 先去测一遍,看哪些能正确转发、哪些会报错,这个结论比看任何宣传文档都有用。

3.2 协议层负载均衡

另一类方案是协议层负载均衡,比如好多团队会在数据库前面放一个基于四层代理的负载均衡器。这类方案本身不做 SQL 解析,也不懂什么是分片,它做的事情就是按照指定策略(轮询、最少连接数、一致性哈希)把连接分发到后端不同的数据库实例。

这类方案和透明代理型有什么区别?很简单:协议层负载均衡适合“后端是多台完全对等节点”的场景,通常用于独立的只读从库集群,或者在数据库本身已经做了高可用组的情况下,为不同角色提供流量入口。它也能做故障摘除、端口转发,但它不理解 SQL 语义,没办法做“读走从、写走主”这种智能路由。

我会把它和透明代理型结合使用:底下一层用协议层负载均衡保证数据库入口的稳定接入,上面再用透明代理做 SQL 级的路由决策,各干各的活。

3.3 客户端SDK方案与代理的差异

还有一种路线,是把分片和读写分离能力以 SDK 的方式集成到应用里。应用在启动时从注册中心获取数据库拓扑,在代码运行时直接解析 SQL、主动路由到不同的分片。

SDK 方案的优势是性能最好,不需要中间一次网络转发,也容易实现更灵活的路由策略,比如根据上下文自己决定走主库还是走从库。但它的问题很突出:每一个业务服务的代码都要改造,都需要集成这个 SDK;每一个新来的开发都要理解分片规则,否则容易写出一堆全分片扫的 SQL;一旦拓扑变更,所有服务要联动发布更新。

这个方案在多团队、多语言、多技术栈的公司里,推进阻力特别大。比如不同语言的团队,就得维护不同语言版本的 SDK,这个成本容易被低估。我个人比较倾向于:新业务、小团队、技术栈统一的场景可以用 SDK;存量系统多、团队边界清晰的场景,用代理更省心。

3.4 按团队情况怎么选

选型这件事,最终还是得结合团队实际。我给自己总结了一个很简单的决策框架,分享出来供参考:

  • 如果团队没有专职 DBA、运维能力比较弱,更推荐透明代理型,因为部署和管理相对集中,配套功能也完善,业务侧改动少。
  • 如果团队里面有很强的数据库内核背景,而且业务高度定制,推荐走 SDK 方案或者自研代理,能把性能压榨到极致。
  • 如果只是单纯想解决读写分离的小问题,后端就两三台机器,那可以先上协议层负载均衡,等规模扩大再做升级。
  • 如果公司内部已经有比较完善的基础设施,比如注册中心、配置中心、监控体系,选型时可以优先考虑能和现有体系打通的方案,避免又造一套轮子。

我的习惯是,不管最后选哪套,都要先做一轮完整的压测和故障演练,把读写比例调到接近线上水平,把后端节点故意搞挂几次,看看代理层的行为是否符合预期。纸上谈兵永远不如实战一次。

4. 实操:从零部署一套可用的代理集群

理论聊得再多,不如动手实操一遍。这里我用一个模拟的订单系统(以下简称“模拟项目X”)来演示,怎么从零部署一套带读写分离和分片能力的代理集群。整个流程是我实际操作过、验证过能跑通的,你可以把它当成一份“抄作业”的案例。

4.1 环境与拓扑规划

模拟项目X的订单表数据量增长很快,单库快吃不消了。我规划的拓扑如下:

  • 应用节点:3 个无状态服务实例,连接代理地址。
  • 代理节点:2 个实例,前面用负载均衡器做虚拟入口。
  • 后端数据库:订单库按用户 ID 拆成 2 个分片库,每个分片库各带 1 个只读从库。

具体样例如下:

角色地址说明
负载均衡入口192.168.10.100:3306应用真正连接的地址
代理节点1192.168.10.101:3307接入节点
代理节点2192.168.10.102:3307接入节点
订单分片1主库192.168.20.11:3306存放 user_id 哈希取模结果为 0 的数据
订单分片1从库192.168.20.12:3306分片1只读副本
订单分片2主库192.168.20.21:3306存放 user_id 哈希取模结果为 1 的数据
订单分片2从库192.168.20.22:3306分片2只读副本

为什么要用负载均衡入口,而不是让应用直连某个代理节点?因为代理节点属于无状态接入层,任何一个挂掉,只要入口还在,应用就不需要感知变化。这一层带来的可用性提升很值。

4.2 代理配置拆解

下面是这个模拟项目里,代理配置的一个简化示例。不同产品的语法会有差异,但核心逻辑相通。

# 模拟代理服务核心配置片段 proxy: listen: 0.0.0.0:3307 proxy-address: 192.168.10.100:3306 # 对外暴露的虚拟入口标识 dataSources: ds_0: url: jdbc:mysql://192.168.20.11:3306/order_db username: proxydb password: "******" connectionPoolSize: 50 ds_0_slave: url: jdbc:mysql://192.168.20.12:3306/order_db username: proxydb password: "******" connectionPoolSize: 50 ds_1: url: jdbc:mysql://192.168.20.21:3306/order_db username: proxydb password: "******" connectionPoolSize: 50 ds_1_slave: url: jdbc:mysql://192.168.20.22:3306/order_db username: proxydb password: "******" connectionPoolSize: 50 rules: # 分片规则 sharding: tables: t_order: actualDataNodes: ds_${0..1}.t_order tableStrategy: standard: shardingColumn: user_id shardingAlgorithmName: user_id_mod shardingAlgorithms: user_id_mod: type: HASH_MOD props: sharding-count: 2 # 读写分离规则 readwrite-splitting: dataSources: ds_0_group: writeDataSourceName: ds_0 readDataSourceNames: - ds_0_slave loadBalancerName: round_robin ds_1_group: writeDataSourceName: ds_1 readDataSourceNames: - ds_1_slave loadBalancerName: round_robin

这个配置里有一个迭代过程中非常关键的坑,我要特意说一下:我一开始把读写分离和分片配置成了两层独立的规则,结果发现代理在“先分片后读写分离”还是“先读写分离后分片”的优先级上表现不一致。后来我把读写分离的数据源粒度定义在分片之下,也就是每个分片各自集成一组主从数据源,然后代理对第一个分片的路由决策会同时完成“该去哪个分片”和“该去主库还是从库”两步。这个“数据源组织方式”的问题,和代理的理论模型强相关,建议在实际配置前先看看文档对“分组”的定义,别想当然。

4.3 业务接入与路由验证

配置完成、代理节点启动之后,应用接入就比较简单了,只需要修改数据库连接配置:

spring: datasource: url: jdbc:mysql://192.168.10.100:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai username: proxydb password: "******" driver-class-name: com.mysql.cj.jdbc.Driver

应用层连接的是负载均衡入口,代理层会根据 SQL 和配置自动路由。接入完成后,我做了一轮路由验证,核心思路就是通过代理执行 SQL,然后确认请求真正落在了哪个后端节点。

验证手段主要靠后端数据库的general_log,或者靠代理侧的 SQL 路由日志。比如我执行一条:

SELECT * FROM t_order WHERE user_id = 1001;

查代理的路由日志,看到的预期结果是:这条 SQL 被路由到ds_0_group,并且因为它是查询语句,被分发到ds_0_slave节点。如果我执行:

INSERT INTO t_order (order_id, user_id, amount) VALUES (5001, 1001, 99.00);

预期结果:被路由到ds_0_group的主库ds_0,不会走到从库。

这种验证一定要在业务接入早期做透,别等到线上流量打进来才发现路由规则不对。我有一个比较“笨”但很有效的方法:直接把三个后端节点的general_log打开,然后观察一小段时间,看实际流量的分布是否符合预期。如果发现某个从库一条流量都没有,那大概率是路由规则或者负载均衡策略有问题。

4.4 故障转移演练

光验证正常路由还不够,故障转移演练是我每次必做的环节。

我特意在业务低峰期,用运维平台对ds_1主库执行了“模拟宕机”操作。预期的代理行为是:探测到主库连接异常,触发主从切换,把ds_1_slave提升为新主库,并且后续写操作自动转发到新主库。

演练过程中我关注几个指标:

  • 从触发故障到代理完成切换,中间隔了多少秒。
  • 切换期间,有多少请求报错、多少请求重试成功。
  • 业务侧的监控能不能快速感知到这次切换。
  • 原本的ds_1主库恢复之后,会不会发生“双主”冲突。

那次演练得到的经验是:代理默认的探测间隔和失败重试次数都偏保守,如果不调参,切换时间会比预期长不少。我后来把健康检查间隔从原来的 5 秒调到了 2 秒,把失败判定阈值从连续 3 次失败降到 2 次,切换速度提升了 60% 左右。不过也不能太激进,否则后端数据库一有轻微抖动,代理就频繁切换,反而更伤。这个“度”需要在真实环境里多试几次。

另外一个容易被忽略的细节:应用侧的连接池对故障转移的感知同样是关键。如果应用拿了一个断掉的连接还继续用,应用自身不重试,代理切换再快也白搭。所以接入代理之后,应用侧也需要把连接池的testWhileIdle、testOnBorrow这类参数打开,确保每次从池里取出的连接都是可用的。

5. 踩过的坑与排查实录

这部分我打算写成一份“避坑速查表”,每一个都是我或者身边的团队实际踩过、并且最终找到解决办法的问题。

5.1 连接池被占满,代理成了新的瓶颈

当我第一次把几十个应用连接全部收敛到代理上时,出现了完全没预料到的情况:代理本身的连接池被打满了。

原因是很多应用侧连接池的初始化配置是“最小连接数就 20、最大 50”,也就是说,代理要为每个应用维护到后端的连接。应用数量一多,代理连接后端数据库的连接数量也会爆炸。代理还要做 SQL 解析、路由计算,结果 CPU 飙高,延迟上升。

排查思路其实不复杂:先看代理到后端每个数据源的活跃连接数分布,再看是不是有慢 SQL 把连接长期占住。核心解法是两层:一是合理设置代理侧到后端数据源的连接池上限,不要让代理无条件为应用创建连接;二是应用侧连接池也要收敛,很多业务在最开始根本不需要几十个连接,十几个就绰绰有余。

这个坑的根源其实是“连接收敛”这件事远没有表面看到的那么简单,需要把应用端连接池和代理端连接池当成一个整体来调优,而不是各自为战。

5.2 事务跨库导致数据错乱

分片环境下,事务是最难搞的问题之一。早期我把事务想得太简单,觉得代理会在后台帮忙处理跨分片事务,结果上线后发现部分场景出现数据不一致。

最典型的问题是这样的:一个订单服务里,代码同时更新了t_order和t_order_item,但两条数据的分片键算出来落在不同的分片上。代理没法感知一次数据库会话里的事务上下文,它只能把每条 SQL 路由到对应的分片,结果这个事务被拆成了两个独立的事务,要么一个成功一个失败,要么两个都成功了但全局状态不一致。

我后来的处理办法是,业务在设计分片规则的时候,尽量让“需要事务一致性”的数据落在同一个分片,比如t_order和t_order_item都用user_id做分片键,并且保证同一用户的订单和订单明细一定进同一个库。如果确实需要跨分片强一致,那就不要指望代理透明化处理,必须引入分布式事务方案(比如基于消息最终一致性的柔性事务),并且业务代码做出明确取舍。

代理不是银弹,它能帮你解决路由问题,但没法帮你把“事务边界”变成“全局事务”,这一点必须写进团队的架构设计文档里。

5.3 分页排序结果不对

另外一个很隐蔽的坑是跨分片的分页排序。

假设t_order分成了 4 个分片,你要查某个用户分页第 3 页、每页 20 条、按创建时间倒序。代理的做法是把 SQL 下发到每个分片,各取 60 条(不是只取第 3 页的 20 条),然后在代理层做内存归并排序,最后返回正确的第 3 页。看起来没问题,但如果你没有明确告诉代理“排序键 + 分页大小”,代理的默认行为可能是每个分片只取 20 条,然后合并排序,这样结果就错了。

我排查这个问题的过程也很有意思:业务反馈“某一页多了一条数据,下一页少了条数据”,一开始还以为是缓存问题,后来抓出代理的路由日志才发现,代理把ORDER BY create_time DESC LIMIT 40, 20透传给每个分片执行,每个分片执行时先按自己的顺序取 40 条偏移量,然后在代理层合并时,这个“偏移量”语义就完全错了。

正确做法是,要么在代理层明确开启“分布式分页”优化,要么业务侧把分页的 SQL 写得更加可控。我的经验是,像这种深分页的场景,最好改成“基于游标”的分页,比如WHERE create_time < 上次页最后一条的 create_time ORDER BY create_time DESC LIMIT 20,这样代理下发给每个分片时语义都比较清晰,聚合后的结果也更容易准确。

5.4 扩容时的数据迁移

最后说说扩容。前面提到了虚拟分片的设计,这里我用一个实际案例补充说明。

模拟项目X的订单库最初是 2 个分片,半年之后数据量到边界,要扩到 4 个分片。由于我一开始用了逻辑分片和物理分片映射,扩分片时只需要把逻辑分片中属于新物理节点的那部分映射关系替换掉,然后把对应数据从原物理节点迁移到新节点,再切换映射、清理旧数据。

这个过程中最容易出错的地方是“迁移窗口”的一致性保证。我是用数据同步工具先把存量数据同步过去,再通过增量订阅追上最新变更,最后在低峰期做一个短暂的只读锁库,确认新老数据一致后切换映射。如果不用逻辑分片,直接user_id % 2改成user_id % 4,那所有数据的位置都变了,迁移量直接翻倍不说,业务还要停机配合。所以,还是那句老话:前期设计多花点心思,后期运维少掉很多头发。

5.5 一份常见问题速查表

现象可能原因关键排查步骤推荐解法
代理连接被占满应用连接池过大或慢 SQL 堆积观察代理到后端的活跃连接数分布应用侧限流、代理侧限池,全局统一调优
写入后立刻查询无数据主从延迟查看复制延迟指标写后读场景强制路由主库
部分 SQL 报错代理解析兼容性不足抓取路由日志定位失败 SQL改写 SQL 或升级代理版本
故障切换慢健康检查间隔过长查看切换耗时监控调短探测周期,但不要过度激进
跨分片事务数据不一致事务被拆分审查分片键和事务边界将相关表按相同分片键分片,或用柔性事务

6. 最后分享一点自己的经验体会

分布式数据库代理这个组件,我做了几年之后最大的感受是:它不是一个“装上就能用”的插件,而是一个需要持续调优、持续观测的基础设施。

从接入前的分片键规划、读写分离策略,到接入后的连接池调优、故障演练、容量规划,每一个环节都需要提前想清楚。代理真正发挥价值的时间点,往往不是上线那一刻,而是半年后、一年后,当业务量增长、当数据库节点拓扑变化、当你发现业务代码完全没有因为扩展而改动时,你才会意识到“这层代理当时设计得真值”。

如果你现在正处在单库快要扛不住的阶段,我的建议是:先别急着分库分表,先从读写分离和连接收敛开始,用代理把流量治理能力补上。等数据量确实大到读写分离也救不了的时候,再沿着代理的扩展路径,按既定规划去拆库。这样每一步的代价都更小,业务也更稳。

最后再给一个切身建议:任何代理方案,上线前都要做一次完整的故障演练,最好连“代理节点全挂”这种极端场景也模拟一下。因为你永远不知道,线上环境会在哪一刻给你来点“意外惊喜”,而提前演练过,至少心里有底。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询