MySQL 高可用架构终极选型指南:MGR、主从 + Keepalived、MyCat 全维度拆解与生产避坑指南

简介: 本文系统解析MySQL三大高可用架构:主从+Keepalived(成熟稳定、零侵入)、MGR(强一致、自动自愈)和MyCat(分库分表+读写分离)。涵盖核心指标(RTO/RPO/一致性/自愈能力等)、配置实例、优劣势及选型决策矩阵,助力企业精准匹配业务需求。

一、MySQL高可用的核心标尺与选型维度

在分布式业务架构中,数据库是整个系统的状态核心,单点故障会直接导致全业务瘫痪。MySQL高可用架构的核心目标,是通过冗余设计解决单点故障问题,保障业务连续性与数据安全性。在选型前,我们先明确统一的评估维度,所有方案都将基于以下核心指标进行拆解:

  • RTO(恢复时间目标):故障发生后,业务恢复正常服务的最长耗时
  • RPO(恢复点目标):故障发生后,业务允许丢失的最大数据量
  • 数据一致性:故障切换前后,数据是否保持一致,是否存在丢失、脏写风险
  • 故障自愈能力:故障处理是否需要人工介入,能否自动完成检测、选主、切换全流程
  • 性能损耗:高可用机制对数据库读写性能的影响幅度
  • 运维复杂度:架构搭建、日常维护、扩容、故障排查的技术门槛
  • 扩展性:能否通过横向扩展提升数据库的读写能力
  • 业务侵入性:是否需要改造业务代码,对现有业务的适配成本

二、主从复制 + Keepalived 经典高可用架构

主从复制+Keepalived是业界使用最广泛、成熟度最高的MySQL高可用方案,适用于绝大多数中小规模业务场景。

2.1 底层核心原理

该架构分为两层核心能力:

  1. MySQL主从复制:基于binlog的增量数据同步机制。主库将数据变更写入二进制日志binlog,从库的IO线程拉取binlog写入本地中继日志relay log,SQL线程重放中继日志中的事务,最终实现主从节点的数据同步。MySQL 8.0默认使用增强半同步复制,确保事务在从库收到binlog后才在主库提交,最大程度降低数据丢失风险。
  2. Keepalived:基于VRRP(虚拟路由冗余协议)实现虚拟IP(VIP)的漂移。通过定时心跳检测主节点的健康状态,当主库宕机或服务不可用时,自动将VIP漂移到从节点,业务无需修改连接地址,实现无感知切换。

2.2 架构拓扑图

2.3 生产级配置实例

2.3.1 MySQL 8.0 主库配置(my.cnf)

[mysqld]

server-id=1

gtid_mode=ON

enforce_gtid_consistency=ON

binlog_format=ROW

log_bin=mysql-bin

binlog_row_image=FULL

relay_log=relay-bin

log_slave_updates=ON

master_info_repository=TABLE

relay_log_info_repository=TABLE

sync_binlog=1

innodb_flush_log_at_trx_commit=1

plugin_load_add = semisync_master.so

rpl_semi_sync_master_enabled=1

rpl_semi_sync_master_wait_for_slave_count=1

rpl_semi_sync_master_wait_point=AFTER_SYNC

read_only=OFF

super_read_only=OFF

2.3.2 MySQL 8.0 从库配置(my.cnf)

[mysqld]

server-id=2

gtid_mode=ON

enforce_gtid_consistency=ON

binlog_format=ROW

log_bin=mysql-bin

binlog_row_image=FULL

relay_log=relay-bin

log_slave_updates=ON

master_info_repository=TABLE

relay_log_info_repository=TABLE

sync_binlog=1

innodb_flush_log_at_trx_commit=1

plugin_load_add = semisync_slave.so

rpl_semi_sync_slave_enabled=1

read_only=ON

super_read_only=ON

2.3.3 主从复制搭建SQL

主库创建复制用户:

SET SQL_LOG_BIN=0;
CREATE USER 'repl'@'%' IDENTIFIED BY 'Repl@123456';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
GRANT BACKUP_ADMIN ON *.* TO 'repl'@'%';
SET SQL_LOG_BIN=1;

从库配置主从连接并启动复制:

CHANGE MASTER TO
MASTER_HOST='192.168.1.100',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='Repl@123456',
MASTER_AUTO_POSITION=1;
START SLAVE;

主从状态校验:

SHOW SLAVE STATUS\G

2.3.4 Keepalived 核心配置

Master节点keepalived.conf:

! Configuration File for keepalived

global_defs {

  router_id MySQL_HA

}

vrrp_script chk_mysql {

   script "/etc/keepalived/chk_mysql.sh"

   interval 2

   weight -20

   fall 3

   rise 2

}

vrrp_instance VI_1 {

   state BACKUP

   interface eth0

   virtual_router_id 51

   priority 100

   nopreempt

   advert_int 1

   authentication {

       auth_type PASS

       auth_pass 1111

   }

   virtual_ipaddress {

       192.168.1.200/24

   }

   track_script {

       chk_mysql

   }

}

