读写分离与数据库中间件选型

概述

系列定位:本文是"分布式数据架构与存储选型"系列的第二篇。在《分库分表架构选型:ShardingSphere vs Mycat vs DBLE》拆解数据水平拆分的核心技术后,本文将聚焦于应对读多写少场景的核心扩展手段------读写分离。我们将从"主从延迟 vs 读一致性"这一根本矛盾出发,系统解构客户端代理与服务端代理的架构差异,通过对 ShardingSphere-Proxy、MySQL Router、ProxySQL 与 MaxScale 四种主流中间件的深度内核拆解与可量化性能基准对比,帮助架构师与高级开发者建立从原理、选型到落地的完整决策框架。

文章组织架构

flowchart TB subgraph s1 ["1 核心概念与前置条件"] direction LR A1["适用场景与读/写比例"] --> A2["主从延迟量化分析"] --> A3["主从拓扑设计"] --> A4["与分库分表组合"] end subgraph s2 ["2 两种架构模式深度对比"] B1["客户端代理: ShardingSphere-JDBC"] <--> B2["服务端代理: 独立中间件"] end subgraph s3 ["3 ProxySQL 内核深度拆解"] C1["架构与线程模型"] --> C2["三层配置模型详解"] --> C3["Monitor 延迟感知路由机制"] --> C4["连接池与性能"] end subgraph s4 ["4 ShardingSphere-Proxy 内核深度拆解"] D1["统一内核与配置互通"] --> D2["ReadwriteSplitting 路由引擎"] --> D3["负载均衡算法与Hint管理"] end subgraph s5 ["5 MySQL Router 与 MaxScale 内核"] E1["MySQL Router 轻量级端口路由"] --> E2["MaxScale readwritesplit 智能路由"] end subgraph s6 ["6 全方位对比与选型决策"] F1["多维量化对比矩阵"] --> F2["性能基准测试与数据"] --> F3["选型决策树与典型配置"] end subgraph s7 ["7 跨系列知识串联"] G1["关联分库分表"] --> G2["关联主从复制"] --> G3["关联Seata分布式事务"] end subgraph s8 ["8 面试高频专题·深度解析"] end s1 --> s2 --> s3 --> s4 --> s5 --> s6 --> s7 --> s8 classDef c1 fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a classDef c2 fill:#f8fafc,stroke:#475569,stroke-width:2px,color:#1e293b classDef c3 fill:#e2e8f0,stroke:#64748b,stroke-width:2px,color:#1e293b class s1,s2 c1 class s3,s4 c2 class s5,s6 c3 class s7,s8 c1

架构图说明

  • 总览说明:全文 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 事件写入不等待从库确认。延迟主要来自:

  1. 网络传输:Binlog 日志块从主库发送到从库的耗时。
  2. 从库 IO 线程写入 Relay Log:受磁盘 IO 能力影响。
  3. 从库 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 主从拓扑设计

  1. 一主一从:最简单,无读负载均衡,适用于读压力略高于写的场景,或作为数据备份。
  2. 一主多从 :核心读写分离拓扑。从库之间独立,通过中间件实现读负载均衡。需考虑从库权重:不同实例规格可分配不同权重。
  3. 级联复制:主库 → 一级从库 (Blackhole/InnoDB) → 二级从库。减轻主库 Binlog 发送压力,适合需要数十个从库的大规模读扩展,但整体延迟增大。
  4. 双主复制:互为主从,结合 VIP 或 keepalived 实现故障切换,一般不直接用于常规读写分离,避免写冲突。

1.4 与分库分表组合部署

在分库分表架构上叠加读写分离是大多数大型系统的终极形态。基本模式为:每个分片独立管理一主多从

ShardingSphere-JDBC 内部采用两级路由引擎:

  1. 分片路由StandardShardingStrategy 根据分片键(如 user_id)计算分片值,定位到逻辑数据源名(如 ds_0)。
  2. 读写分离路由ReadwriteSplittingDataSource 将逻辑数据源 ds_0 映射为一个包含 writeDataSourceNamereadDataSourceNames 的组合数据源,根据 SQL 类型选择物理连接。

