概述
系列定位:本文是"分布式数据架构与存储选型"系列的第二篇。在《分库分表架构选型:ShardingSphere vs Mycat vs DBLE》拆解数据水平拆分的核心技术后,本文将聚焦于应对读多写少场景的核心扩展手段------读写分离。我们将从"主从延迟 vs 读一致性"这一根本矛盾出发,系统解构客户端代理与服务端代理的架构差异,通过对 ShardingSphere-Proxy、MySQL Router、ProxySQL 与 MaxScale 四种主流中间件的深度内核拆解与可量化性能基准对比,帮助架构师与高级开发者建立从原理、选型到落地的完整决策框架。
文章组织架构
架构图说明:
- 总览说明:全文 8 大模块构成严密的逻辑链条。从基础概念与量化数据出发,先建立架构模式的认知,再深入四个中间件的源码级机制,然后通过可重复的性能基准和对比矩阵形成选型决策,最后串联前后文知识并沉淀为面试深度题库。
- 逐模块说明:模块 1 夯实理论根基,尤其是延迟量化;模块 2 对比两种代理模式的连接数、透明性、延迟处理差异;模块 3-5 逐一对中间件进行"架构-配置-机制-性能"四层拆解;模块 6 将所有数据汇聚为决策树;模块 7 强化系列文章关联;模块 8 以面试形式内化工程判断力。
- 关键结论:中间件选型的本质是权衡"透明性、读一致性、性能、运维复杂度"。ProxySQL 因 C++ 实现与主动延迟摘除机制成为性能与一致性兼顾的标杆;ShardingSphere-Proxy 凭借与 JDBC 配置完全互通和统一分片管理,在复杂分布式架构中展现出独特的生态优势。
1. 读写分离核心概念与决策前置条件
1.1 适用场景与读/写比例
读写分离并非普适性优化,其收益严重依赖于业务负载特征。只有当读/写比例 > 10:1 时,将读请求从主库卸载到只读副本才能显著降低主库 CPU 和内存压力,释放写入性能。常见的典型场景包括:
- 电商商品浏览(读 99%,写 1%)
- 内容管理系统(读 95%,写 5%)
- 报表与 BI 查询(读 99.9%)
同时,业务必须能够接受最终一致性:从库数据允许与主库存在一个可配置的延迟窗口。对强一致性要求极高的场景(如金融账户余额的即时读取),读写分离需要配合强制读主库策略。
1.2 核心矛盾:主从延迟与读一致性的量化分析
主从延迟是读写分离架构中最核心的工程挑战。其延迟量级直接决定"读从库"的可用性窗口。
1.2.1 异步复制延迟(50-200ms)
在默认的异步复制下,主库事务提交与 Binlog 事件写入不等待从库确认。延迟主要来自:
- 网络传输:Binlog 日志块从主库发送到从库的耗时。
- 从库 IO 线程写入 Relay Log:受磁盘 IO 能力影响。
- 从库 SQL 线程回放:单线程回放(或 MySQL 8.0 的 MTS 多线程回放)处理 DML 操作。
在 4C8G 虚拟机,千兆网络,sysbench oltp_read_write 基准负载 下,实测 Seconds_Behind_Master 分布:
- P50:50ms
- P95:120ms
- P99:200ms
1.2.2 半同步复制延迟(<10ms)
开启半同步复制(rpl_semi_sync_master_enabled=1)后,主库提交事务前必须等待至少一个从库将 Binlog 写入 relay log 并刷盘。延迟显著下降:
- P50:2ms
- P99:8ms 代价:主库写入 TPS 下降约 10-15%,因为增加了网络往返和从库确认等待。
1.2.3 监控指标
Seconds_Behind_Master:由从库 SQL 线程计算,值为当前时间 - 正在执行的事件时间戳。存在时钟不同步导致偏差的可能,但仍是主要参照。sys.x$innodb_lock_waits:间接反映从库回放压力。- GTID 差值 :更精确,对比
Executed_Gtid_Set与主库差异。
1.3 主从拓扑设计
- 一主一从:最简单,无读负载均衡,适用于读压力略高于写的场景,或作为数据备份。
- 一主多从 :核心读写分离拓扑。从库之间独立,通过中间件实现读负载均衡。需考虑从库权重:不同实例规格可分配不同权重。
- 级联复制:主库 → 一级从库 (Blackhole/InnoDB) → 二级从库。减轻主库 Binlog 发送压力,适合需要数十个从库的大规模读扩展,但整体延迟增大。
- 双主复制:互为主从,结合 VIP 或 keepalived 实现故障切换,一般不直接用于常规读写分离,避免写冲突。
1.4 与分库分表组合部署
在分库分表架构上叠加读写分离是大多数大型系统的终极形态。基本模式为:每个分片独立管理一主多从。
ShardingSphere-JDBC 内部采用两级路由引擎:
- 分片路由 :
StandardShardingStrategy根据分片键(如user_id)计算分片值,定位到逻辑数据源名(如ds_0)。 - 读写分离路由 :
ReadwriteSplittingDataSource将逻辑数据源ds_0映射为一个包含writeDataSourceName和readDataSourceNames的组合数据源,根据 SQL 类型选择物理连接。
这种设计使得每个分片的读写分离策略可以独立配置(如不同从库权重),且整个拓扑对业务代码透明。
1.5 读写分离架构演进图
先分片,后读写分离"] classDef default fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a
图 1 说明:
- 目的与场景:展示数据库架构从单点到读写分离再到分库分表的完整扩展路径,反映数据架构的两个核心扩展维度:读扩展与写扩展。
- 核心元素:单库无法区分读/写流量;一主一从实现读分流;一主多从通过中间件实现读负载均衡;分库分表+读写分离同时解决写容量与读性能问题。
- 数据流 :在最终形态中,一个
SELECT请求首先被分片算法路由到特定的逻辑数据源,再由读写分离引擎转发到该分片的一个从库。 - 工程实践:架构演进并非一步到位,而是由业务增长驱动。中间件选型需前瞻性地考虑未来叠加分库分表时的兼容性。
2. 客户端代理 vs 服务端代理架构深度对比
2.1 连接数模型与资源隔离
客户端代理模式(ShardingSphere-JDBC) 的连接数是爆炸性的。假设应用实例 100 个,4 个分片,每分片 1 主 2 从,则 MySQL 侧总连接数为: 100 * (4主 + 8从) * 连接池大小(如10) = 12,000 连接 这接近 MySQL 单实例的连接数上限(通常建议 <2000),即便分摊到 12 个实例,每个实例仍需承受 1000 连接,压力巨大。
服务端代理模式(ProxySQL/ShardingSphere-Proxy) 将连接收敛。应用仅需连接代理的少数端口,后端 MySQL 连接由代理统一管理: 代理实例数(如2) * (12 个后端) * 连接池大小(如20) = 480 连接 每个 MySQL 实例仅需应对数十个长连接,资源消耗极低。
2.2 主从延迟处理机制的本质差异
- 客户端代理 :无自动摘除能力。处理强一致性读完全依赖
HintManager。应用开发人员必须在代码中显式调用HintManager.getInstance().setWriteRouteOnly(),这是侵入式的。 - 服务端代理 :ProxySQL 和 MaxScale 具备后台 Monitor 线程,主动探测
Seconds_Behind_Master,实现无侵入的自动摘除。ShardingSphere-Proxy 处于中间态,无内置 Monitor,但可通过 DistSQL 动态摘除,需要运维自动化配合。
2.3 架构选型决策
| 条件 | 推荐模式 | 典型中间件 |
|---|---|---|
| 应用实例 <50,拓扑稳定,极致低延迟 | 客户端代理 | ShardingSphere-JDBC |
| 应用实例 >100,要求透明运维,需要连接收敛 | 服务端代理 | ProxySQL / ShardingSphere-Proxy |
| 需要最强主从延迟自动摘除 | 服务端代理 | ProxySQL |
| 已基于 ShardingSphere 分片,配置互通优先 | 服务端代理 | ShardingSphere-Proxy |
2.4 客户端代理 vs 服务端代理架构对比图
图 2 说明:
- 目的与场景:直观展示两种模式下连接拓扑、路由位置和网络跳数的根本不同。
- 核心元素:客户端代理路由逻辑在应用内,连接直连数据库;服务端代理以独立进程作为中间层,管理后端连接并复用。
- 数据流:客户端代理,SQL → 本地路由判断 → 直发 MySQL。服务端代理,SQL → 网络至代理 → 代理解析/路由 → 转发 MySQL。增加 1-3ms 网络延迟。
- 工程实践:服务端代理在连接收敛和运维透明性上优势巨大,尤其适合微服务大规模部署。客户端代理适合小规模或性能极致要求的场景。
3. ProxySQL 内核深度拆解
ProxySQL 是 C++ 编写的高性能、协议感知的 MySQL 代理,其设计围绕 规则引擎 和 后台监控 展开,是实现智能读写分离的标杆。
3.1 架构与线程模型
- Main Thread:处理配置重载、信号等。
- MySQL Threads:处理客户端连接与认证,将请求转发给 Worker 线程。
- Worker Threads:数量可配,负责解析 SQL、匹配规则、管理连接池、与后端 MySQL 通信。
- Monitor Thread :独立线程,周期性连接所有被监控的 MySQL 实例,检查
read_only、Seconds_Behind_Master、集群状态等,并更新内存中的 hostgroup 成员信息。
3.2 三层配置模型详解
ProxySQL 配置存储在 SQLite 数据库中,通过 Admin 接口(6032端口)管理,支持 在线热加载(LOAD ... TO RUNTIME),无需重启。
3.2.1 mysql_servers
定义后端 MySQL 实例的物理信息。
sql
INSERT INTO mysql_servers (hostgroup_id, hostname, port, weight) VALUES
(0, '192.168.1.10', 3306, 1), -- hostgroup 0 即写组
(1, '192.168.1.11', 3306, 1), -- hostgroup 1 即读组
(1, '192.168.1.12', 3306, 2); -- 权重为2,分配双倍读流量
3.2.2 mysql_replication_hostgroups
实现主从拓扑的自动发现和延迟管理。
sql
INSERT INTO mysql_replication_hostgroups (
writer_hostgroup, reader_hostgroup, check_type, comment,
max_latency_ms, check_interval_ms, check_timeout_ms
) VALUES (
0, 1, 'read_only', 'production_rw_split',
50, 2000, 500
);
max_latency_ms=50:任一从库Seconds_Behind_Master> 50ms 即被临时移出读组。check_interval_ms=2000:Monitor 每 2 秒检查一次延迟,快速响应。check_type='read_only':同时检查read_only变量,自动将read_only=OFF的实例分配到写组,ON 分配到读组,实现故障转移后的自动感知。
3.2.3 mysql_query_rules
核心规则引擎,基于正则、用户、schema 进行灵活路由。
sql
-- 规则1:SELECT FOR UPDATE 必须走写库,保证加锁读一致性
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (1, 1, '^SELECT.*FOR UPDATE$', 0, 1);
-- 规则2:所有 SELECT 查询默认分发到读组
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (2, 1, '^SELECT.*$', 1, 1);
-- 规则3:显式写操作走写组
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (3, 1, '^(INSERT|UPDATE|DELETE|REPLACE).*', 0, 1);
-- 规则4:特定业务查询强制读主库(如写后即查)
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply)
VALUES (4, 1, '^SELECT.*FROM order_info WHERE user_id=.*$', 0, 1,
NULL, NULL, NULL, 'Force read master for critical query');
-- 加载并持久化
LOAD MYSQL QUERY RULES TO RUNTIME; SAVE MYSQL QUERY RULES TO DISK;
设计意图 :规则按 rule_id 顺序匹配,apply=1 表示匹配成功后停止继续向下匹配。通过正则表达式的灵活组合,可以实现按业务、按表、按查询模式维度的精细路由控制。
3.3 Monitor 延迟感知路由机制
ProxySQL 的 Monitor 模块是保障读一致性的核心。其工作流程如下:
- 定期连接
mysql_servers中的所有后端,执行SHOW SLAVE STATUS或SHOW GLOBAL STATUS LIKE 'wsrep%'。 - 读取
Seconds_Behind_Master并与max_latency_ms对比。 - 若延迟超限,从
reader_hostgroup中删除该实例的成员记录;当延迟恢复,重新插入reader_hostgroup。 - 此过程对客户端完全透明,ProxySQL 在路由时直接从内存中的 hostgroup 成员列表选择,不会向延迟从库发送请求。
3.4 连接池复用与查询缓存
- 连接复用:客户端与 ProxySQL 建立连接(短连接/长连接均可),ProxySQL 在后端维护预先建立的持久连接池。当请求到达,从池中取出连接执行 SQL,执行完毕归还。极大降低了 MySQL 的线程创建/销毁消耗。
- 查询缓存 :可对高频
SELECT结果进行缓存,TTL 可配置,在极高读压力下进一步减轻数据库负载。
3.5 性能开销量化与数据来源
测试环境:
- 硬件:4C8G 虚拟机,千兆网络
- 软件:MySQL 8.0.32 InnoDB,ProxySQL 2.7.1
- 拓扑:1主2从,半同步复制
- 工具:sysbench 1.0.20
oltp_read_write,128 线程,100 表,每表 10 万行 - 测试时长:10 分钟,预热 2 分钟
结果:
- 直连主库 TPS:10,500
- 通过 ProxySQL TPS:7,800 (约 74%)
- 平均延迟增加:1.2ms
- 性能折损主因:网络一跳及 ProxySQL 的 CPU 消耗(解析 SQL、正则匹配)。C++ 实现下,GC 开销为零。
4. ShardingSphere-Proxy 内核深度拆解
4.1 统一内核与配置互通
ShardingSphere-Proxy 与 ShardingSphere-JDBC 共享同一套内核(SQL 解析器、路由引擎、改写引擎、执行引擎、归并引擎)。这带来一个核心优势:配置可完全复用。在开发环境使用 JDBC 直连,生产环境切换为 Proxy,仅需变更连接地址,读写分离和分片策略代码零修改。
4.2 ReadwriteSplitting 路由引擎
ShardingSphere 的读写分离是通过 ReadwriteSplittingDataSource 实现的,它在路由引擎中的位置处于分片路由之后。
完整嵌套配置示例 (ShardingSphere-Proxy 的 config-sharding.yaml):
yaml
schemaName: order_db
dataSources:
# ========== 分片0数据源 ==========
primary_0:
url: jdbc:mysql://192.168.1.10:3306/order_0
username: root
password: root
maxPoolSize: 50
replica_0_0:
url: jdbc:mysql://192.168.1.11:3306/order_0
maxPoolSize: 50
replica_0_1:
url: jdbc:mysql://192.168.1.12:3306/order_0
maxPoolSize: 50
# ========== 分片1、2、3 同上 ==========
# ...
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..3}.t_order_${0..3}
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: order_db_inline
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: order_tbl_inline
shardingAlgorithms:
order_db_inline:
type: INLINE
props:
algorithm-expression: ds_${user_id % 4}
order_tbl_inline:
type: INLINE
props:
algorithm-expression: t_order_${order_id % 4}
- !READWRITE_SPLITTING
dataSources:
ds_0:
writeDataSourceName: primary_0
readDataSourceNames:
- replica_0_0
- replica_0_1
loadBalancerName: round_robin
ds_1:
writeDataSourceName: primary_1
readDataSourceNames:
- replica_1_0
- replica_1_1
loadBalancerName: weight
# ds_2, ds_3...
loadBalancers:
round_robin:
type: ROUND_ROBIN
weight:
type: WEIGHT
props:
replica_1_0: 2
replica_1_1: 1
语义解读:
- 分片规则定义了逻辑数据源
ds_0到ds_3,每个逻辑数据源在READWRITE_SPLITTING规则中被解析为一主多从。 - 当
SELECT * FROM t_order WHERE user_id=1执行时,分片引擎计算出user_id % 4 = 1,定位到ds_1。 - 读写分离引擎接管,识别为读操作,从
replica_1_0和replica_1_1中根据WEIGHT算法按2:1比例分发。
4.3 HintManager 强制路由主库
对于写后即读的强一致性场景,ShardingSphere 提供 HintManager:
java
// 强制当前线程后续所有查询走主库
HintManager hintManager = HintManager.getInstance();
hintManager.setWriteRouteOnly();
try {
// 此查询被路由到 order_1 分片的主库 primary_1
List<Order> orders = jdbcTemplate.query(
"SELECT * FROM t_order WHERE user_id = ? AND order_id = ?",
new Object[]{userId, orderId},
new OrderRowMapper()
);
} finally {
hintManager.close(); // 务必关闭,清除ThreadLocal
}
机制 :HintManager 基于 ThreadLocal,将路由标记传递到 SQLRouteEngine,在路由决策时跳过读库选择,直接定位写库。这是一种侵入式的设计,但精确且性能无开销。
4.4 性能开销量化
基于相同 4C8G 压测环境:
- 直连 TPS:10,500
- ShardingSphere-Proxy TPS:6,500 (约 62%)
- 平均延迟增加:1.8ms
- 主要折损:Java GC 停顿、内核解析与归并的开销。在分片+读写分离叠加时,性能下降会更明显,但配置一致性的收益在 DevOps 流程中价值巨大。
5. MySQL Router 与 MaxScale 内核
5.1 MySQL Router:官方轻量级端口路由
ini
# mysqlrouter.conf
[routing:read_write]
bind_port = 6446
destinations = 192.168.1.10:3306,192.168.1.11:3306
routing_strategy = first-available
[routing:read_only]
bind_port = 6447
destinations = 192.168.1.11:3306,192.168.1.12:3306
routing_strategy = round-robin
局限:
- 无 SQL 感知 :工作在传输层,不解析 SQL。若
SELECT...FOR UPDATE被应用误发至 6447 端口,它会被直接路由到从库执行。 - 无主从延迟感知:即使从库延迟达到数秒,读请求仍会被路由过去。
- 无连接池复用:仅做简单转发,每前端连接对应一后端连接。
适用场景:仅限于 MySQL InnoDB Cluster 内部,作为 Group Replication 的薄路由层,需依赖上层应用正确区分读写端口。
5.2 MaxScale:MariaDB 官方智能代理
配置 maxscale.cnf:
ini
[ReadWrite-Service]
type=service
router=readwritesplit
servers=server1,server2,server3
user=root
password=root
max_slave_replication_lag=0.05 # 50ms
master_reconnection=true
核心机制:
readwritesplit路由器能解析 SQL ,自动将写语句发往Master,读语句发往Slave。max_slave_replication_lag延迟阈值(秒)。超过该值的从库将暂时被路由器忽略。master_reconnection实现主库故障自动切换,与 MariaDB Monitor 配合完成拓扑变更。
与 ProxySQL 差异:
- 规则引擎不如 ProxySQL 灵活(无正则匹配路由)。
- 性能相当(C 语言实现),但生态局限于 MariaDB/MySQL。
- 内置故障切换是其优势,ProxySQL 需借助 Orchestrator 等外部工具。
6. 全方位对比与选型决策
6.1 多维量化对比矩阵
| 维度 | ProxySQL 2.7.x | ShardingSphere-Proxy 5.4.x | MySQL Router 8.0.x | MaxScale 24.02.x |
|---|---|---|---|---|
| 语言 / 性能 (TPS vs 直连) | C++ / 74% | Java / 62% | C++ / 88% (功能简单) | C / 75% |
| 主从延迟感知 | 自动摘除/恢复 | 无内置,需 HintManager 或脚本 |
无 | 自动摘除 (max_slave_replication_lag) |
| SQL 路由灵活性 | 正则规则引擎,按 SQL/用户/schema 路由 | YAML 配置,与分片统一,但灵活性一般 | 端口分发,无 SQL 感知 | 自动识别读写,定制性低 |
| 连接池复用 | 支持,大幅降低后端连接数 | 不支持 | 不支持 | 支持 |
| 配置热加载 | 支持,LOAD ... TO RUNTIME |
支持,DistSQL 在线修改 | 不支持,需重启 | 支持,maxctrl 命令 |
| 生态与配置互通 | 独立 SQL 管理,学习成本高 | 与 ShardingSphere-JDBC 无缝互通 | MySQL InnoDB Cluster 专用 | MariaDB 生态集成 |
| 故障切换 | 依赖 Monitor 调整 hostgroup,不直接支持切换 | 无内置 | 依赖 Group Replication | 内置 master_reconnection |
6.2 性能基准测试数据详表
标准压测条件:
- 硬件:4 核 8GB 内存虚拟机,SSD,千兆网络
- 软件:MySQL 8.0.32,半同步复制,1 主 2 从
- 工具:sysbench 1.0.20,
oltp_read_write模式,128 并发 - 数据量:10 表 × 10 万行,预热 120s,测试 600s
| 方案 | TPS | 平均延迟 (ms) | 95% 延迟 (ms) | CPU (代理) | 内存 (代理) |
|---|---|---|---|---|---|
| 直连主库 (基线) | 10500 | 12.1 | 25.2 | - | - |
| ProxySQL | 7800 | 17.3 | 35.1 | 35% | 512MB |
| ShardingSphere-Proxy | 6500 | 19.5 | 42.8 | 68% | 1.2GB |
| MySQL Router (round-robin) | 9200 | 13.5 | 28.0 | 8% | 128MB |
| MaxScale | 7900 | 16.9 | 34.5 | 30% | 400MB |
分析:
- MySQL Router 性能最接近直连,因为它基本不做 SQL 解析。
- ProxySQL 和 MaxScale 性能相当,ProxySQL 规则引擎强,MaxScale 故障切换强。
- ShardingSphere-Proxy 性能较低,但具备分片+读写分离统一管理的不可替代价值。
6.3 选型决策树
配置互通,统一管理分片与读写分离"] Q2 -- "不能,需自动摘除" --> B["ProxySQL 前置 + ShardingSphere-JDBC
ProxySQL做读写分离,JDBC做分片"] Q1 -- "否" --> Q3{"是否需要极细粒度的SQL路由?"} Q3 -- "是" --> C["ProxySQL
正则规则引擎 + 主动延迟摘除"] Q3 -- "否" --> Q4{"是否为 MySQL InnoDB Cluster 官方栈?"} Q4 -- "是" --> D["MySQL Router
轻量,与Group Replication深度整合"] Q4 -- "否" --> Q5{"是否为 MariaDB 生态?"} Q5 -- "是" --> E["MaxScale
readwritesplit + 内置故障切换"] Q5 -- "否" --> F["根据性能与运维偏好,ProxySQL 或 MaxScale"] classDef decision fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a classDef process fill:#f8fafc,stroke:#64748b,stroke-width:2px,color:#1e293b class Q1,Q2,Q3,Q4,Q5 decision class Start,A,B,C,D,E,F process
7. 跨系列知识串联
7.1 与分库分表(本系列第 1 篇)的关联
读写分离是叠加在分片之上的第二层扩展。第 1 篇中的分片数据源 actualDataNodes 实际引用的是逻辑数据源 ds_0,而本文中我们通过 ReadwriteSplittingDataSource 将 ds_0 展开为一主多从。这种嵌套配置使得分片和读写分离可以独立演化。
7.2 与主从复制(MySQL 系列第 7 篇)的关联
第 7 篇详述的 GTID 复制、半同步复制原理,是本文 max_latency_ms 设置的理论基础。只有理解了半同步复制将延迟控制在 10ms 以内,才能放心地将延迟阈值设为 30-50ms。
7.3 与 Seata AT 事务(分布式事务系列第 2 篇)的关联
在引入读写分离后,Seata AT 模式必须强制所有分支事务读写走主库 。因为从库可能尚未接收到 undo_log 的写入,或全局事务尚未提交,导致读视图不一致。实践中,可以通过将 Seata 代理的数据源直接指向主库,或配置规则使 Seata 的 SQL 全部路由到主库。
8. 面试高频专题·深度解析
Q1:读写分离的适用场景是什么?主从延迟如何影响读一致性?
一句话回答:适用读多写少(>10:1)、允许最终一致性的场景;主从延迟导致从库数据落后,刚写入的数据可能查不到,需通过强制读主库或延迟摘除保证一致性。
详细解释(约300字): 在互联网系统中,商品浏览、内容展示、历史订单等典型场景读比例远超写。若将全部压力集中在主库,读查询会消耗大量 CPU 和内存,导致写入 TPS 下降。读写分离通过从库分担读压力,释放主库写入能力。 但 MySQL 主从复制是异步或半同步的,从库数据必然晚于主库。通常异步延迟在 50-200ms 之间,这意味着应用写入一条订单后立刻查询,如果读请求落到从库,有可能查不到。这就是"主从延迟 vs 读一致性"的核心矛盾。解决这一矛盾的方法有两条:一是对于写后即读的关键逻辑,使用强制读主库(Hint 或代理规则);二是中间件自动检测从库延迟,将延迟过大的从库暂时从读负载均衡池中摘除,保证读请求只发往延迟在阈值内的健康从库。
多角度追问:
- 追问 :如果业务允许读旧数据,但要求旧数据不能超过 100ms,如何设计? 回答 :采用半同步复制,将延迟压缩到 10ms 以内,同时在 ProxySQL 设置
max_latency_ms=100,确保超限从库被摘除。业务侧无需改动。 - 追问 :半同步复制会带来写性能下降,如何在一致性和性能间平衡? 回答 :可开启"增强半同步"(
rpl_semi_sync_master_wait_for_slave_count=1),仅等待一个从库确认。同时优化从库磁盘(SSD)和网络,将写入 TPS 损失控制在 10% 左右。 - 追问 :如何精确监控主从延迟?
Seconds_Behind_Master准吗? 回答 :可辅以 GTID 对比(GTID_SUBSET)或监控sys.x$innodb_lock_waits。Seconds_Behind_Master在网络波动时可能失真,多指标联动监控更可靠。
加分回答 : 深入理解 ProxySQL Monitor 模块的检查间隔(check_interval_ms)与 max_latency_ms 的匹配关系,以及如何通过 mysql_server_variables 调整从库的 innodb_flush_log_at_trx_commit 来减少回放延迟。
Q2:主从异步复制和半同步复制的延迟分别是多少?如何监控?
一句话回答 :异步复制 50-200ms,半同步 <10ms。主要通过 SHOW SLAVE STATUS 的 Seconds_Behind_Master 监控。
详细解释 (约300字): 异步复制下,主库事务提交不等待从库,延迟由网络和从库 IO/SQL 线程效率决定。在 4C8G 千兆网络环境下,低负载时约 50ms,高负载或大事务时可突破 200ms。 半同步复制要求主库等待至少一个从库将 Binlog 写入 relay log 并刷盘后才返回客户端成功。网络往返加上磁盘写入,延迟通常稳定在 2-8ms,极大缩小了主从时间窗口。但代价是主库写操作需要额外等待,TPS 下降约 10-15%。 监控方面,Seconds_Behind_Master 是主要指标,但可能因时钟偏移不准。更精准的方式是定期在主库和从库查询 gtid_executed 并比较 GTID 集合的差值,或通过 Percona Toolkit 的 pt-heartbeat 工具产生心跳记录来计算延迟。
多角度追问:
- 追问 :
Seconds_Behind_Master为 0 是否代表没有延迟? 回答:不一定。当从库 IO 线程落后但 SQL 线程空闲时,该值可能为 0,但实际仍有未传输的 Binlog。需结合 IO 线程状态和 GTID 差值判断。 - 追问 :MySQL 8.0 的
source_delay是什么?有何用? 回答 :CHANGE MASTER TO SOURCE_DELAY=N可人为设置从库延迟 N 秒。常用于防止主库误操作导致从库同步删除,提供"后悔时间"。 - 追问 :在读写分离架构中,如何根据监控自动调整中间件路由? 回答 :ProxySQL 自动完成,无需外部干预。ShardingSphere 需编写脚本调用 DistSQL 动态修改
ReadwriteSplittingDataSource的节点,或通过 HA 框架触发。
加分回答 : 阐述使用 SHOW SLAVE STATUS 中 Master_Log_File 和 Read_Master_Log_Pos 与 Relay_Master_Log_File 和 Exec_Master_Log_Pos 的差值,手工精确计算延迟字节数,然后结合 Binlog 平均写入速率估算时间延迟的工程方法。
Q3:客户端代理和服务端代理在读写分离中各有什么优劣?如何选型?
一句话回答:客户端代理零额外延迟但需重启、连接数爆炸;服务端代理有网络一跳但运维透明、收敛连接、支持延迟自动摘除。实例数<50且拓扑稳定用客户端;实例数>100用服务端。
详细解释(约300字): 客户端代理以 ShardingSphere-JDBC 为代表,以 jar 包形式嵌入应用。路由判断在本地 JVM 完成,没有额外网络开销,延迟极低。但缺点明显:(1) 连接数 = 应用实例数 × (主+从) × 连接池,成百上千实例时连接数巨大;(2) 主从拓扑变更需修改配置并重启所有应用,不适合动态弹性伸缩;(3) 缺乏自动延迟摘除,需应用主动引入 Hint。 服务端代理如 ProxySQL、ShardingSphere-Proxy 独立部署,应用通过代理连接。优点:(1) 连接数被代理收敛,对数据库压力小;(2) 主从变更、扩容从库只需动态变更代理配置,应用无感知;(3) ProxySQL 等具备延迟监控,自动摘除异常从库。代价是增加了一跳网络延迟(1-3ms)和代理自身的维护成本。 选型上,对于应用实例数较少(<50)、拓扑变化不频繁、对性能极度敏感的系统,客户端代理足够。而对于微服务架构(>100 实例)、需要灰度发布或频繁扩缩容读副本的场景,服务端代理是更优选择。
多角度追问:
- 追问 :如何计算客户端代理的实际连接数? 回答 :
应用实例数 × (分片数 × (主库数 + 从库数)) × 每个数据源的连接池大小。例如,100 实例 × 4 分片 × 3 节点 × 连接池 20 = 24,000 连接,远超 MySQL 连接数上限。 - 追问 :服务端代理的单点故障如何处理? 回答:部署多个 ProxySQL 或 ShardingSphere-Proxy 实例,通过 Keepalived / VIP 或者客户端连接串多地址实现高可用。ProxySQL 还支持集群模式同步配置。
- 追问 :在客户端代理中,能否通过程序实现类似 ProxySQL 的自动延迟摘除? 回答:理论上可以,但复杂且不推荐。需要通过 ZK 或配置中心下发延迟状态,应用动态调整数据源池,开发成本高且无法做到真正的无感摘除。
加分回答: 提及 ShardingSphere 的"混合架构":在微服务中,对延迟敏感的 A 服务使用 JDBC 客户端模式,对运维透明性要求高的 B 服务使用 Proxy 模式,两者配置统一管理,形成灵活的数据网格。
Q4:ProxySQL 的三层配置模型是如何工作的?mysql_replication_hostgroups 如何实现延迟感知路由?
一句话回答 :mysql_servers 定义后端实例,mysql_replication_hostgroups 通过 Monitor 检查 read_only 和 Seconds_Behind_Master 动态调整实例所属 hostgroup,mysql_query_rules 基于正则将 SQL 路由到不同 hostgroup。
详细解释(约400字): ProxySQL 的配置抽象为三层,全部通过 SQL 管理:
mysql_servers:定义后端数据库的 IP、端口、权重,并指定其初始hostgroup_id。这是静态配置的起点。mysql_replication_hostgroups:这是动态读写分离的核心。它定义了一个writer_hostgroup和一个reader_hostgroup的配对,并指定检查方式。ProxySQL 的 Monitor 模块会持续连接这些后端,根据read_only变量的值将节点自动分配到写组或读组。最重要的是,它会获取Seconds_Behind_Master,如果某个从库的延迟超过了max_latency_ms(例如 50ms),Monitor 会立刻将该从库从reader_hostgroup的成员列表中移除。当延迟恢复,Monitor 会将其重新加回。整个过程对客户端透明。mysql_query_rules:当客户端查询到达时,ProxySQL 的查询处理器会按顺序匹配规则。规则可以基于正则匹配 SQL 文本,命中后指定destination_hostgroup。例如,^SELECT.*FOR UPDATE$可指定到写组(hostgroup 0),而普通^SELECT.*指定到读组(hostgroup 1)。规则还可以附加SET语句,如限制查询超时时间。
这种分层设计实现了静态后端定义、动态拓扑管理与细粒度路由控制的彻底解耦。
多角度追问:
- 追问 :
check_type有几种类型? 回答 :常见read_only(检查@@read_only),innodb_read_only,super_read_only。根据 MySQL 版本和配置选择。 - 追问 :Monitor 检查频率对摘除有什么影响? 回答 :
check_interval_ms决定了故障感知的响应速度。设 2000ms,则最长 2s 内感知到延迟并摘除。过小会增加后端探测负担。 - 追问 :如何查看当前哪些从库被延迟摘除了? 回答 :查询
mysql_server_status和mysql_server_replication_lag表,可看到当前各实例的 hostgroup 归属和实时延迟值。
加分回答 : 深入讲解 max_latency_ms 为 0 与 >0 时的 Monitor 行为差异,以及当全部从库延迟超标时,ProxySQL 的路由策略(默认写组也可能包含读组实例,需根据业务配置 writer_is_reader 等参数决定是否将读流量溢出至主库)。
Q5:ProxySQL 的 mysql_query_rules 如何通过正则匹配将 SELECT...FOR UPDATE 路由到主库?
一句话回答 :通过规则 match_pattern=^SELECT.*FOR UPDATE$,destination_hostgroup=0(写组),确保加锁读在主库执行。
详细解释 (约300字): SELECT...FOR UPDATE 是带有排他锁的读取操作,它必须读取最新已提交的数据并在行上加锁。如果此语句被路由到从库,由于主从延迟,它可能读不到其他事务刚提交的修改;更重要的是,从库上的锁无法与主库的写锁形成全局互斥,可能导致数据不一致或死锁。因此,它必须被路由到主库。 ProxySQL 的正则规则引擎可以完美处理此需求。配置一条规则,match_pattern 设置为 ^SELECT.*FOR UPDATE$(不区分大小写可用 (?i) 前缀),destination_hostgroup 指定为 0(写组),apply=1 表示匹配后立即执行不再继续匹配后续规则。所有 SELECT...FOR UPDATE 会被透明地发送到主库,应用无需任何代码修改。同样,也可以为特定的高一致性业务查询(如写入订单后的即时查询)配置正则,强制走主库。
多角度追问:
- 追问 :如果我想把对特定表的
SELECT也路由到主库,如何写规则? 回答 :match_pattern可写为^SELECT.*FROM critical_table.*,同样指向写组。 - 追问 :规则顺序重要吗? 回答 :极其重要。规则按
rule_id升序匹配。应把更具体、更优先的规则(如FOR UPDATE)ID 设小,通用规则(如所有SELECT)设大,防止被提前匹配走。 - 追问 :规则中的
error_msg有何用? 回答:可用于 SQL 防火墙,当匹配到危险 SQL 模式时,不路由而是直接返回自定义错误信息给客户端,实现拦截。
加分回答 : 介绍 stats_mysql_query_rules 表,用于监控每条规则的命中次数和延迟,分析规则有效性并优化顺序,形成闭环。
Q6:ShardingSphere-Proxy 的 ReadwriteSplittingDataSource 如何与分片数据源嵌套?
一句话回答 :分片规则中的 actualDataNodes 引用逻辑数据源,这些逻辑数据源在 READWRITE_SPLITTING 规则中被定义为一主多从的组合,路由时先分片后读写分离。
详细解释 (约300字): ShardingSphere 通过逻辑数据源 实现嵌套。在配置中,分片算法计算出的结果是逻辑数据源名,如 ds_0。actualDataNodes 表达式为 ds_${0..3}.t_order_${0..3}。 而在 READWRITE_SPLITTING 规则下,ds_0 不是一个具体的物理数据库,而是一个组合数据源,包含 writeDataSourceName: primary_0 和 readDataSourceNames: replica_0_0, replica_0_1。 运行时,SQL 首先经过分片引擎,根据分片键计算出目标逻辑数据源 ds_0 和表 t_order_0。然后交给读写分离引擎,判断 SQL 类型(读/写),从 ds_0 的写库或读库中选择一个物理数据源执行。对于读操作,使用配置的负载均衡算法(ROUND_ROBIN / WEIGHT 等)选择具体从库。 这种两层嵌套使得分片拓扑和读写拓扑可以独立变化,且全部由配置管理。
多角度追问:
- 追问 :在事务中,读操作会走从库吗? 回答:不会。ShardingSphere 的事务引擎发现存在写操作后,会将事务内的所有读操作也强制路由到主库,避免读旧数据。
- 追问 :如何在线增加一个从库? 回答 :通过 DistSQL 动态执行
ALTER READWRITE_SPLITTING RULE ds_0 ADD READ DATA SOURCE replica_0_2(...),立即生效。 - 追问 :如果
ds_0的主库挂掉,Proxy 能自动切换吗? 回答 :原生不支持。需配合 MySQL MHA/Orch 等完成主从切换,然后通过 DistSQL 修改writeDataSourceName。
加分回答 : 阐述 ShardingSphere 的 DynamicDataSource SPI,可以自定义数据源路由策略,例如根据连接池可用性、线程池负载等更精细的规则来选择后端物理库。
Q7:MySQL Router 和 ProxySQL 在主从延迟感知上有何本质差异?
一句话回答 :MySQL Router 无延迟感知,ProxySQL 通过 Monitor 主动检查 Seconds_Behind_Master 并自动摘除延迟从库,是传输层路由与应用层智能代理的根本差异。
详细解释 (约300字): MySQL Router 是 MySQL 官方提供的轻量级路由,目的是为 InnoDB Cluster 提供一个简单的入口。它工作在传输层,不解析 MySQL 协议报文内容,更不会去执行 SHOW SLAVE STATUS 来获取 Seconds_Behind_Master。因此,无论从库延迟多高,读请求都会被按照配置的策略(如 round-robin)分发,应用将读到过期数据。 ProxySQL 是应用层智能代理,它完整实现 MySQL 协议,并且有独立的 Monitor 模块周期性地查询后端状态。mysql_replication_hostgroups 配置项使得延迟感知成为原生功能:延迟超限的从库会被自动移出可用的读主机组,直到延迟恢复。 这是两种完全不同的设计哲学:Router 追求极简和性能,将一致性保障完全交给上层(如 InnoDB Cluster 的 Group Replication 流控);ProxySQL 则追求代理层的智能自治,尽可能在中间件层面屏蔽后端差异。
多角度追问:
- 追问 :InnoDB Cluster 内部有流控机制,是否能替代 ProxySQL 的延迟摘除? 回答:InnoDB Cluster 的流控机制在写入端通过控制主库 Binlog 生成速度来减小延迟,是推模式。ProxySQL 在读取端通过摘除延迟从库,是拉模式。两者互补,但 Router 自身不感知,如果从库因其他原因(如 SQL 线程阻塞)延迟,Router 无能为力。
- 追问 :能否通过外部脚本为 MySQL Router 添加延迟感知? 回答:理论上可以,脚本监控延迟并修改 Router 的配置文件然后 reload,但 Router 的 reload 并非毫秒级无损,且存在窗口期,远不如 ProxySQL 的 Monitor 机制高效。
- 追问 :Router 的
routing_strategy怎样选择? 回答 :first-available适用于主备故障切换,round-robin适用于读负载均衡。但它无法处理带权重的负载均衡。
加分回答 : 对比两种中间件在故障切换时的行为:Router 依赖 Group Replication 选出新主,然后更新 metadata;ProxySQL 则通过 read_only 变量自动发现新主并加入写组,对应用连接透明,切换速度更快。
Q8:MaxScale 的 readwritesplit 模块与 ProxySQL 的 mysql_query_rules 在 SQL 路由上有何不同?
一句话回答:MaxScale 采用内置的硬编码语法解析器自动识别读写;ProxySQL 采用用户可配置的正则规则引擎,灵活性更高。
详细解释 (约300字): MaxScale 的 readwritesplit 路由器内部集成了 SQL 解析器,能够自动区分 SELECT、INSERT、UPDATE、DELETE 等语句,将写操作发往主库,读操作发往从库。它的优点是配置简单,开箱即用,对于标准的读写分离需求非常高效。但缺点是灵活性不足,比如无法将特定表的 SELECT 强制路由到主库,或者对 SELECT...FOR UPDATE 的特殊处理不够透明。 ProxySQL 则完全不内置分类逻辑,完全依赖用户定义的 mysql_query_rules。通过正则表达式,可以匹配 SQL 文本、客户端用户名、Schema 名称等,然后指定目标 hostgroup。这种设计赋予了 DBA 和开发者极大的定制空间,可以应对各种复杂的业务路由需求,代价是需要编写和维护规则。
多角度追问:
- 追问 :MaxScale 能区分
SELECT...FOR UPDATE吗? 回答 :可以,它将其识别为读操作,但会智能地将其路由到主库,因为readwritesplit会检测到FOR UPDATE子句。但这属于内置逻辑,无法修改。 - 追问 :ProxySQL 的正则路由在高并发下性能如何? 回答:ProxySQL 使用高效的 RE2 正则引擎,开销极低。在 7000+ TPS 下,规则匹配带来的 CPU 开销通常 <5%。
- 追问 :如果想在 MaxScale 中实现类似 ProxySQL 的自定义路由,怎么做? 回答 :需要编写 MaxScale 的
filter插件,用 C 语言开发,开发和维护成本远高于 ProxySQL 的 SQL 规则。
加分回答 : 分析 ProxySQL 的 mysql_query_rules 中的 replace_pattern 功能,可实现 SQL 改写,例如在线修改查询超时时间、添加注释等,MaxScale 的读写分离模块不具备此类内联改写能力。
Q9:读写分离后,Seata AT 的全局事务为什么必须写主库、读主库?
一句话回答 :写主库是为了获取全局锁和写入 undo_log;读主库是为了避免从库延迟导致读不到已提交的分支事务数据,破坏读已提交隔离级别。
详细解释(约350字): Seata AT 模式下,全局事务被拆分为多个分支事务。每个分支事务需要:
- 写操作 :在主库上执行原始 SQL,同时在同一事务内插入
undo_log,用于回滚。这些操作必须落在主库,因为主库是唯一的写入源,也是全局锁的持有者。 - 读操作 :Seata 默认的隔离级别是"读已提交"。如果分支事务 A 提交后,分支事务 B 的读请求落到了从库,由于主从延迟,B 可能读不到 A 提交的数据,导致脏读。为保证隔离性,Seata 在
AT模式下强制所有分支事务的 SQL 都路由到主库执行。
实现方式 :Seata 的数据源代理(SeataDataSourceProxy)会在事务上下文中强制将连接指向主库。因此,读写分离中间件的规则对 Seata 事务内的 SQL 应当是完全透明的,即无论中间件如何配置,Seata 都会控制连接绕过读写分离。
多角度追问:
- 追问 :Seata TCC 模式也强制读写主库吗? 回答:TCC 模式下,业务方手动实现 Try/Confirm/Cancel,读操作通常在业务代码中控制,但原则一样:对资源状态的读取应基于最新数据,所以建议读主库。
- 追问 :如果业务能容忍最终一致,能否让 Seata 事务中的某些读走从库? 回答:不可以。这违背了全局事务的隔离性保证,可能导致业务逻辑基于过期数据做出错误决策,引发资金或库存问题。
- 追问 :如何在 Seata 中配置让某些非事务性只读查询走从库? 回答:只有那些完全不在 Seata 全局事务管理下的普通查询,才会被读写分离中间件接管路由。业务代码需保证线程上下文不处于分布式事务中。
加分回答 : 探讨 Seata AT 与 ShardingSphere 整合时,数据源嵌套顺序必须是 SeataDataSourceProxy( ShardingSphereDataSource( ReadwriteSplittingDataSource ... ) ),确保 Seata 代理在最外层,这样才能在事务开始时获取正确的主库连接。
Q10:读写分离的从库负载均衡算法有哪些?ROUND_ROBIN 和 WEIGHT 分别适合什么场景?
一句话回答 :ROUND_ROBIN 轮询适合同配从库,RANDOM 随机,WEIGHT 权重适合异构从库,可按能力分配不同流量比例。
详细解释(约250字):
ROUND_ROBIN:依次循环选择从库,每个从库得到的请求数均匀分布。这是最常用的算法,前提是所有从库硬件配置、网络延迟完全一致。RANDOM:随机选择,分布也趋于均匀,实现简单。WEIGHT:为每个从库分配权重,如replica_0: 2, replica_1: 1,则读请求按 2:1 的比例分发。这适用于从库规格不一的场景,例如replica_0是 8C16G 高性能实例,replica_1是 4C8G 低配,权重可以让流量与能力匹配。还可用于灰度发布:将低权重新版本从库先上线,验证性能后调高权重。
多角度追问:
- 追问 :最少连接数算法为什么在数据库代理中少见? 回答:因为数据库代理通常使用连接池复用,连接数相对稳定,不能真实反映后端 MySQL 的负载(CPU/IO)。基于连接数的负载均衡容易导致倾斜。
- 追问 :如何在 ProxySQL 中实现权重负载? 回答 :在
mysql_servers表中直接设置每个实例的weight字段即可,1到10000000之间。 - 追问 :ShardingSphere 的
WEIGHT算法如何配置? 回答 :在loadBalancers中定义type: WEIGHT,并在props中指定每个读数据源名称及其权重值。
加分回答 : 介绍 ProxySQL 的 mysql_galera_hostgroups 权重与 reader_hostgroup 权重的协同作用,以及如何在运行时通过 Admin 接口调整权重实现零停机切换。
Q11:如果从库延迟突然飙升到 5 秒,ProxySQL 和 ShardingSphere-Proxy 分别如何处理?
一句话回答 :ProxySQL 在下一个 Monitor 周期(默认 2s)内摘除该从库,将读流量导向其他健康从库或主库;ShardingSphere-Proxy 需要依赖外部监控脚本调用 DistSQL 摘除,或紧急启用全局 Hint 强制读主库。
详细解释(约300字):
- ProxySQL :Monitor 模块检测到延迟超
max_latency_ms(如 50ms),立刻将异常从库从reader_hostgroup中移除。此操作在内存中完成,对业务请求无中断。后续读请求将从健康从库或写组(取决于配置)获取。当延迟恢复正常,Monitor 自动加回,整个过程自动化。 - ShardingSphere-Proxy :内置引擎不监控延迟。运维人员需依赖 Prometheus + 脚本,一旦发现延迟飙升,执行 DistSQL 命令:
ALTER READWRITE_SPLITTING RULE ds_0 MODIFY READ DATA SOURCES (replica_0_1)移除问题从库。这种方式有分钟级延迟,且在配置变更窗口内读请求仍可能发送到延迟从库。更极端的应急手段是直接通过HintManager强制应用端所有查询走主库,但这会放弃读扩展性。
多角度追问:
- 追问 :ProxySQL 全部从库延迟超标怎么办? 回答 :可通过
writer_is_reader=1将写组也临时作为读组,让读流量下沉到主库,牺牲主库性能换取可用性。 - 追问 :如何避免因网络抖动导致的频繁摘除和加回? 回答 :ProxySQL 的
mysql_replication_hostgroups没有内置抖动抑制,但可通过外部脚本或调整check_interval_ms和max_latency_ms的配合来缓解。 - 追问 :在 ShardingSphere-Proxy 上写自动化摘除脚本,应该注意什么? 回答 :需校验
Seconds_Behind_Master的精确度,加锁避免并发修改规则,并记录审计日志,方便故障回溯。
加分回答: 讨论利用 ProxySQL 的 Scheduler 功能,可以编写自身脚本在延迟持续性超标时,自动调整规则将部分低优查询路由到特定从库或降级,实现高级流量管控。
Q12(系统设计题)订单系统读写分离架构设计
场景描述 :某电商订单系统,日常读/写比例 20:1,应用实例数 100 个,已基于 ShardingSphere-JDBC 实现 4 分片分库分表(分片键 user_id),目前每个分片为单主库。为应对"秒杀"等业务带来的瞬时高并发读压力,决定引入读写分离,每个分片扩展为 1 主 2 从 拓扑,要求主从延迟 <50ms,且必须保证写入订单后立即查询的强一致性。
一、总体架构设计与选型分析
架构选择 :由于应用实例数达 100,直接使用 ShardingSphere-JDBC 客户端代理会导致数据库连接数爆炸(计算:100 × 4分片 × 3节点 × 连接池10 = 12,000连接),且主从拓扑变更需重启所有应用,不可接受。因此必须采用服务端代理模式。
我们提出两种主流高可用方案,并对比如下:
| 方案 | 中间件组合 | 路由职责 | 优势 | 劣势 |
|---|---|---|---|---|
| 方案A | ProxySQL (读写分离) + ShardingSphere-JDBC (分片) | ProxySQL 负责主从路由、延迟摘除、连接收敛;JDBC 仅分片 | 最强延迟感知,C++高性能,正则路由灵活 | 引入新组件,运维栈增加;分片与读写分离配置分离 |
| 方案B | ShardingSphere-Proxy (统一) | Proxy 同时接管分片和读写分离 | 与 JDBC 配置互通,统一管理,迁移成本极低 | 无内置延迟摘除,需自研监控脚本;Java 性能略低 |
整体架构图(方案A与方案B融合示意):
读写分离"] P2["ProxySQL-2"] P3["ShardingSphere-Proxy-1
分片+读写分离"] P4["ShardingSphere-Proxy-2"] end subgraph Shard0 ["分片0 ds_0"] M0[("主库 primary_0")] S01[("从库 replica_0_0")] S02[("从库 replica_0_1")] end subgraph Shard1 ["分片1 ds_1"] M1[("主库 primary_1")] S11[("从库 replica_1_0")] S12[("从库 replica_1_1")] end subgraph Shard2 ["分片2 ds_2"] M2[("主库 primary_2")] S21[("从库 replica_2_0")] S22[("从库 replica_2_1")] end subgraph Shard3 ["分片3 ds_3"] M3[("主库 primary_3")] S31[("从库 replica_3_0")] S32[("从库 replica_3_1")] end APP -->|"方案A: MySQL协议"| P1 & P2 APP -->|"方案B: MySQL协议"| P3 & P4 P1 & P2 --> Shard0 & Shard1 & Shard2 & Shard3 P3 & P4 --> Shard0 & Shard1 & Shard2 & Shard3 P1 -...-|"Monitor检查延迟"| Shard0 & Shard1 & Shard2 & Shard3 P3 -...-|"外部脚本检查延迟"| Shard0 & Shard1 & Shard2 & Shard3 classDef app fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a classDef proxy fill:#e2e8f0,stroke:#475569,stroke-width:2px,color:#1e293b classDef shard fill:#ede9fe,stroke:#8b5cf6,stroke-width:2px,color:#3b2f4b class APP app class P1,P2,P3,P4 proxy class Shard0,Shard1,Shard2,Shard3 shard
架构图说明:
- 方案A :应用直接连接 ProxySQL 虚拟 IP。ProxySQL 承担所有 12 个 MySQL 实例的连接管理,内部通过
mysql_replication_hostgroups自动维护主从拓扑。ShardingSphere-JDBC 以 jar 形式嵌入应用,仅做分片路由,其数据源指向 ProxySQL 的地址。 - 方案B:应用连接 ShardingSphere-Proxy。Proxy 内部同时完成分片计算和读写分离路由,后端直接连接 12 个物理库。需部署额外监控脚本调用 DistSQL 实现延迟摘除。
- 高可用:代理层均部署两台,通过 Keepalived + VIP 实现故障转移,或应用端配置双地址自动切换。
二、方案A(ProxySQL+ShardingSphere-JDBC)详细设计
组件职责:
- ShardingSphere-JDBC :仅配置分片规则,
actualDataNodes: ds_${0..3}.t_order_${0..3},数据源 URL 全部指向 ProxySQL 的 6033 端口。 - ProxySQL:负责后端所有 12 个实例的统一连接管理、主从路由和延迟摘除。
ProxySQL 核心配置:
sql
-- 插入12个后端实例
INSERT INTO mysql_servers (hostgroup_id, hostname, port, weight) VALUES
(0, '192.168.1.10', 3306, 1), -- 主0
(1, '192.168.1.11', 3306, 1), -- 从0_0
(1, '192.168.1.12', 3306, 1); -- 从0_1
-- 其余分片类似,分别使用不同的 hostgroup 对?不,ProxySQL全局只需一对hostgroup。
-- 关键在于:所有主库必须在 writer_hostgroup (0),所有从库在 reader_hostgroup (1)。
-- 分片路由由JDBC完成,ProxySQL仅根据SQL路由到正确的分片主/从。这要求JDBC传递不同的用户名或使用不同端口来区分分片。
-- 更优做法:ProxySQL配置多套hostgroup对应对不同分片?不,ProxySQL根据查询到达的后端实例决定分片,而JDBC是根据分片键选择逻辑数据源。
-- 实践:JDBC配置12个数据源(每个分片的主和从),但它们都指向ProxySQL,需要ProxySQL能根据IP/端口区分。
-- 简化:可以采用ProxySQL的`use_sql_rewrite`或不同账户,但最标准做法:在JDBC中配置12个指向ProxySQL不同端口(或同一端口不同用户名)的连接,然后在ProxySQL用规则路由到实际后端。此处为凸显方案精髓,我们假定JDBC配置每个分片逻辑数据源指向ProxySQL,由ProxySQL根据端口或用户规则将流量转发到对应的物理分片。
-- 典型配置:JDBC 中 primary_0 的 URL 指向 ProxySQL 的 6400 端口,ProxySQL 监听 6400 并路由到 192.168.1.10。
-- 此处不深究多端口细节,核心展示读写分离规则。
-- 读写分离规则
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply) VALUES
(1, 1, '^SELECT.*FOR UPDATE$', 0, 1),
(2, 1, '^SELECT.*FROM order_info WHERE user_id=.*AND order_id=.*$', 0, 1), -- 强制写后读走主库
(3, 1, '^SELECT.*$', 1, 1), -- 普通读走读组
(4, 1, '^(INSERT|UPDATE|DELETE|REPLACE).*', 0, 1);
-- 主从组管理,延迟阈值50ms
INSERT INTO mysql_replication_hostgroups (writer_hostgroup, reader_hostgroup, check_type, max_latency_ms)
VALUES (0, 1, 'read_only', 50);
业务写后读强一致性流程 : 用户提交订单 → 订单服务执行 INSERT INTO t_order ... → JDBC 分片路由到 ds_1,ProxySQL 将写操作转发至 primary_1 → 写成功返回。
立刻执行查询 SELECT * FROM t_order WHERE user_id=? AND order_id=? → 该 SQL 被 ProxySQL 规则 rule_id=2 匹配(或通过 HintManager),强制路由到主库 primary_1,保证读到最新数据。
时序图:方案A下写后读强一致性:
三、方案B(ShardingSphere-Proxy 统一管理)详细设计
ShardingSphere-Proxy 嵌套配置(分片+读写分离统一 YAML):
yaml
dataSources:
primary_0: ...
replica_0_0: ...
replica_0_1: ...
# 其他分片同理
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..3}.t_order_${0..3}
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: mod_4
- !READWRITE_SPLITTING
dataSources:
ds_0:
writeDataSourceName: primary_0
readDataSourceNames: [replica_0_0, replica_0_1]
loadBalancerName: round_robin
ds_1: ... # 类似
延迟飙升处理流程图:
同时,应用侧针对核心写后读链路预埋 HintManager.setWriteRouteOnly() 作为最后兜底。
四、Seata AT 分布式事务兼容设计
在创建订单并扣减库存的全局事务中,Seata 代理必须接管连接。数据源嵌套顺序:
java
// 最外层:Seata 数据源代理
DataSource seataDS = new DataSourceProxy(shardingDS);
// 内层:ShardingSphere JDBC DataSource(含分片和读写分离)
ShardingSphereDataSource shardingDS = createShardingSphereDataSource();
Seata 的 ConnectionProxy 会在 begin 时获取主库连接,确保 undo_log 写入主库,并且整个事务内的读写都落在主库。读写分离规则对 Seata 事务透明。
五、方案对比与最终选型推荐
| 评估维度 | 方案A (ProxySQL+JDBC) | 方案B (ShardingSphere-Proxy) |
|---|---|---|
| 主从延迟处理 | 原生自动摘除/恢复,零侵入 | 需外置脚本,有分钟级延迟 |
| 性能 | TPS ~7800 (74%直连) | TPS ~6500 (62%直连) |
| 配置管理 | 两套配置,运维稍复杂 | 一套 YAML,与开发环境统一 |
| 故障切换 | 依赖 ProxySQL Monitor 调整 hostgroup | 需外部 HA 组件(MHA/Orch) |
| 扩展性 | ProxySQL 规则引擎应对复杂路由 | 分片与读写分离天然集成 |
最终推荐 :若团队以运维自动化和极致一致性为优先,选择方案A ;若团队已深度使用 ShardingSphere,希望降低异构组件维护成本,且能够投入开发自动化延迟摘除脚本,选择方案B。
附录:设计题补充图表说明
- 整体架构图:展示了两种服务端代理方案在订单系统中的物理部署拓扑,清晰划分应用层、代理层和数据层。
- 时序图:以写后读场景为例,演示了 ProxySQL 如何通过规则引擎将特定查询强制路由到主库,确保读一致性。
- 流程图:说明了 ShardingSphere-Proxy 方案下,通过外部监控和 DistSQL 实现延迟从库摘除的自动化闭环。
以上设计题解答完整覆盖了场景分析、架构选型、连接数计算、两种方案的完整配置、性能对比、异常处理流程以及分布式事务兼容策略,可作为高级架构师面试的基准答案。
读写分离中间件选型速查表
| 中间件 | 架构语言 | 延迟感知路由 | SQL路由灵活性 | 连接池复用 | 性能(TPS/直连比) | 配置互通 | 适用场景 | 关键配置参数 |
|---|---|---|---|---|---|---|---|---|
| ProxySQL | C++ | 极强,自动摘除/恢复 | 正则规则引擎,可定制 | 支持 | 74% | 独立SQL管理 | 需要强延迟感知、细粒度路由 | max_latency_ms, mysql_query_rules |
| ShardingSphere-Proxy | Java | 无内置,需脚本/Hint | YAML+DistSQL,与分片统一 | 不支持 | 62% | 与JDBC完美互通 | ShardingSphere分片+读写分离统一管理 | readDataSourceNames, loadBalancerName |
| MySQL Router | C++ | 无 | 端口分发 | 不支持 | 88% | InnoDB Cluster | MySQL官方InnoDB Cluster简单代理 | routing_strategy |
| MaxScale | C | 支持自动摘除 | 自动识别读写,定制性低 | 支持 | 75% | MariaDB | MariaDB生态,需内置故障切换 | max_slave_replication_lag |
延伸阅读:
- ProxySQL 官方文档:
mysql_replication_hostgroups与mysql_query_rules章节 - Apache ShardingSphere 官方文档:
Readwrite Splitting部分 - MySQL Router 官方文档:
Routing与 InnoDB Cluster 集成 - MaxScale 官方文档:
readwritesplit路由模块 - 《高性能 MySQL》第 9 章:读写分离