Backup节点仅需修改priority为90,其余配置保持一致。

MySQL健康检测脚本chk_mysql.sh:

#!/bin/bash
mysql -uroot -p'Root@123456' -e "SELECT 1;" > /dev/null 2>&1
if [ $? -ne 0 ]; then
   systemctl stop keepalived
   exit 1
fi
exit 0

2.4 优劣势分析

核心优势

  1. 方案成熟稳定,社区资料丰富,运维门槛极低,绝大多数DBA均可快速掌握
  2. 性能损耗极小,异步复制损耗低于5%,增强半同步复制损耗低于10%
  3. 业务零侵入,所有基于MySQL的业务均可直接适配,无需任何代码改造
  4. 部署灵活,支持一主一从、一主多从架构,可搭配读写分离无限扩展读能力

核心劣势

  1. 数据一致性存在短板:异步复制存在明确的数据丢失风险,增强半同步在主库宕机且binlog未同步到从库的极端场景下,仍有数据丢失可能
  2. 脑裂风险:VRRP协议在网络分区场景下,可能出现双主节点同时持有VIP的情况,导致数据冲突与脏写
  3. 故障自愈能力有限:仅能实现VIP漂移,主从切换后需要人工修复主从关系,无法自动恢复集群架构
  4. 写能力无法扩展:单主架构决定了写性能无法通过横向扩展提升

2.5 生产落地核心注意事项

  1. 必须使用GTID模式,彻底解决传统复制的位点偏移问题,大幅简化故障切换与主从修复流程
  2. 强制开启增强半同步复制,设置rpl_semi_sync_master_wait_point=AFTER_SYNC,确保事务在从库收到binlog后再提交
  3. Keepalived必须配置非抢占模式nopreempt,避免主节点恢复后VIP回切导致的业务闪断
  4. 脑裂防护:配置双网卡心跳、第三方仲裁节点,健康检测脚本必须在MySQL不可用时自动停止Keepalived服务
  5. 主从节点硬件配置必须完全对称,避免从库出现复制延迟,同时配置监控告警,延迟超过1s立即触发告警
  6. 每月至少执行一次主从切换演练,验证切换流程的可用性,同时完成主从关系修复

三、MySQL Group Replication(MGR)官方原生强一致高可用架构

MGR是MySQL官方推出的基于分布式共识协议的高可用方案,原生支持强数据一致性与自动故障自愈,是金融级业务的首选方案。

3.1 底层核心原理

MGR基于Paxos分布式共识协议实现,集群内所有节点通过多数派投票机制达成数据共识,确保所有节点的数据完全一致。核心逻辑分为三个部分:

  1. 事务执行流程:客户端提交事务到主节点,主节点将事务广播到集群内所有节点,所有节点对事务进行冲突检测与合法性校验,超过半数节点认证通过后,事务才能正式提交,确保集群内数据一致性。
  2. 故障检测机制:集群内节点通过心跳机制互相检测状态,当某个节点失联超过阈值,集群内超过半数节点会判定该节点故障,自动将其踢出集群,避免故障节点影响集群可用性。
  3. 自动选主与切换:单主模式下,主节点故障后,集群会自动根据节点权重、server-id等规则选举新的主节点,搭配MySQL Router可实现业务无感知的写请求切换,故障节点恢复后可自动重新加入集群。

3.2 架构拓扑图

3.3 生产级配置实例

MGR生产环境必须使用奇数节点集群,推荐3节点单主模式,以下为MySQL 8.0 核心配置。

3.3.1 节点通用配置(my.cnf)

[mysqld]

server-id=1

gtid_mode=ON

enforce_gtid_consistency=ON

binlog_format=ROW

binlog_row_image=FULL

log_bin=mysql-bin

log_slave_updates=ON

master_info_repository=TABLE

relay_log_info_repository=TABLE

relay_log=relay-bin

transaction_write_set_extraction=XXHASH64

loose-group_replication_group_name="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"

loose-group_replication_start_on_boot=OFF

loose-group_replication_local_address= "192.168.1.101:33061"

loose-group_replication_group_seeds= "192.168.1.101:33061,192.168.1.102:33061,192.168.1.103:33061"

loose-group_replication_bootstrap_group=OFF

loose-group_replication_single_primary_mode=ON

loose-group_replication_enforce_update_everywhere_checks=OFF

loose-group_replication_ip_allowlist="192.168.1.0/24"

sync_binlog=1

innodb_flush_log_at_trx_commit=1

disabled_storage_engines="MyISAM,BLACKHOLE,FEDERATED,ARCHIVE,MEMORY"

其余两个节点仅需修改server-idloose-group_replication_local_address为对应节点的配置。

3.3.2 集群搭建步骤

所有节点创建复制用户:

SET SQL_LOG_BIN=0;
CREATE USER 'repl'@'%' IDENTIFIED BY 'Repl@123456';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
GRANT BACKUP_ADMIN ON *.* TO 'repl'@'%';
SET SQL_LOG_BIN=1;

主节点启动集群:

SET GLOBAL group_replication_bootstrap_group=ON;
START GROUP_REPLICATION;
SET GLOBAL group_replication_bootstrap_group=OFF;

从节点加入集群:

START GROUP_REPLICATION;

集群状态校验:

SELECT * FROM performance_schema.replication_group_members;

3.3.3 MySQL Router 配置

MySQL Router用于实现读写分离与自动故障切换,执行以下命令完成bootstrap配置:

mysqlrouter --bootstrap root@192.168.1.101:3306 --user=mysqlrouter

配置完成后,业务通过6446端口访问写服务,6447端口访问读服务,主节点故障后,Router会自动将写请求转发到新选举的主节点。

3.4 优劣势分析

核心优势

  1. 原生强数据一致性:基于Paxos多数派共识协议,只要集群内超过半数节点存活,就能保证RPO=0,数据零丢失,完全满足金融级合规要求
  2. 完善的故障自愈能力:自动完成故障检测、选主、切换、节点重加入全流程,无需人工介入,RTO可控制在30s以内
  3. 无脑裂风险:基于多数派机制,网络分区时只有持有多数节点的分区能提供服务,彻底避免双主数据冲突
  4. 官方原生支持:与MySQL内核深度整合,版本迭代同步,兼容性最佳,无需引入第三方组件
  5. 支持多主模式:可实现多节点同时写入,解决单主架构的写性能瓶颈

核心劣势

  1. 性能损耗较高:事务需要广播到所有节点并经过多数派认证,写性能比主从架构低15%-30%,集群节点越多,性能损耗越大
  2. 运维复杂度高:对DBA的技术能力要求极高,需要掌握分布式共识协议、事务冲突检测、分布式事务等底层知识,故障排查难度大
  3. 严格的使用限制:仅支持InnoDB引擎,所有表必须有显式主键,不支持大事务、外键级联操作,binlog必须为ROW格式
  4. 集群规模受限:官方推荐集群节点数不超过9个,超过后性能会急剧下降
  5. 网络要求极高:节点间网络延迟必须低于1ms,否则会严重影响集群性能与稳定性,不适合跨机房部署

3.5 生产落地核心注意事项

  1. 生产环境必须使用单主模式:多主模式存在严重的事务冲突、死锁风险,除非有极强的技术把控能力,否则严禁使用
  2. 集群节点数必须为奇数:推荐3、5、7个节点,确保多数派机制正常工作,避免脑裂问题
  3. 严格控制事务大小:大事务会导致节点同步阻塞、认证超时,甚至被踢出集群,单个事务数据量必须控制在100MB以内,严禁执行大事务
  4. 所有业务表必须有显式主键:无主键的表会导致事务认证失败、同步异常,甚至引发节点宕机
  5. 禁用所有非事务引擎:MGR仅支持InnoDB引擎,其他引擎会导致数据不一致问题
  6. 节点间必须使用万兆网卡,同机房部署,网络延迟控制在1ms以内,严禁跨公网部署
  7. 配置合理的故障检测阈值:调整group_replication_member_expel_timeout参数,避免网络抖动导致节点被误踢
  8. 开启自动重加入机制:配置group_replication_autorejoin_tries参数,让故障节点自动尝试重新加入集群,减少人工介入

四、MyCat 分布式中间件高可用架构

MyCat是开源的分布式数据库中间件,核心能力是分库分表、SQL路由与读写分离,通过自身集群+后端MySQL高可用架构,实现端到端的全链路高可用。

4.1 底层核心原理

MyCat通过模拟MySQL协议,让业务应用将其视为一个标准的MySQL实例,业务的SQL请求发送到MyCat后,MyCat会根据预设的规则解析SQL、路由到后端对应的真实MySQL节点,执行完成后汇总结果返回给业务应用。其高可用能力分为两层:

  1. 接入层高可用:MyCat自身通过多节点集群部署,搭配HAProxy+Keepalived实现负载均衡与VIP漂移,单个MyCat节点故障不会影响业务可用性
  2. 数据层高可用:后端MySQL节点采用主从、MGR等高可用架构,MyCat通过定时心跳检测后端节点的健康状态,故障时自动剔除故障节点,将请求切换到可用节点

4.2 架构拓扑图

4.3 生产级配置实例

采用MyCat2,核心配置如下:

4.3.1 schema.xml 逻辑库与节点配置