这种设计使得每个分片的读写分离策略可以独立配置(如不同从库权重),且整个拓扑对业务代码透明。

1.5 读写分离架构演进图

flowchart TD A["单库 读写混合"] -->|"读压力增大"| B["一主一从 读从库"] B -->|"读能力扩展"| C["一主多从 读负载均衡"] C -->|"数据量/写压力增长"| D["分库分表 + 每片一主多从
先分片,后读写分离"] classDef default fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a

图 1 说明

  1. 目的与场景:展示数据库架构从单点到读写分离再到分库分表的完整扩展路径,反映数据架构的两个核心扩展维度:读扩展与写扩展。
  2. 核心元素:单库无法区分读/写流量;一主一从实现读分流;一主多从通过中间件实现读负载均衡;分库分表+读写分离同时解决写容量与读性能问题。
  3. 数据流 :在最终形态中,一个 SELECT 请求首先被分片算法路由到特定的逻辑数据源,再由读写分离引擎转发到该分片的一个从库。
  4. 工程实践:架构演进并非一步到位,而是由业务增长驱动。中间件选型需前瞻性地考虑未来叠加分库分表时的兼容性。

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 服务端代理架构对比图

flowchart TD subgraph Client[客户端代理: ShardingSphere-JDBC] APP1[App] -- JVM 内嵌 --> JDBC[ReadwriteSplittingDataSource] JDBC -- 直连 TCP --> M1[(主库)] JDBC -- 直连 TCP --> S1[(从库1)] JDBC -- 直连 TCP --> S2[(从库2)] end subgraph Server[服务端代理: ProxySQL / ShardingSphere-Proxy] APP2[App] -- 标准MySQL协议:3306 --> PXY[Proxy 独立进程] PXY -- 后端连接池 --> M2[(主库)] PXY -- 后端连接池 --> S3[(从库1)] PXY -- 后端连接池 --> S4[(从库2)] end style JDBC fill:#f9f,stroke:#333 style PXY fill:#9f9,stroke:#333

图 2 说明

  1. 目的与场景:直观展示两种模式下连接拓扑、路由位置和网络跳数的根本不同。
  2. 核心元素:客户端代理路由逻辑在应用内,连接直连数据库;服务端代理以独立进程作为中间层,管理后端连接并复用。
  3. 数据流:客户端代理,SQL → 本地路由判断 → 直发 MySQL。服务端代理,SQL → 网络至代理 → 代理解析/路由 → 转发 MySQL。增加 1-3ms 网络延迟。
  4. 工程实践:服务端代理在连接收敛和运维透明性上优势巨大,尤其适合微服务大规模部署。客户端代理适合小规模或性能极致要求的场景。

3. ProxySQL 内核深度拆解

ProxySQL 是 C++ 编写的高性能、协议感知的 MySQL 代理,其设计围绕 规则引擎后台监控 展开,是实现智能读写分离的标杆。

3.1 架构与线程模型

  • Main Thread:处理配置重载、信号等。
  • MySQL Threads:处理客户端连接与认证,将请求转发给 Worker 线程。
  • Worker Threads:数量可配,负责解析 SQL、匹配规则、管理连接池、与后端 MySQL 通信。
  • Monitor Thread :独立线程,周期性连接所有被监控的 MySQL 实例,检查 read_onlySeconds_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 模块是保障读一致性的核心。其工作流程如下:

  1. 定期连接 mysql_servers 中的所有后端,执行 SHOW SLAVE STATUSSHOW GLOBAL STATUS LIKE 'wsrep%'
  2. 读取 Seconds_Behind_Master 并与 max_latency_ms 对比。
  3. 若延迟超限,从 reader_hostgroup 中删除该实例的成员记录;当延迟恢复,重新插入 reader_hostgroup
  4. 此过程对客户端完全透明,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_0ds_3,每个逻辑数据源在 READWRITE_SPLITTING 规则中被解析为一主多从。
  • SELECT * FROM t_order WHERE user_id=1 执行时,分片引擎计算出 user_id % 4 = 1,定位到 ds_1
  • 读写分离引擎接管,识别为读操作,从 replica_1_0replica_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 选型决策树

flowchart TD Start["业务读多写少,需读写分离"] --> Q1{"是否已采用 ShardingSphere 分片?"} Q1 -- "是" --> Q2{"运维能否接受无自动延迟摘除?"} Q2 -- "能,需配合Hint或脚本" --> A["ShardingSphere-Proxy
配置互通,统一管理分片与读写分离"] 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,而本文中我们通过 ReadwriteSplittingDataSourceds_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 或代理规则);二是中间件自动检测从库延迟,将延迟过大的从库暂时从读负载均衡池中摘除,保证读请求只发往延迟在阈值内的健康从库。

多角度追问

  1. 追问 :如果业务允许读旧数据,但要求旧数据不能超过 100ms,如何设计? 回答 :采用半同步复制,将延迟压缩到 10ms 以内,同时在 ProxySQL 设置 max_latency_ms=100,确保超限从库被摘除。业务侧无需改动。
  2. 追问 :半同步复制会带来写性能下降,如何在一致性和性能间平衡? 回答 :可开启"增强半同步"(rpl_semi_sync_master_wait_for_slave_count=1),仅等待一个从库确认。同时优化从库磁盘(SSD)和网络,将写入 TPS 损失控制在 10% 左右。
  3. 追问 :如何精确监控主从延迟?Seconds_Behind_Master 准吗? 回答 :可辅以 GTID 对比(GTID_SUBSET)或监控 sys.x$innodb_lock_waitsSeconds_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 STATUSSeconds_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 工具产生心跳记录来计算延迟。

多角度追问

  1. 追问Seconds_Behind_Master 为 0 是否代表没有延迟? 回答:不一定。当从库 IO 线程落后但 SQL 线程空闲时,该值可能为 0,但实际仍有未传输的 Binlog。需结合 IO 线程状态和 GTID 差值判断。
  2. 追问 :MySQL 8.0 的 source_delay 是什么?有何用? 回答CHANGE MASTER TO SOURCE_DELAY=N 可人为设置从库延迟 N 秒。常用于防止主库误操作导致从库同步删除,提供"后悔时间"。
  3. 追问 :在读写分离架构中,如何根据监控自动调整中间件路由? 回答 :ProxySQL 自动完成,无需外部干预。ShardingSphere 需编写脚本调用 DistSQL 动态修改 ReadwriteSplittingDataSource 的节点,或通过 HA 框架触发。

加分回答 : 阐述使用 SHOW SLAVE STATUSMaster_Log_FileRead_Master_Log_PosRelay_Master_Log_FileExec_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 实例)、需要灰度发布或频繁扩缩容读副本的场景,服务端代理是更优选择。

多角度追问

  1. 追问 :如何计算客户端代理的实际连接数? 回答应用实例数 × (分片数 × (主库数 + 从库数)) × 每个数据源的连接池大小。例如,100 实例 × 4 分片 × 3 节点 × 连接池 20 = 24,000 连接,远超 MySQL 连接数上限。
  2. 追问 :服务端代理的单点故障如何处理? 回答:部署多个 ProxySQL 或 ShardingSphere-Proxy 实例,通过 Keepalived / VIP 或者客户端连接串多地址实现高可用。ProxySQL 还支持集群模式同步配置。
  3. 追问 :在客户端代理中,能否通过程序实现类似 ProxySQL 的自动延迟摘除? 回答:理论上可以,但复杂且不推荐。需要通过 ZK 或配置中心下发延迟状态,应用动态调整数据源池,开发成本高且无法做到真正的无感摘除。

加分回答: 提及 ShardingSphere 的"混合架构":在微服务中,对延迟敏感的 A 服务使用 JDBC 客户端模式,对运维透明性要求高的 B 服务使用 Proxy 模式,两者配置统一管理,形成灵活的数据网格。


Q4:ProxySQL 的三层配置模型是如何工作的?mysql_replication_hostgroups 如何实现延迟感知路由?

一句话回答mysql_servers 定义后端实例,mysql_replication_hostgroups 通过 Monitor 检查 read_onlySeconds_Behind_Master 动态调整实例所属 hostgroup,mysql_query_rules 基于正则将 SQL 路由到不同 hostgroup。

详细解释(约400字): ProxySQL 的配置抽象为三层,全部通过 SQL 管理:

  1. mysql_servers :定义后端数据库的 IP、端口、权重,并指定其初始 hostgroup_id。这是静态配置的起点。
  2. mysql_replication_hostgroups :这是动态读写分离的核心。它定义了一个 writer_hostgroup 和一个 reader_hostgroup 的配对,并指定检查方式。ProxySQL 的 Monitor 模块会持续连接这些后端,根据 read_only 变量的值将节点自动分配到写组或读组。最重要的是,它会获取 Seconds_Behind_Master,如果某个从库的延迟超过了 max_latency_ms(例如 50ms),Monitor 会立刻将该从库从 reader_hostgroup 的成员列表中移除。当延迟恢复,Monitor 会将其重新加回。整个过程对客户端透明。
  3. mysql_query_rules :当客户端查询到达时,ProxySQL 的查询处理器会按顺序匹配规则。规则可以基于正则匹配 SQL 文本,命中后指定 destination_hostgroup。例如,^SELECT.*FOR UPDATE$ 可指定到写组(hostgroup 0),而普通 ^SELECT.* 指定到读组(hostgroup 1)。规则还可以附加 SET 语句,如限制查询超时时间。

这种分层设计实现了静态后端定义、动态拓扑管理与细粒度路由控制的彻底解耦。

多角度追问

  1. 追问check_type 有几种类型? 回答 :常见 read_only(检查 @@read_only),innodb_read_onlysuper_read_only。根据 MySQL 版本和配置选择。
  2. 追问 :Monitor 检查频率对摘除有什么影响? 回答check_interval_ms 决定了故障感知的响应速度。设 2000ms,则最长 2s 内感知到延迟并摘除。过小会增加后端探测负担。
  3. 追问 :如何查看当前哪些从库被延迟摘除了? 回答 :查询 mysql_server_statusmysql_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 会被透明地发送到主库,应用无需任何代码修改。同样,也可以为特定的高一致性业务查询(如写入订单后的即时查询)配置正则,强制走主库。

多角度追问

  1. 追问 :如果我想把对特定表的 SELECT 也路由到主库,如何写规则? 回答match_pattern 可写为 ^SELECT.*FROM critical_table.*,同样指向写组。
  2. 追问 :规则顺序重要吗? 回答 :极其重要。规则按 rule_id 升序匹配。应把更具体、更优先的规则(如 FOR UPDATE)ID 设小,通用规则(如所有 SELECT)设大,防止被提前匹配走。
  3. 追问 :规则中的 error_msg 有何用? 回答:可用于 SQL 防火墙,当匹配到危险 SQL 模式时,不路由而是直接返回自定义错误信息给客户端,实现拦截。

加分回答 : 介绍 stats_mysql_query_rules 表,用于监控每条规则的命中次数和延迟,分析规则有效性并优化顺序,形成闭环。


Q6:ShardingSphere-Proxy 的 ReadwriteSplittingDataSource 如何与分片数据源嵌套?

一句话回答 :分片规则中的 actualDataNodes 引用逻辑数据源,这些逻辑数据源在 READWRITE_SPLITTING 规则中被定义为一主多从的组合,路由时先分片后读写分离。

详细解释 (约300字): ShardingSphere 通过逻辑数据源 实现嵌套。在配置中,分片算法计算出的结果是逻辑数据源名,如 ds_0actualDataNodes 表达式为 ds_${0..3}.t_order_${0..3}。 而在 READWRITE_SPLITTING 规则下,ds_0 不是一个具体的物理数据库,而是一个组合数据源,包含 writeDataSourceName: primary_0readDataSourceNames: replica_0_0, replica_0_1。 运行时,SQL 首先经过分片引擎,根据分片键计算出目标逻辑数据源 ds_0 和表 t_order_0。然后交给读写分离引擎,判断 SQL 类型(读/写),从 ds_0 的写库或读库中选择一个物理数据源执行。对于读操作,使用配置的负载均衡算法(ROUND_ROBIN / WEIGHT 等)选择具体从库。 这种两层嵌套使得分片拓扑和读写拓扑可以独立变化,且全部由配置管理。

多角度追问

  1. 追问 :在事务中,读操作会走从库吗? 回答:不会。ShardingSphere 的事务引擎发现存在写操作后,会将事务内的所有读操作也强制路由到主库,避免读旧数据。
  2. 追问 :如何在线增加一个从库? 回答 :通过 DistSQL 动态执行 ALTER READWRITE_SPLITTING RULE ds_0 ADD READ DATA SOURCE replica_0_2(...),立即生效。
  3. 追问 :如果 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 则追求代理层的智能自治,尽可能在中间件层面屏蔽后端差异。

多角度追问

  1. 追问 :InnoDB Cluster 内部有流控机制,是否能替代 ProxySQL 的延迟摘除? 回答:InnoDB Cluster 的流控机制在写入端通过控制主库 Binlog 生成速度来减小延迟,是推模式。ProxySQL 在读取端通过摘除延迟从库,是拉模式。两者互补,但 Router 自身不感知,如果从库因其他原因(如 SQL 线程阻塞)延迟,Router 无能为力。
  2. 追问 :能否通过外部脚本为 MySQL Router 添加延迟感知? 回答:理论上可以,脚本监控延迟并修改 Router 的配置文件然后 reload,但 Router 的 reload 并非毫秒级无损,且存在窗口期,远不如 ProxySQL 的 Monitor 机制高效。
  3. 追问 :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 解析器,能够自动区分 SELECTINSERTUPDATEDELETE 等语句,将写操作发往主库,读操作发往从库。它的优点是配置简单,开箱即用,对于标准的读写分离需求非常高效。但缺点是灵活性不足,比如无法将特定表的 SELECT 强制路由到主库,或者对 SELECT...FOR UPDATE 的特殊处理不够透明。 ProxySQL 则完全不内置分类逻辑,完全依赖用户定义的 mysql_query_rules。通过正则表达式,可以匹配 SQL 文本、客户端用户名、Schema 名称等,然后指定目标 hostgroup。这种设计赋予了 DBA 和开发者极大的定制空间,可以应对各种复杂的业务路由需求,代价是需要编写和维护规则。

多角度追问

  1. 追问 :MaxScale 能区分 SELECT...FOR UPDATE 吗? 回答 :可以,它将其识别为读操作,但会智能地将其路由到主库,因为 readwritesplit 会检测到 FOR UPDATE 子句。但这属于内置逻辑,无法修改。
  2. 追问 :ProxySQL 的正则路由在高并发下性能如何? 回答:ProxySQL 使用高效的 RE2 正则引擎,开销极低。在 7000+ TPS 下,规则匹配带来的 CPU 开销通常 <5%。
  3. 追问 :如果想在 MaxScale 中实现类似 ProxySQL 的自定义路由,怎么做? 回答 :需要编写 MaxScale 的 filter 插件,用 C 语言开发,开发和维护成本远高于 ProxySQL 的 SQL 规则。

加分回答 : 分析 ProxySQL 的 mysql_query_rules 中的 replace_pattern 功能,可实现 SQL 改写,例如在线修改查询超时时间、添加注释等,MaxScale 的读写分离模块不具备此类内联改写能力。


Q9:读写分离后,Seata AT 的全局事务为什么必须写主库、读主库?

一句话回答 :写主库是为了获取全局锁和写入 undo_log;读主库是为了避免从库延迟导致读不到已提交的分支事务数据,破坏读已提交隔离级别。

详细解释(约350字): Seata AT 模式下,全局事务被拆分为多个分支事务。每个分支事务需要:

  1. 写操作 :在主库上执行原始 SQL,同时在同一事务内插入 undo_log,用于回滚。这些操作必须落在主库,因为主库是唯一的写入源,也是全局锁的持有者。
  2. 读操作 :Seata 默认的隔离级别是"读已提交"。如果分支事务 A 提交后,分支事务 B 的读请求落到了从库,由于主从延迟,B 可能读不到 A 提交的数据,导致脏读。为保证隔离性,Seata 在 AT 模式下强制所有分支事务的 SQL 都路由到主库执行。

实现方式 :Seata 的数据源代理(SeataDataSourceProxy)会在事务上下文中强制将连接指向主库。因此,读写分离中间件的规则对 Seata 事务内的 SQL 应当是完全透明的,即无论中间件如何配置,Seata 都会控制连接绕过读写分离。

多角度追问

  1. 追问 :Seata TCC 模式也强制读写主库吗? 回答:TCC 模式下,业务方手动实现 Try/Confirm/Cancel,读操作通常在业务代码中控制,但原则一样:对资源状态的读取应基于最新数据,所以建议读主库。
  2. 追问 :如果业务能容忍最终一致,能否让 Seata 事务中的某些读走从库? 回答:不可以。这违背了全局事务的隔离性保证,可能导致业务逻辑基于过期数据做出错误决策,引发资金或库存问题。
  3. 追问 :如何在 Seata 中配置让某些非事务性只读查询走从库? 回答:只有那些完全不在 Seata 全局事务管理下的普通查询,才会被读写分离中间件接管路由。业务代码需保证线程上下文不处于分布式事务中。

加分回答 : 探讨 Seata AT 与 ShardingSphere 整合时,数据源嵌套顺序必须是 SeataDataSourceProxy( ShardingSphereDataSource( ReadwriteSplittingDataSource ... ) ),确保 Seata 代理在最外层,这样才能在事务开始时获取正确的主库连接。


Q10:读写分离的从库负载均衡算法有哪些?ROUND_ROBINWEIGHT 分别适合什么场景?

一句话回答ROUND_ROBIN 轮询适合同配从库,RANDOM 随机,WEIGHT 权重适合异构从库,可按能力分配不同流量比例。

详细解释(约250字):

  • ROUND_ROBIN:依次循环选择从库,每个从库得到的请求数均匀分布。这是最常用的算法,前提是所有从库硬件配置、网络延迟完全一致。
  • RANDOM:随机选择,分布也趋于均匀,实现简单。
  • WEIGHT :为每个从库分配权重,如 replica_0: 2, replica_1: 1,则读请求按 2:1 的比例分发。这适用于从库规格不一的场景,例如 replica_0 是 8C16G 高性能实例,replica_1 是 4C8G 低配,权重可以让流量与能力匹配。还可用于灰度发布:将低权重新版本从库先上线,验证性能后调高权重。

多角度追问

  1. 追问 :最少连接数算法为什么在数据库代理中少见? 回答:因为数据库代理通常使用连接池复用,连接数相对稳定,不能真实反映后端 MySQL 的负载(CPU/IO)。基于连接数的负载均衡容易导致倾斜。
  2. 追问 :如何在 ProxySQL 中实现权重负载? 回答 :在 mysql_servers 表中直接设置每个实例的 weight 字段即可,110000000 之间。
  3. 追问 :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 强制应用端所有查询走主库,但这会放弃读扩展性。

多角度追问

  1. 追问 :ProxySQL 全部从库延迟超标怎么办? 回答 :可通过 writer_is_reader=1 将写组也临时作为读组,让读流量下沉到主库,牺牲主库性能换取可用性。
  2. 追问 :如何避免因网络抖动导致的频繁摘除和加回? 回答 :ProxySQL 的 mysql_replication_hostgroups 没有内置抖动抑制,但可通过外部脚本或调整 check_interval_msmax_latency_ms 的配合来缓解。
  3. 追问 :在 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融合示意)

