MySQL Connector/J 负载均衡与故障转移实践
MySQL Connector/J 负载均衡与故障转移实践
前言
在 Java 应用中连接 MySQL,最常见的方式是使用 MySQL Connector/J。除了普通单主机连接外,Connector/J 还支持多主机连接模式,用于实现一定程度的故障转移、负载均衡和读写分离。
不过需要先明确一点:
Connector/J 的多主机能力不是完整的数据库高可用方案,它只能解决“客户端连接如何选择或切换 MySQL 主机”的一部分问题。
真正的生产高可用,还需要结合 MySQL 复制、MGR / InnoDB Cluster、MySQL Router、ProxySQL、负载均衡器、连接池、应用重试和监控告警一起设计。
本文重点整理 Connector/J 中常见的三类多主机连接模式:
- Failover:故障转移;
- LoadBalance:负载均衡;
- Source/Replica Replication:读写分离;
- DNS SRV:通过 DNS 服务发现主机。
同时会说明一些容易踩坑的点,例如 autoReconnect 不推荐、事务失败不能无脑重试、synchronized 式的“自动切换”不等于业务无感。
Connector/J 多主机模式概览
Connector/J 常见 JDBC URL 如下。
| 模式 | JDBC URL 示例 | 典型用途 |
|---|---|---|
| 普通连接 | jdbc:mysql://host:3306/db |
单实例或代理入口 |
| Failover | jdbc:mysql://host1:3306,host2:3306/db |
主机不可用时切换 |
| LoadBalance | jdbc:mysql:loadbalance://host1:3306,host2:3306/db |
多个可写节点之间分摊连接 |
| Replication | jdbc:mysql:replication://source:3306,replica1:3306/db |
Source / Replica 读写分离 |
| DNS SRV | jdbc:mysql+srv://service.example.com/db |
通过 DNS SRV 发现主机 |
实践中需要根据数据库拓扑选择模式:
- 单主多从:更适合 Replication 或使用代理层;
- 多主可写:可以考虑 LoadBalance,但要确认写入冲突和一致性模型;
- 主备高可用:可以考虑 Failover,但仍需应用处理异常;
- Kubernetes / 动态主机发现:可以评估 DNS SRV 或代理入口。
Failover:主机故障后的连接切换
Failover URL 的基本形式:
jdbc:mysql://source-host:3306,backup-host-1:3306,backup-host-2:3306/app_db
常见参数示例:
jdbc:mysql://source-host:3306,backup-host:3306/app_db\
?failOverReadOnly=true\
&secondsBeforeRetrySource=30\
&queriesBeforeRetrySource=50\
&retriesAllDown=120\
&connectTimeout=3000\
&socketTimeout=10000
Failover 的基本思想是:当当前连接主机不可用时,驱动尝试切换到底层列表中的其他主机。
但要注意:
Failover 不是业务完全无感。连接切换通常发生在出现连接异常之后,应用仍然需要处理
SQLException,并在合适的事务边界进行重试。
failOverReadOnly
failOverReadOnly 控制故障转移到非 Source 主机后,连接是否设置为只读。
failOverReadOnly=true
如果备机或 Replica 不应该承载写请求,建议保持只读,避免故障期间误写入错误节点。
需要注意:只读是连接层语义,并不等于数据库权限控制。生产环境仍应在数据库账号、拓扑和代理层面限制写入路径。
secondsBeforeRetrySource
secondsBeforeRetrySource 表示故障转移后,多久尝试重新回到 Source 主机。
secondsBeforeRetrySource=30
适合在 Source 临时不可用时,让连接在一段时间后尝试恢复到原 Source。
queriesBeforeRetrySource
queriesBeforeRetrySource 表示故障转移后,执行多少次查询后尝试回到 Source。
queriesBeforeRetrySource=50
它和 secondsBeforeRetrySource 可以配合使用。一个按时间,一个按查询次数控制回切尝试。
retriesAllDown
retriesAllDown 表示当所有主机都不可用时,驱动的重试次数。
retriesAllDown=120
这个参数只能帮助连接层继续尝试,并不能保证业务请求成功。超过重试限制后,应用仍然会收到异常。
为什么不推荐 autoReconnect?
很多旧文章会建议:
autoReconnect=true
但生产环境不建议依赖 autoReconnect 作为高可用方案。
原因是连接断开后,重连出来的是一个新的物理连接,原连接上的会话状态可能已经丢失,例如:
- 临时表;
- session variables;
- user variables;
- 当前事务;
- prepared statement 状态;
- lock 状态;
- 当前数据库或字符集等 session 级状态。
因此更推荐的方式是:
- 连接池负责检测失效连接并剔除;
- 应用捕获
SQLException; - 在明确的事务边界整体重试;
- 幂等性由业务层保证;
- 不假设某条 SQL 可以被驱动安全自动重放。
简单说:
autoReconnect解决的是“尝试重新连上”,不是“保证业务语义正确”。
LoadBalance:多主机分摊连接
LoadBalance URL 的基本形式:
jdbc:mysql:loadbalance://mysql-1:3306,mysql-2:3306,mysql-3:3306/app_db
示例:
jdbc:mysql:loadbalance://mysql-1:3306,mysql-2:3306,mysql-3:3306/app_db\
?ha.loadBalanceStrategy=random\
&loadBalanceConnectionGroup=app-cluster\
&ha.enableJMX=true\
&loadBalanceHostRemovalGracePeriod=300000\
&connectTimeout=3000\
&socketTimeout=10000
LoadBalance 适用于多个主机都可以承载同类请求的场景。例如多主写入、多个等价只读节点、或者上层已经保证数据一致性的拓扑。
如果是普通 MySQL 主从复制架构,不建议简单把 Source 和 Replica 放进 loadbalance:// 里承载所有请求,否则写请求可能打到 Replica。
ha.loadBalanceStrategy
当前 Connector/J 文档推荐使用 ha.loadBalanceStrategy 指定负载均衡策略。
常见策略包括:
| 策略 | 说明 | 适用场景 |
|---|---|---|
random |
随机选择主机 | 默认通用场景 |
sequential |
按顺序选择 | 希望稳定优先级 |
bestResponseTime |
根据响应时间选择 | 追求低延迟 |
serverAffinity |
优先固定主机 | 希望连接尽量落在指定主机 |
示例:
ha.loadBalanceStrategy=bestResponseTime
旧资料中常见的 roundRobinLoadBalance 容易和新版本参数体系混淆,建议新文章和新配置优先参考当前官方文档。
loadBalanceConnectionGroup 与 ha.enableJMX
如果需要通过 JMX 管理负载均衡连接,可以配置连接组:
loadBalanceConnectionGroup=app-cluster
ha.enableJMX=true
这样可以在运行时观察和管理某些负载均衡连接行为。
loadBalanceHostRemovalGracePeriod
loadBalanceHostRemovalGracePeriod 用于控制主机从负载均衡列表中移除前的宽限时间。
需要注意单位是毫秒。
loadBalanceHostRemovalGracePeriod=300000
上面表示约 5 分钟。不要误写成:
loadBalanceHostRemovalGracePeriod=300
后者只是 300 毫秒,并不是 300 秒。
事务边界与连接切换
负载均衡连接并不意味着一笔事务中的每条 SQL 都可以随意切到不同主机。
生产中需要注意:
- 一个事务应绑定到同一个物理连接;
- 事务中连接失败后,应整体回滚并重新执行业务流程;
- 不要在事务中依赖驱动对单条 SQL 做自动重试;
- 非幂等写操作必须由业务层保证重试安全。
Source/Replica Replication:读写分离
Replication URL 的基本形式:
jdbc:mysql:replication://source-host:3306,replica-1:3306,replica-2:3306/app_db
示例:
jdbc:mysql:replication://source-host:3306,replica-1:3306,replica-2:3306/app_db\
?readFromSourceWhenNoReplicas=true\
&connectTimeout=3000\
&socketTimeout=10000
Connector/J 的 Replication 模式通常通过连接的只读状态选择 Source 或 Replica:
Connection connection = dataSource.getConnection();
// 写操作:使用 Source
connection.setReadOnly(false);
try (PreparedStatement ps = connection.prepareStatement(
"insert into user(name) values (?)")) {
ps.setString(1, "Alice");
ps.executeUpdate();
}
// 读操作:可以使用 Replica
connection.setReadOnly(true);
try (PreparedStatement ps = connection.prepareStatement(
"select * from user where name = ?")) {
ps.setString(1, "Alice");
ps.executeQuery();
}
readFromSourceWhenNoReplicas
旧文档和旧资料中常见 readFromMasterWhenNoSlaves。现在 MySQL 官方文档已经使用 Source / Replica 术语,建议改用:
readFromSourceWhenNoReplicas=true
含义是:当没有可用 Replica 时,读请求是否回退到 Source。
这个参数要谨慎使用:
- 设置为
true:可用性更高,但 Source 压力可能上升; - 设置为
false:避免 Source 被读流量压垮,但 Replica 不可用时读请求会失败。
读写分离的一致性风险
读写分离最大的风险不是 JDBC URL,而是复制延迟。
例如:
写入 Source 成功
↓
马上从 Replica 查询
↓
Replica 尚未追上复制进度
↓
读不到刚写入的数据
因此,如果业务需要“写后立刻读到”,要考虑:
- 写后读强制走 Source;
- 根据事务上下文选择连接;
- 使用中间件或框架做读写路由;
- 监控复制延迟;
- 对关键读请求避免走 Replica。
DNS SRV:通过 DNS 发现主机
Connector/J 支持 DNS SRV 方式发现 MySQL 主机,例如:
jdbc:mysql+srv://_mysql._tcp.example.com/app_db
DNS SRV 适合主机列表可能变化的环境,但也要注意:
- DNS 缓存时间;
- JVM DNS 缓存策略;
- 域名解析故障;
- 与连接池长连接的配合;
- 是否符合当前基础设施规范。
在 Kubernetes 或云环境中,如果已经有稳定的 Service、MySQL Router 或 ProxySQL,通常也可以直接连接代理入口。
与连接池结合使用
大多数 Java 应用不会直接裸用 DriverManager,而是通过连接池,例如 HikariCP。
示例配置:
spring.datasource.hikari.jdbc-url=jdbc:mysql://source-host:3306,backup-host:3306/app_db?failOverReadOnly=true&connectTimeout=3000&socketTimeout=10000
spring.datasource.hikari.username=app_user
spring.datasource.hikari.password=******
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.connection-timeout=3000
spring.datasource.hikari.validation-timeout=1000
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.keepalive-time=300000
建议:
- 不要依赖
autoReconnect=true; - 让连接池负责检测和回收失效连接;
- 设置合理的
connectionTimeout和validationTimeout; maxLifetime不要超过数据库或网络设备的连接回收时间;- 对事务失败做业务层整体重试;
- 给关键异常打日志,方便判断是连接故障、主机切换还是数据库异常。
异常处理与重试建议
数据库连接高可用的关键不只是“能不能重连”,而是“失败后业务语义是否正确”。
建议按以下原则处理:
1. 只在事务边界重试
不要在事务中间对某条 SQL 盲目重试。正确做法通常是:
开始事务
↓
执行业务 SQL
↓
提交失败或连接异常
↓
回滚 / 丢弃连接
↓
重新开始整个业务流程
2. 只重试幂等操作
查询通常可以重试,但写操作要特别谨慎。
对于写操作,应该通过业务唯一键、请求号、幂等表、去重逻辑等方式保证重复执行不会造成脏数据。
3. 区分连接异常和业务异常
连接异常、死锁、唯一键冲突、SQL 语法错误,不应该使用同一套重试策略。
常见连接类异常通常会体现在 SQLState、异常类型或底层通信异常中。建议结合连接池、日志和监控统一分类。
生产环境建议
1. 优先评估代理层方案
如果目标是生产级高可用,建议优先评估:
- MySQL Router;
- ProxySQL;
- 云数据库代理;
- Kubernetes Service;
- SLB / LVS / HAProxy。
代理层可以把拓扑变化、主从切换、健康检查、读写路由等能力从应用中剥离出来,应用侧只连接一个稳定入口。
2. 应用仍然必须处理 SQLException
不管使用 Connector/J Failover、LoadBalance 还是代理层,应用都不能假设数据库故障完全无感。
网络抖动、事务提交中断、连接池旧连接、主从切换窗口,都可能让业务看到异常。
3. 写请求不要透明切到 Replica
如果 Replica 没有写权限,写请求会失败;如果 Replica 被错误开放写权限,风险更大,可能导致数据分叉。
写路径应明确指向 Source 或可写主节点。
4. 读写分离要监控复制延迟
读写分离不是简单地把 select 全部丢给 Replica。
需要关注:
- 复制延迟;
- 写后读一致性;
- 大事务造成的延迟;
- Replica 故障后的流量回退策略。
5. 配置要区分驱动层和连接池层
Connector/J 参数控制驱动行为;HikariCP 等连接池参数控制连接生命周期、校验、池大小和超时。
两者解决的问题不同,不能混为一谈。
常见配置示例
Failover 示例
jdbc:mysql://mysql-source:3306,mysql-backup:3306/app_db\
?failOverReadOnly=true\
&secondsBeforeRetrySource=30\
&queriesBeforeRetrySource=50\
&retriesAllDown=120\
&connectTimeout=3000\
&socketTimeout=10000
LoadBalance 示例
jdbc:mysql:loadbalance://mysql-1:3306,mysql-2:3306,mysql-3:3306/app_db\
?ha.loadBalanceStrategy=random\
&loadBalanceConnectionGroup=app-cluster\
&ha.enableJMX=true\
&loadBalanceHostRemovalGracePeriod=300000
Source/Replica 示例
jdbc:mysql:replication://mysql-source:3306,mysql-replica-1:3306,mysql-replica-2:3306/app_db\
?readFromSourceWhenNoReplicas=true\
&connectTimeout=3000\
&socketTimeout=10000
不推荐示例
jdbc:mysql://mysql-source:3306/app_db?autoReconnect=true
不推荐把 autoReconnect=true 当成生产高可用方案。它可能掩盖连接异常,并带来 session state 丢失和事务语义不确定的问题。
总结
MySQL Connector/J 提供了多主机连接能力,但它解决的是“客户端连接如何选择和切换主机”的问题,而不是完整的数据库高可用问题。
实践建议:
- Failover 适合主机故障后的连接切换,但应用必须处理异常;
- LoadBalance 适合多个等价节点分摊请求,不适合随意混放 Source 和 Replica;
- Replication 模式可以做读写分离,但要关注复制延迟和写后读一致性;
autoReconnect不推荐作为生产高可用方案;- 连接池负责连接生命周期,业务层负责事务边界和幂等重试;
- 生产环境优先评估 MySQL Router、ProxySQL 或云数据库代理等统一入口。
一句话总结:
Connector/J 可以帮助应用连接多个 MySQL 主机,但真正可靠的高可用,需要驱动、连接池、数据库拓扑、代理层和业务重试共同配合。