<?xml version="1.0" encoding="UTF-8"?>
<mycat:schema xmlns:mycat="http://io.mycat/">
   <schema name="user_db" sqlMaxLimit="100" dataNode="dn1">
       <table name="t_user" primaryKey="id" dataNode="dn1,dn2" rule="mod-long"/>
   </schema>
   <dataNode name="dn1" dataHost="dh1" database="user_db_1" />
   <dataNode name="dn2" dataHost="dh2" database="user_db_2" />
   <dataHost name="dh1" maxCon="1000" minCon="10" balance="1" writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1" slaveThreshold="100">
       <heartbeat>select user()</heartbeat>
       <writeHost host="hostM1" url="jdbc:mysql://192.168.1.100:3306/user_db_1?useSSL=false&amp;serverTimezone=Asia/Shanghai" user="root" password="Root@123456">
           <readHost host="hostS1" url="jdbc:mysql://192.168.1.101:3306/user_db_1?useSSL=false&amp;serverTimezone=Asia/Shanghai" user="root" password="Root@123456" />
       </writeHost>
   </dataHost>
   <dataHost name="dh2" maxCon="1000" minCon="10" balance="1" writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1" slaveThreshold="100">
       <heartbeat>select user()</heartbeat>
       <writeHost host="hostM2" url="jdbc:mysql://192.168.1.102:3306/user_db_2?useSSL=false&amp;serverTimezone=Asia/Shanghai" user="root" password="Root@123456">
           <readHost host="hostS2" url="jdbc:mysql://192.168.1.103:3306/user_db_2?useSSL=false&amp;serverTimezone=Asia/Shanghai" user="root" password="Root@123456" />
       </writeHost>
   </dataHost>
</mycat:schema>

4.3.2 server.xml 用户权限配置

<?xml version="1.0" encoding="UTF-8"?>
<mycat:server xmlns:mycat="http://io.mycat/">
   <user name="root" defaultAccount="true">
       <property name="password">Mycat@123456</property>
       <property name="schemas">user_db</property>
   </user>
</mycat:server>

4.3.3 rule.xml 分表规则配置

<?xml version="1.0" encoding="UTF-8"?>
<mycat:rule xmlns:mycat="http://io.mycat/">
   <tableRule name="mod-long">
       <rule>
           <columns>id</columns>
           <algorithm>mod-long</algorithm>
       </rule>
   </tableRule>
   <function name="mod-long" class="io.mycat.route.function.PartitionByMod">
       <property name="count">2</property>
   </function>
</mycat:rule>

4.4 优劣势分析

核心优势

  1. 一站式解决分库分表+读写分离+高可用需求,当单库性能达到瓶颈时,无需额外引入其他组件
  2. 业务零侵入:业务代码无需修改,只需将数据库连接地址改为MyCat的VIP,即可实现分库分表与高可用
  3. 灵活的路由策略:支持哈希、范围、一致性哈希等多种分库分表规则,可适配不同的业务场景
  4. 全链路高可用保障:接入层与数据层均有高可用设计,单个MyCat节点或MySQL节点故障都不会影响业务
  5. 强大的SQL管控能力:支持SQL防火墙、慢SQL拦截、流量控制等能力,提升数据库的安全性与稳定性

核心劣势

  1. 架构复杂度极高:引入了MyCat、HAProxy、Keepalived等多个组件,运维成本大幅提升,需要同时掌握中间件与数据库的运维知识
  2. 性能损耗明显:SQL经过MyCat的解析、路由、结果汇总,会产生10%-20%的性能损耗,复杂查询的损耗更高
  3. SQL兼容性有限:对复杂SQL、存储过程、函数、触发器的支持不完善,部分SQL需要改造后才能适配
  4. 分布式事务风险:分库分表场景下,MyCat的弱XA事务存在数据不一致风险,强XA事务性能极差
  5. 社区活跃度下降:MyCat1.6已停止维护,MyCat2的社区活跃度与文档完善度不足

4.5 生产落地核心注意事项

  1. 严禁过度分库分表:单表数据量低于500万行、单库数据量低于1TB时,无需分库分表,避免引入不必要的架构复杂度
  2. 合理选择分表键:分表键必须是业务高频查询的字段,避免跨库JOIN、跨库分页查询,此类操作会导致性能急剧下降
  3. 后端MySQL必须搭配高可用架构:MyCat本身不提供数据高可用能力,后端MySQL必须使用主从或MGR架构,避免数据单点故障
  4. 生产环境禁用分布式事务:通过业务设计规避跨库事务,避免使用MyCat的XA事务,防止数据不一致与性能问题
  5. 严格管控SQL规范:严禁使用不带分表键的查询、跨库JOIN、子查询、大分页查询,此类SQL会导致MyCat全表扫描
  6. MyCat必须集群部署:至少2个MyCat节点,通过HAProxy实现负载均衡,避免MyCat单点故障
  7. 配置合理的心跳检测:后端节点心跳间隔设置为1s,故障切换阈值设置为3次,避免网络抖动导致的误切换

五、三大方案全维度横向对比

对比维度 主从复制 + Keepalived MGR单主模式 MyCat中间件架构
RTO 30s-5min(取决于人工介入) <30s(自动切换) <10s(自动切换)
RPO 异步复制>0,半同步≈0(极端场景有丢失) =0(多数派存活时零丢失) 取决于后端MySQL架构
数据一致性 最终一致,存在数据丢失风险 强一致,分布式事务保障 最终一致,分布式事务有风险
性能损耗 <10%(半同步) 15%-30% 10%-20%
故障自愈能力 弱,仅VIP漂移,需人工修复主从 强,自动检测、选主、切换、重加入 强,自动剔除故障节点,自动切换
运维复杂度 低,成熟稳定,门槛低 高,需掌握分布式协议,排障难度大 极高,需同时掌握中间件和数据库
写扩展性 无,单主写,无法横向扩展 有限,最多9节点,多主模式风险高 好,可通过分库扩展写能力
读扩展性 好,可无限扩展从库 有限,最多9节点 好,分库+从库可无限扩展
业务侵入性 低(需适配表结构要求) 低(需适配SQL规范)
适用场景 中小规模业务,运维能力有限,对RPO要求不高 金融、支付等对数据一致性要求极高的业务 超大规模数据量,需分库分表的业务