flowchart LR subgraph App ["应用层 100实例"] APP["订单服务"] end subgraph Proxy ["服务端代理层"] direction TB P1["ProxySQL-1
读写分离"] 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下写后读强一致性

sequenceDiagram participant Svc as 订单服务 participant JDBC as ShardingSphere-JDBC participant PXY as ProxySQL participant M1 as 主库 primary_1 participant S1 as 从库 replica_1_0 Svc->>JDBC: INSERT INTO t_order ... JDBC->>JDBC: 分片 user_id %4 -> ds_1 JDBC->>PXY: 写SQL (hostgroup 0) PXY->>M1: 执行写入 M1-->>PXY: OK PXY-->>JDBC: OK JDBC-->>Svc: 写入成功 Svc->>JDBC: SELECT * FROM t_order WHERE order_id=... JDBC->>JDBC: 分片 -> ds_1 JDBC->>PXY: 读SQL (含FOR UPDATE 或 匹配规则) Note over PXY: 规则匹配:强制路由至 hostgroup 0 PXY->>M1: 执行查询 M1-->>PXY: 返回最新数据 PXY-->>JDBC: 结果 JDBC-->>Svc: 订单详情

三、方案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: ... # 类似

延迟飙升处理流程图

flowchart TD Monitor[外部监控 Prometheus] -->|Seconds_Behind_Master > 50ms| Alert[触发告警] Alert --> Script[自动化脚本] Script -->|DistSQL| PXY[ShardingSphere-Proxy] PXY -->|ALTER READWRITE_SPLITTING RULE ds_1 DROP READ DATA SOURCE replica_1_0| Update[规则更新] Update -->|读流量切换至 replica_1_1| OK[延迟从库摘除] Recover[监控到延迟恢复] --> Script2[脚本] Script2 -->|DistSQL| PXY2[ShardingSphere-Proxy] PXY2 -->|ADD READ DATA SOURCE replica_1_0| Update2[加回读组]

同时,应用侧针对核心写后读链路预埋 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_hostgroupsmysql_query_rules 章节
  • Apache ShardingSphere 官方文档:Readwrite Splitting 部分
  • MySQL Router 官方文档:Routing 与 InnoDB Cluster 集成
  • MaxScale 官方文档:readwritesplit 路由模块
  • 《高性能 MySQL》第 9 章:读写分离
相关推荐
Mahir082 小时前
Redis 分布式锁与 Redisson 深度解析:从原生实现到工业级解决方案
数据库·redis·分布式·缓存·面试
敖正炀2 小时前
分布式事务监控与手动恢复平台设计
分布式
逆境不可逃3 小时前
Hello-Agents 第二部分-第四章总结:智能体经典范式构建-包含习题解析和Java版
java·开发语言·javascript·人工智能·分布式·agent
heimeiyingwang3 小时前
【架构实战】RocketMQ实战:分布式消息中间件
分布式·架构·rocketmq
报错小能手3 小时前
分布式讲解—分布式事务解决方案 刚性(2PC、3PC、XA协议)
分布式
Evand J1 天前
【MATLAB例程】5个UAV 分布式围捕编队运动仿真 —— 基于PID控制
开发语言·分布式·matlab
蓝眸少年CY1 天前
Spark - Code 核心教程
大数据·分布式·spark
敖正炀1 天前
CAP 定理、BASE 理论与一致性模型深度
分布式
勤自省1 天前
ROS2分布式通信与Launch文件实战:从踩坑到打通(第12-20讲总结)
分布式·ubuntu·ros2·gazebo·launch·rqt·rviz2