问题:分库后,就有多个数据源需要,dbproxy 对机器做代理,一般需要lvs/f5 等手段来实现流量的负载均衡,跨机房可能需要dns分发,例如 mycat 阿里的主键。
就这个问题通过一问一答的方式解答
什么是 dbproxy,为什么要用 dbproxy?
dbproxy(数据库代理)可以理解为在应用层与数据库之间插入的一层代理服务,它的主要作用是“屏蔽后端数据库的复杂性”,对上层提供一个统一的访问接口。
屏蔽数据分片(sharding)的细节:
当数据库做了分库分表(sharding)之后,后端往往会出现多台数据库实例、多套数据源。
如果没有代理层,应用需要自己知道每条 SQL 或者每个主键应该访问哪一台数据库实例。
这样应用层会变得非常复杂,而且维护成本高。
统一的连接管理:
在没有代理的情况下,应用可能需要和多台数据库都建立连接,而且应用可能会开启大量连接,会导致数据库压力上升或者网络开销变大。引入代理层后,可以集中管理连接池,并对数据库连接进行复用、路由等操作。
读写分离、负载均衡:
如果有读写分离需求(主库写、从库读),dbproxy 可以帮我们根据 SQL 类型(读/写)自动路由到不同的数据库实例。此外,如果有多台从库,需要做负载均衡时,也可以在代理层实现。
此外,MyCat、阿里 DRDS(Alibaba 的分布式关系数据库服务)、ShardingSphere 等其实都是常见的分库分表中间件或数据库代理的一些解决方案。
为什么分库就需要多个数据源?
所谓“分库”,可以理解成将原本在单一数据库中的数据,按照一定的规则(比如按照用户 ID 范围、订单 ID 哈希等)拆分到多个数据库实例中。这样做的目的是:
水平扩展:单库的容量或性能达到瓶颈后,将数据分到多台机器可以提高整体吞吐量。
分摊压力:多台数据库服务器分担请求,能够缓解单库/单机的压力。
但是分库带来的一个直接后果就是后端出现了多个数据源:
数据源 1(库 A)
数据源 2(库 B)
…
数据源 N(库 N)
对应用来说,就不是只连一个“数据库”,而是多个。所以应用需要知道——“我这条 SQL 到底要去哪一个库执行?”。这时如果缺少一个统一的代理或中间件,应用就得自己写很多逻辑来进行路由判断:
if (用户ID % 2 == 0) {
走 数据源A
} else {
走 数据源B
}
而且当分库策略或者分库结构发生改变时,又得改业务逻辑。
所以大部分情况下,会加一个 dbproxy 或类似的中间件,把多库对应用‘伪装’成一个逻辑库,由它来做路由、聚合操作等。
为什么需要 LVS / F5 等做负载均衡?
假设你的 dbproxy 部署了多台实例,或者你的数据库实例本身也有多台,需要对外提供统一的访问地址。这时一般会需要一个负载均衡层,比如:
LVS(Linux Virtual Server):内核层级的负载均衡转发,性能高;
F5:硬件负载均衡设备,很多大公司可能用来做专业的流量调度。
它们可以提供一个虚拟 IP 或者域名,对外只暴露一个地址,而内部则把请求按一定的策略分发到真正的 dbproxy 或数据库后端。这样:
如果某个实例挂了,负载均衡可以自动摘除,保证服务的高可用。
可以让多个实例并行,达到横向扩展的效果。
为什么跨机房可能需要 DNS 分发?
当你的应用或服务需要跨机房(甚至跨地域)部署时,网络拓扑就更加复杂。常见有两种层面的流量调度手段:
在网络或四层/七层负载均衡上做流量转发:
比如同一个业务在北京、上海、广州都有机房,那么应用流量先走到一个全局的负载均衡设备或 GSLB(Global Server Load Balancing),再根据 IP 或者距离、延迟等策略把流量引导到就近的机房。
DNS 级别的负载均衡(域名解析层面):
比如你配置一个域名 db.xxx.com,通过 DNS 轮询或者智能 DNS 的方式,自动把请求解析到离用户最近或健康的机房 IP。
常见的做法叫做 GSLB + DNS 方案,比如一些云厂商的 “智能 DNS” 或者 Akamai、Cloudflare 的 GSLB 服务,都能帮你做全局流量调度。
好处是:不需要手动去改 IP,用户通过域名访问,不同区域的解析得到不同的 IP,可以就近接入,提高访问速度,降低延迟,而且当某个机房出现故障时,DNS 解析可以切换到其他机房 IP。
所以,跨机房的负载均衡往往涉及到 DNS 层面的全局调度(GSLB),并配合一定的健康检查或路由策略,使得应用或客户端能够快速接入最近且健康的机房。
关于主键冲突等问题?
分库分表之后,为了保证每台库中的主键(或 ID)全局唯一,通常需要在代理层或者ID 生成服务进行处理。例如:
雪花算法(Snowflake),保证在分布式场景中生成的 ID 全局唯一,而且还能保证有一定的递增趋势。
或者 MyCat/DRDS 自带一些主键策略,像 全局自增主键、多列联合主键 等方式,避免分片数据库中的主键冲突。
这类中间件/代理层也会在一定程度上统一管理或生成主键,屏蔽后面多数据源的冲突问题。
常见的全局唯一主键生成方案?
数据库自增序列/表
通过某个中心数据库表或者序列来获取全局递增的 ID。
缺点:中心化依赖,性能和可用性受限,单点故障风险。
UUID
系统随机生成 128 bit(通常存储为 36 字符的字符串)的标识。
优点:本地生成,无中心依赖。
缺点:存储空间大(影响索引性能),不适合有序索引,难以根据 ID 做路由分片。
Snowflake/雪花算法(Twitter 提出的经典算法)
64 位长整型ID,通常结构是时间戳 + 机器ID + 序列号。
优点:按时间递增,大多是本地生成(需正确配置机器ID、时钟等),性能好,索引有序。
缺点:需要分配 workerId,时钟回拨(时间不同步)会带来复杂度。
号段(Segment)/号段缓存(Leaf)方案
利用数据库的自增列或序列,一次分配一段区间(比如 1000 个ID的区间)给某个节点,节点本地生成 ID。
优点:减少数据库频繁访问,提升性能。
缺点:依赖号段中心库,遇到数据库故障时要有兜底策略。
Redis 原子自增
借助 Redis 的 INCR 命令进行分布式递增;
优点:性能高、实现简单。
缺点:Redis 挂了影响可用性,需要持久化或主从架构保证可靠性。
分布式协调(Zookeeper)
通过 ZK 的顺序节点特性生成全局有序 ID。
缺点:并发高时性能瓶颈明显,不是高并发场景的最佳选择。
实际生产中,雪花算法和号段方案是非常常见的选择。
具体实现细节:以雪花算法为例
雪花算法基础
Snowflake 算法生成的 64 位长整型,通常格式如下(不同实现可能略有差异):
| 1 bit 符号位 | 41 bit 时间戳(毫秒)| 10 bit 工作机器ID | 12 bit 序列号 |
符号位 (1 bit):通常是 0,不用。
时间戳 (41 bit):可以支撑约 69 年的毫秒级时间。
机器ID (10 bit):可以表示 1024 台机器/进程。
序列号 (12 bit):同一毫秒内可生成 4096 个 ID。
生成过程:
取当前时间戳(毫秒级),减去一个自定义起始时间(epoch),得到相对毫秒数。
拼接机器ID 和当毫秒内的序列号。
得到 64 位整形数字(在 Java 中通常是 long 类型)。
机器ID分配
通常会在配置中心或注册中心(Zookeeper、Nacos、Apollo 等)配置每台实例的唯一机器ID。
避免重复机器ID,否则会造成 ID 冲突。
处理时间回拨
如果因为服务器时间突然往回跳,会导致 ID 重复。
一种常见策略:
如果检测到当前时间小于上一次生成 ID 的时间,则等待或直接抛异常,让调用方进行重试;
或在机器ID 上再加一层备用标记,用以覆盖临时回拨(但实现较复杂)。
ID 的数据类型、在数据库如何存储?
无论是雪花算法、号段方案还是其他方式,最终在数据库里通常会用BIGINT类型(MySQL 等常见数据库都是 64 位整型)。为什么?
足够大:64 位可以容纳相当大的数字(9,223,372,036,854,775,807)。
索引效率:数字类型做主键索引效率更高(相比字符串),也有天然顺序(Snowflake 生成的ID大体单调递增)。
通用性好:大多数语言里都有 64 位整型类型(long 类型等)
CREATE TABLE user_order (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2),
create_time TIMESTAMP,
…
);
在插入数据时,应用侧(或中间件)已经生成好了全局唯一的 ID,直接插入即可。
分库分表/跨机房场景下的路由分片?
基于 ID 的分库分表
常见做法是基于 ID 取模(Hash)或区间进行路由。例如:
Hash 路由:库索引 = (id % 库数),表索引 = (id % 表数) 等;
区间路由:如 0 ~ 9999999 存在库0,10000000 ~ 19999999 存在库1,等等。
如果使用雪花算法,往往取其中的低位来做 id % n,这样分散度较好(也要注意避免某些位都是 0 导致不均衡)。
注意:如果将来要扩容分库数,从 2 库扩展到 4 库,ID 取模算法也要做迁移或映射,否则会导致数据不在预期的库中。可以考虑使用一致性哈希或中间件提供的线上扩容功能。
基于 ID 分配机房(跨机房存储)
如果每个机房都存储不相交的数据片,通常也可以通过取模或区间来决定某条数据应该进哪个机房。
例如:id % 2 == 0 存在北京机房,id % 2 == 1 存在上海机房。
或者更精细的分段,比如 ID 前 X 位代表机房编号,这在定制的雪花算法实现里也可以做到(将机器ID 部分的一部分位数当作“机房ID”+“机器ID”)。
但是这种做法会导致“跨机房 JOIN/查询”难度较大。如果一个用户的数据需要经常跨机房访问,就要考虑多机房同步或“同城多活”方案,复杂度会增加。
具体举例:MyCat / ShardingSphere / DRDS?
一些常见的分库分表中间件内部也会内置 ID 生成器,或者可以集成第三方 ID 生成服务,以实现全局主键:
MyCat
提供了多种主键策略,比如自增 ID、UUID、前缀自增等,但很多团队会结合雪花算法自己生成,然后写到 MyCat 路由。
路由规则可自定义(Groovy 表达式等)。
ShardingSphere
可以配置内置的雪花算法生成器 (Snowflake) 或者你自定义的算法;
在 sharding 配置中指定 keyGenerator 的实现类;
还可以定义分片算法(standard/inline/hint 等),在 inline 中写 t_order_$->{order_id % 4} 之类的表达式。
阿里 DRDS / PolarDB-X
本身也提供全局自增 ID和雪花算法两种模式,可以在控制台上进行配置。
如果使用雪花算法,ID 会不会重复?
只要每台机器的 workerId 唯一,时间戳不回拨,序列号在同毫秒内不溢出,就不会重复。
时间回拨可以通过“等待”或“抛异常重试”来处理。
ID 是不是严格递增?
雪花算法是趋势递增,同一毫秒内生成多个 ID,后者序列号更大,毫秒切换后则跳到新的时间区间。但由于多线程并发或多机并发,全局的顺序性不一定 100% 按数值递增,只有单机单毫秒内是顺序的。
如果要绝对顺序,可以用数据库序列或 ZK 顺序节点,但要考虑性能。
ID 占用多少存储空间?
64 位整型在 MySQL 中一般是 BIGINT (8 字节),相比 32 位INT 多 4 个字节,但是在实际生产中一般都能接受。
修改分库分表策略,ID 取模规则怎么变?
需要迁移数据或使用中间件提供的在线扩容功能;
在早期设计就要考虑可扩容,比如使用 “虚拟桶” 思路(先对 ID 取模 128,再把桶映射到实际库表),扩容时只需改变桶与库表的映射关系,减少大规模数据迁移。
跨机房多活场景,主键还会冲突吗?
如果每个机房都用各自的雪花算法,但机器ID 没做好区分,可能出现冲突;
需要为不同机房分配不同机房ID段,然后再在机房内部用机器ID + 序列号;
或者用统一的中心化 ID 服务(注意多活容灾)
订单表量很大,会不会因为主键太大影响性能?
通常使用雪花算法或递增 ID,都能让主键在InnoDB聚簇索引中大致有序插入,插入性能较好;
如果索引随机性大(如 UUID) 反而会导致数据页分裂、性能下降。
在分布式事务下 ID 是怎么处理的?
通常先拿 ID(本地生成或分布式 ID 服务),再进行插入,这样不依赖全局事务;
避免了“插入时再获取自增ID”,减少锁竞争和分布式事务带来的性能问题
公司订单量爆炸增长,单库性能不足,如何拆分数据库?
先做读写分离,缓解读压力。
如果还是不够,进一步垂直拆分(用户、订单等分不同数据库),再进行水平拆分(根据订单ID或用户ID哈希拆分)。
引入分库分表中间件(MyCat、ShardingSphere)或云厂商 DRDS/PolarDB-X,实现自动路由、主键生成。
同时考虑搜索、统计类场景,可能要引入ES、大数据平台等。
“为什么要用 MyCat / ShardingSphere,而不是自己在应用里写路由逻辑?”
避免业务代码与分库分表策略耦合,后期维护成本高。
中间件往往提供成熟的功能,如全局主键生成、分片路由、读写分离、分布式事务支持等。
便于在线扩容、迁移,减少对应用的改动。
问道 谈到多数据源 / 分库分表 / 负载均衡 / 跨机房 / DNS 分发 ?
业务背景:为什么要分库/分表,为什么要做负载均衡,为什么要跨机房。
技术实现:
分库分表中间件的核心功能(路由、读写分离、主键生成);
负载均衡(LVS/F5/HAProxy)的工作原理和如何部署;
DNS/GSLB 进行跨机房流量调度的思路。
关键挑战:分布式事务、数据一致性、主键冲突、迁移扩容、故障切换、DNS 缓存等。
实践经验:
是否有线上规模案例?遇到哪些坑,比如 SQL 分片错误、热点数据、跨机房延迟等。
如何进行监控与报警,比如 metrics、日志、慢 SQL。
扩展思考:
会不会继续往微服务、消息队列、大数据分析方向演进?如何结合 Spring Cloud 或云平台服务?
项目实践或线上真实数据(QPS、TPS、库规模、节点数、机房分布)
深挖底层原理,可以谈一些LVS 的四层转发机制 (DR/NAT 模式)、F5 处理 SSL 卸载、DB 中间件如何做 SQL 解析和执行计划等底层知识。
场景设计,你可以尝试描述如何保证当机房A挂了以后,机房B能够无缝顶上,涉及到数据同步、应用配置、DNS 切换、监控报警等全链路内容。