六、生产级选型决策矩阵

没有完美的高可用架构,只有最适合业务场景的方案,基于业务特征的选型决策如下:

  1. 优先选择主从+Keepalived的场景
  • 业务规模中小,QPS低于1万,单库数据量低于1TB
  • 运维团队规模小,DBA技术能力有限
  • 对RPO要求不高,允许极端场景下少量数据丢失
  • 业务对数据库性能要求极高,不能接受过高的性能损耗
  1. 优先选择MGR的场景
  • 金融、支付、证券等对数据一致性要求极高,RPO必须为0的业务
  • 7*24小时不间断服务,对故障自愈能力要求高,不能接受人工介入的业务
  • 有完善的DBA团队,具备分布式数据库运维能力
  • 节点同机房部署,网络延迟极低
  1. 优先选择MyCat的场景
  • 单表数据量超过1000万行,单库数据量超过5TB,单库性能达到瓶颈
  • 业务需要分库分表,同时需要读写分离和高可用能力
  • 业务代码无法大幅改造,需要零侵入的分库分表方案
  • 有完善的中间件与DBA运维团队,能支撑复杂架构的运维

七、生产落地通用避坑指南

无论选择哪种高可用架构,以下核心规则必须严格遵守,才能保障架构的稳定性与可用性:

  1. 监控体系必须先行:搭建全链路监控体系,覆盖数据库层(连接数、QPS、主从延迟、锁等待)、高可用组件层(节点状态、心跳状态)、系统层(CPU、内存、磁盘IO、网络),核心指标必须配置电话告警
  2. 备份是最后的救命稻草:制定完善的备份策略,每天一次全量物理备份,每小时一次增量备份,实时备份binlog到异地存储,每月至少执行一次备份恢复演练,确保备份可用
  3. 故障切换演练常态化:每月至少执行一次故障切换演练,覆盖主库宕机、从库故障、网络分区等场景,每次演练后复盘优化流程
  4. 数据一致性定期校验:每周使用pt-table-checksum等工具校验节点间的数据一致性,发现不一致及时修复
  5. 版本与配置规范:必须使用MySQL 8.0最新稳定版,开启GTID模式,binlog设置为ROW格式,启用双1配置(sync_binlog=1,innodb_flush_log_at_trx_commit=1),从库开启super_read_only
  6. 网络安全规范:所有数据库与高可用组件必须部署在内网,禁止暴露公网,数据库用户限制IP访问,密码符合复杂度要求,节点间通信开启SSL加密

八、高可用场景下Java读写分离实现

以下为基于Spring Boot 3.2.x、JDK 17、MyBatis-Plus实现的动态数据源读写分离方案,支持故障自动检测与切换。

8.1 核心依赖配置

<dependencies>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
       <version>3.2.5</version>
   </dependency>
   <dependency>
       <groupId>com.baomidou</groupId>
       <artifactId>mybatis-plus-boot-starter</artifactId>
       <version>3.5.7</version>
   </dependency>
   <dependency>
       <groupId>com.mysql</groupId>
       <artifactId>mysql-connector-j</artifactId>
       <version>8.4.0</version>
       <scope>runtime</scope>
   </dependency>
   <dependency>
       <groupId>org.springdoc</groupId>
       <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
       <version>2.5.0</version>
   </dependency>
   <dependency>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
       <version>1.18.32</version>
       <scope>provided</scope>
   </dependency>
   <dependency>
       <groupId>com.alibaba.fastjson2</groupId>
       <artifactId>fastjson2</artifactId>
       <version>2.0.52</version>
   </dependency>
   <dependency>
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
       <version>33.1.0-jre</version>
   </dependency>
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-jdbc</artifactId>
       <version>3.2.5</version>
   </dependency>
</dependencies>

8.2 动态数据源核心实现

8.2.1 数据源上下文持有器

package com.jam.demo.datasource;

import org.springframework.util.ObjectUtils;

/**
* 动态数据源上下文持有器
* @author ken
*/

public class DynamicDataSourceContextHolder {

   private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

   public static final String MASTER_DATASOURCE = "master";
   public static final String SLAVE_DATASOURCE = "slave";

   private DynamicDataSourceContextHolder() {
   }

   /**
    * 设置数据源类型
    * @param dataSourceType 数据源类型
    */

   public static void setDataSourceType(String dataSourceType) {
       if (ObjectUtils.isEmpty(dataSourceType)) {
           throw new IllegalArgumentException("数据源类型不能为空");
       }
       CONTEXT_HOLDER.set(dataSourceType);
   }

   /**
    * 获取当前数据源类型
    * @return 数据源类型
    */

   public static String getDataSourceType() {
       return ObjectUtils.isEmpty(CONTEXT_HOLDER.get()) ? MASTER_DATASOURCE : CONTEXT_HOLDER.get();
   }

   /**
    * 清除数据源类型
    */

   public static void clearDataSourceType() {
       CONTEXT_HOLDER.remove();
   }
}

8.2.2 动态数据源路由实现

package com.jam.demo.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
* 动态数据源实现类
* @author ken
*/

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

   @Override
   protected Object determineCurrentLookupKey() {
       return DynamicDataSourceContextHolder.getDataSourceType();
   }
}

8.2.3 数据源配置类

package com.jam.demo.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.jam.demo.datasource.DynamicRoutingDataSource;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
* 动态数据源配置类
* @author ken
*/

@Configuration
@MapperScan(basePackages = "com.jam.demo.mapper", sqlSessionFactoryRef = "sqlSessionFactory")
public class DynamicDataSourceConfig {

   @Bean(name = "masterDataSource")
   @ConfigurationProperties(prefix = "spring.datasource.master")
   public DataSource masterDataSource() {
       return DataSourceBuilder.create().type(DruidDataSource.class).build();
   }

   @Bean(name = "slaveDataSource")
   @ConfigurationProperties(prefix = "spring.datasource.slave")
   public DataSource slaveDataSource() {
       return DataSourceBuilder.create().type(DruidDataSource.class).build();
   }

   @Bean(name = "dynamicDataSource")
   @Primary
   public DataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                         @Qualifier("slaveDataSource") DataSource slaveDataSource) {
       DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
       Map<Object, Object> targetDataSources = new HashMap<>(2);
       targetDataSources.put("master", masterDataSource);
       targetDataSources.put("slave", slaveDataSource);
       dynamicRoutingDataSource.setTargetDataSources(targetDataSources);
       dynamicRoutingDataSource.setDefaultTargetDataSource(masterDataSource);
       return dynamicRoutingDataSource;
   }

   @Bean(name = "sqlSessionFactory")
   public MybatisSqlSessionFactoryBean sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception {
       MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
       sqlSessionFactoryBean.setDataSource(dynamicDataSource);
       sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"));
       MybatisConfiguration configuration = new MybatisConfiguration();
       configuration.setMapUnderscoreToCamelCase(true);
       configuration.setCacheEnabled(false);
       sqlSessionFactoryBean.setConfiguration(configuration);
       GlobalConfig globalConfig = new GlobalConfig();
       globalConfig.setBanner(false);
       sqlSessionFactoryBean.setGlobalConfig(globalConfig);
       return sqlSessionFactoryBean;
   }

   @Bean
   public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
       return new DataSourceTransactionManager(dynamicDataSource);
   }
}

8.2.4 读写分离切面

package com.jam.demo.aspect;

import com.jam.demo.datasource.DynamicDataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
* 读写分离切面
* @author ken
*/

@Slf4j
@Aspect
@Component
public class ReadWriteSeparationAspect {

   @Pointcut("execution(* com.jam.demo.service..*.select*(..)) " +
           "|| execution(* com.jam.demo.service..*.get*(..)) " +
           "|| execution(* com.jam.demo.service..*.query*(..)) " +
           "|| execution(* com.jam.demo.service..*.list*(..)) " +
           "|| execution(* com.jam.demo.service..*.count*(..))")
   public void readPointcut() {
   }

   @Pointcut("execution(* com.jam.demo.service..*.insert*(..)) " +
           "|| execution(* com.jam.demo.service..*.update*(..)) " +
           "|| execution(* com.jam.demo.service..*.delete*(..)) " +
           "|| execution(* com.jam.demo.service..*.save*(..))")
   public void writePointcut() {
   }

   @Around("readPointcut()")
   public Object aroundRead(ProceedingJoinPoint joinPoint) throws Throwable {
       try {
           DynamicDataSourceContextHolder.setDataSourceType(DynamicDataSourceContextHolder.SLAVE_DATASOURCE);
           return joinPoint.proceed();
       } finally {
           DynamicDataSourceContextHolder.clearDataSourceType();
       }
   }

   @Around("writePointcut()")
   public Object aroundWrite(ProceedingJoinPoint joinPoint) throws Throwable {
       try {
           DynamicDataSourceContextHolder.setDataSourceType(DynamicDataSourceContextHolder.MASTER_DATASOURCE);
           return joinPoint.proceed();
       } finally {
           DynamicDataSourceContextHolder.clearDataSourceType();
       }
   }
}

8.2.5 数据源健康检测服务

package com.jam.demo.service;

import com.jam.demo.datasource.DynamicDataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
* 数据源健康检测服务
* @author ken
*/

@Slf4j
@Service
public class DataSourceHealthCheckService {

   @Resource
   private JdbcTemplate jdbcTemplate;

   private static volatile boolean slaveAvailable = true;
   private static volatile boolean masterAvailable = true;

   private static final String HEALTH_CHECK_SQL = "SELECT 1";

   @Scheduled(fixedRate = 2000)
   public void checkSlaveHealth() {
       try {
           DynamicDataSourceContextHolder.setDataSourceType(DynamicDataSourceContextHolder.SLAVE_DATASOURCE);
           jdbcTemplate.queryForObject(HEALTH_CHECK_SQL, Integer.class);
           if (!slaveAvailable) {
               log.info("从库恢复可用,已重新加入读池");
               slaveAvailable = true;
           }
       } catch (Exception e) {
           if (slaveAvailable) {
               log.error("从库健康检测失败,已剔除读池", e);
               slaveAvailable = false;
           }
       } finally {
           DynamicDataSourceContextHolder.clearDataSourceType();
       }
   }

   @Scheduled(fixedRate = 1000)
   public void checkMasterHealth() {
       try {
           DynamicDataSourceContextHolder.setDataSourceType(DynamicDataSourceContextHolder.MASTER_DATASOURCE);
           jdbcTemplate.queryForObject(HEALTH_CHECK_SQL, Integer.class);
           if (!masterAvailable) {
               log.info("主库恢复可用");
               masterAvailable = true;
           }
       } catch (Exception e) {
           if (masterAvailable) {
               log.error("主库健康检测失败", e);
               masterAvailable = false;
           }
       } finally {
           DynamicDataSourceContextHolder.clearDataSourceType();
       }
   }

   public boolean isSlaveAvailable() {
       return slaveAvailable;
   }

   public boolean isMasterAvailable() {
       return masterAvailable;
   }
}

8.3 业务层实现

8.3.1 实体类

package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
* 用户实体类
* @author ken
*/

@Data
@TableName("t_user")
@Schema(description = "用户实体")
public class User implements Serializable {

   private static final long serialVersionUID = 1L;

   @TableId(type = IdType.AUTO)
   @Schema(description = "用户ID", example = "1")
   private Long id;

   @Schema(description = "用户名", example = "test_user")
   private String username;

   @Schema(description = "密码", example = "123456")
   private String password;

   @Schema(description = "手机号", example = "13800138000")
   private String phone;

   @Schema(description = "创建时间")
   private LocalDateTime createTime;

   @Schema(description = "更新时间")
   private LocalDateTime updateTime;
}

8.3.2 Mapper接口

package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;

/**
* 用户Mapper接口
* @author ken
*/

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

8.3.3 服务接口与实现

package com.jam.demo.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.demo.entity.User;

import java.util.List;

/**
* 用户服务接口
* @author ken
*/

public interface UserService extends IService<User> {

   User getUserById(Long id);

   List<User> listAllUsers();

   boolean saveUser(User user);

   boolean updateUser(User user);

   boolean deleteUser(Long id);
}

package com.jam.demo.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.User;
import com.jam.demo.mapper.UserMapper;
import com.jam.demo.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.util.List;

/**
* 用户服务实现类
* @author ken
*/

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

   @Override
   public User getUserById(Long id) {
       if (ObjectUtils.isEmpty(id)) {
           throw new IllegalArgumentException("用户ID不能为空");
       }
       return this.getById(id);
   }

   @Override
   public List<User> listAllUsers() {
       return this.list();
   }

   @Override
   public boolean saveUser(User user) {
       if (ObjectUtils.isEmpty(user)) {
           throw new IllegalArgumentException("用户信息不能为空");
       }
       if (!StringUtils.hasText(user.getUsername())) {
           throw new IllegalArgumentException("用户名不能为空");
       }
       return this.save(user);
   }

   @Override
   public boolean updateUser(User user) {
       if (ObjectUtils.isEmpty(user) || ObjectUtils.isEmpty(user.getId())) {
           throw new IllegalArgumentException("用户ID和信息不能为空");
       }
       return this.updateById(user);
   }

   @Override
   public boolean deleteUser(Long id) {
       if (ObjectUtils.isEmpty(id)) {
           throw new IllegalArgumentException("用户ID不能为空");
       }
       return this.removeById(id);
   }
}

8.3.4 接口控制器

package com.jam.demo.controller;

import com.jam.demo.entity.User;
import com.jam.demo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
* 用户控制器
* @author ken
*/

@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
@Tag(name = "用户管理", description = "用户增删改查接口,支持读写分离")
public class UserController {

   private final UserService userService;

   @GetMapping("/{id}")
   @Operation(summary = "根据ID查询用户", description = "读请求,自动路由到从库")
   public ResponseEntity<User> getUserById(@Parameter(description = "用户ID") @PathVariable Long id) {
       User user = userService.getUserById(id);
       return ResponseEntity.ok(user);
   }

   @GetMapping("/list")
   @Operation(summary = "查询所有用户", description = "读请求,自动路由到从库")
   public ResponseEntity<List<User>> listAllUsers() {
       List<User> userList = userService.listAllUsers();
       return ResponseEntity.ok(userList);
   }

   @PostMapping
   @Operation(summary = "新增用户", description = "写请求,自动路由到主库")
   public ResponseEntity<Boolean> saveUser(@RequestBody User user) {
       boolean result = userService.saveUser(user);
       return ResponseEntity.ok(result);
   }

   @PutMapping
   @Operation(summary = "更新用户", description = "写请求,自动路由到主库")
   public ResponseEntity<Boolean> updateUser(@RequestBody User user) {
       boolean result = userService.updateUser(user);
       return ResponseEntity.ok(result);
   }

   @DeleteMapping("/{id}")
   @Operation(summary = "删除用户", description = "写请求,自动路由到主库")
   public ResponseEntity<Boolean> deleteUser(@Parameter(description = "用户ID") @PathVariable Long id) {
       boolean result = userService.deleteUser(id);
       return ResponseEntity.ok(result);
   }
}

8.4 应用配置文件

spring:
 application:
   name: mysql-ha-demo
 datasource:
   master:
     driver-class-name: com.mysql.cj.jdbc.Driver
     url: jdbc:mysql://192.168.1.200:3306/user_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
     username: root
     password: Root@123456
     type: com.alibaba.druid.pool.DruidDataSource
     initial-size: 10
     min-idle: 10
     max-active: 100
   slave:
     driver-class-name: com.mysql.cj.jdbc.Driver
     url: jdbc:mysql://192.168.1.201:3306/user_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
     username: root
     password: Root@123456
     type: com.alibaba.druid.pool.DruidDataSource
     initial-size: 10
     min-idle: 10
     max-active: 100
 jackson:
   date-format: yyyy-MM-dd HH:mm:ss
   time-zone: Asia/Shanghai

mybatis-plus:
 mapper-locations: classpath*:mapper/*.xml
 type-aliases-package: com.jam.demo.entity
 configuration:
   map-underscore-to-camel-case: true
   cache-enabled: false

springdoc:
 swagger-ui:
   path: /swagger-ui.html
   enabled: true
 api-docs:
   enabled: true
   path: /v3/api-docs

server:
 port: 8080

九、总结

MySQL高可用架构的选型,本质是在数据一致性、业务可用性、性能、运维成本之间做平衡。没有万能的架构,只有匹配业务现状与未来发展的方案。

对于绝大多数业务而言,主从+Keepalived是性价比最高的选择;对于金融级强一致需求,MGR是官方原生的最优解;对于超大规模数据量的分库分表场景,MyCat可提供一站式的解决方案。无论选择哪种方案,最终都要回归到业务本身,以保障数据安全与业务连续性为核心目标,同时控制架构复杂度,避免过度设计。

目录
相关文章
|
6天前
|
人工智能 数据可视化 安全
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
本文详解如何用阿里云Lighthouse一键部署OpenClaw,结合飞书CLI等工具,让AI真正“动手”——自动群发、生成科研日报、整理知识库。核心理念:未来软件应为AI而生,CLI即AI的“手脚”,实现高效、安全、可控的智能自动化。
34418 16
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
|
18天前
|
人工智能 JSON 机器人
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
本文带你零成本玩转OpenClaw:学生认证白嫖6个月阿里云服务器,手把手配置飞书机器人、接入免费/高性价比AI模型(NVIDIA/通义),并打造微信公众号“全自动分身”——实时抓热榜、AI选题拆解、一键发布草稿,5分钟完成热点→文章全流程!
45254 142
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
|
8天前
|
人工智能 JSON 监控
Claude Code 源码泄露:一份价值亿元的 AI 工程公开课
我以为顶级 AI 产品的护城河是模型。读完这 51.2 万行泄露的源码,我发现自己错了。
4749 20
|
1天前
|
人工智能 自然语言处理 安全
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)
本文介绍了Claude Code终端AI助手的使用指南,主要内容包括:1)常用命令如版本查看、项目启动和更新;2)三种工作模式切换及界面说明;3)核心功能指令速查表,包含初始化、压缩对话、清除历史等操作;4)详细解析了/init、/help、/clear、/compact、/memory等关键命令的使用场景和语法。文章通过丰富的界面截图和场景示例,帮助开发者快速掌握如何通过命令行和交互界面高效使用Claude Code进行项目开发,特别强调了CLAUDE.md文件作为项目知识库的核心作用。
1084 5
Claude Code 全攻略:命令大全 + 实战工作流(建议收藏)
|
6天前
|
人工智能 API 开发者
阿里云百炼 Coding Plan 售罄、Lite 停售、Pro 抢不到?最新解决方案
阿里云百炼Coding Plan Lite已停售,Pro版每日9:30限量抢购难度大。本文解析原因,并提供两大方案:①掌握技巧抢购Pro版;②直接使用百炼平台按量付费——新用户赠100万Tokens,支持Qwen3.5-Max等满血模型,灵活低成本。
1650 5
阿里云百炼 Coding Plan 售罄、Lite 停售、Pro 抢不到?最新解决方案

热门文章

最新文章