一、MySQL高可用的核心标尺与选型维度
在分布式业务架构中,数据库是整个系统的状态核心,单点故障会直接导致全业务瘫痪。MySQL高可用架构的核心目标,是通过冗余设计解决单点故障问题,保障业务连续性与数据安全性。在选型前,我们先明确统一的评估维度,所有方案都将基于以下核心指标进行拆解:
- RTO(恢复时间目标):故障发生后,业务恢复正常服务的最长耗时
- RPO(恢复点目标):故障发生后,业务允许丢失的最大数据量
- 数据一致性:故障切换前后,数据是否保持一致,是否存在丢失、脏写风险
- 故障自愈能力:故障处理是否需要人工介入,能否自动完成检测、选主、切换全流程
- 性能损耗:高可用机制对数据库读写性能的影响幅度
- 运维复杂度:架构搭建、日常维护、扩容、故障排查的技术门槛
- 扩展性:能否通过横向扩展提升数据库的读写能力
- 业务侵入性:是否需要改造业务代码,对现有业务的适配成本
二、主从复制 + Keepalived 经典高可用架构
主从复制+Keepalived是业界使用最广泛、成熟度最高的MySQL高可用方案,适用于绝大多数中小规模业务场景。
2.1 底层核心原理
该架构分为两层核心能力:
- MySQL主从复制:基于binlog的增量数据同步机制。主库将数据变更写入二进制日志binlog,从库的IO线程拉取binlog写入本地中继日志relay log,SQL线程重放中继日志中的事务,最终实现主从节点的数据同步。MySQL 8.0默认使用增强半同步复制,确保事务在从库收到binlog后才在主库提交,最大程度降低数据丢失风险。
- 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 优劣势分析
核心优势
- 方案成熟稳定,社区资料丰富,运维门槛极低,绝大多数DBA均可快速掌握
- 性能损耗极小,异步复制损耗低于5%,增强半同步复制损耗低于10%
- 业务零侵入,所有基于MySQL的业务均可直接适配,无需任何代码改造
- 部署灵活,支持一主一从、一主多从架构,可搭配读写分离无限扩展读能力
核心劣势
- 数据一致性存在短板:异步复制存在明确的数据丢失风险,增强半同步在主库宕机且binlog未同步到从库的极端场景下,仍有数据丢失可能
- 脑裂风险:VRRP协议在网络分区场景下,可能出现双主节点同时持有VIP的情况,导致数据冲突与脏写
- 故障自愈能力有限:仅能实现VIP漂移,主从切换后需要人工修复主从关系,无法自动恢复集群架构
- 写能力无法扩展:单主架构决定了写性能无法通过横向扩展提升
2.5 生产落地核心注意事项
- 必须使用GTID模式,彻底解决传统复制的位点偏移问题,大幅简化故障切换与主从修复流程
- 强制开启增强半同步复制,设置
rpl_semi_sync_master_wait_point=AFTER_SYNC,确保事务在从库收到binlog后再提交 - Keepalived必须配置非抢占模式
nopreempt,避免主节点恢复后VIP回切导致的业务闪断 - 脑裂防护:配置双网卡心跳、第三方仲裁节点,健康检测脚本必须在MySQL不可用时自动停止Keepalived服务
- 主从节点硬件配置必须完全对称,避免从库出现复制延迟,同时配置监控告警,延迟超过1s立即触发告警
- 每月至少执行一次主从切换演练,验证切换流程的可用性,同时完成主从关系修复
三、MySQL Group Replication(MGR)官方原生强一致高可用架构
MGR是MySQL官方推出的基于分布式共识协议的高可用方案,原生支持强数据一致性与自动故障自愈,是金融级业务的首选方案。
3.1 底层核心原理
MGR基于Paxos分布式共识协议实现,集群内所有节点通过多数派投票机制达成数据共识,确保所有节点的数据完全一致。核心逻辑分为三个部分:
- 事务执行流程:客户端提交事务到主节点,主节点将事务广播到集群内所有节点,所有节点对事务进行冲突检测与合法性校验,超过半数节点认证通过后,事务才能正式提交,确保集群内数据一致性。
- 故障检测机制:集群内节点通过心跳机制互相检测状态,当某个节点失联超过阈值,集群内超过半数节点会判定该节点故障,自动将其踢出集群,避免故障节点影响集群可用性。
- 自动选主与切换:单主模式下,主节点故障后,集群会自动根据节点权重、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-id与loose-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 优劣势分析
核心优势
- 原生强数据一致性:基于Paxos多数派共识协议,只要集群内超过半数节点存活,就能保证RPO=0,数据零丢失,完全满足金融级合规要求
- 完善的故障自愈能力:自动完成故障检测、选主、切换、节点重加入全流程,无需人工介入,RTO可控制在30s以内
- 无脑裂风险:基于多数派机制,网络分区时只有持有多数节点的分区能提供服务,彻底避免双主数据冲突
- 官方原生支持:与MySQL内核深度整合,版本迭代同步,兼容性最佳,无需引入第三方组件
- 支持多主模式:可实现多节点同时写入,解决单主架构的写性能瓶颈
核心劣势
- 性能损耗较高:事务需要广播到所有节点并经过多数派认证,写性能比主从架构低15%-30%,集群节点越多,性能损耗越大
- 运维复杂度高:对DBA的技术能力要求极高,需要掌握分布式共识协议、事务冲突检测、分布式事务等底层知识,故障排查难度大
- 严格的使用限制:仅支持InnoDB引擎,所有表必须有显式主键,不支持大事务、外键级联操作,binlog必须为ROW格式
- 集群规模受限:官方推荐集群节点数不超过9个,超过后性能会急剧下降
- 网络要求极高:节点间网络延迟必须低于1ms,否则会严重影响集群性能与稳定性,不适合跨机房部署
3.5 生产落地核心注意事项
- 生产环境必须使用单主模式:多主模式存在严重的事务冲突、死锁风险,除非有极强的技术把控能力,否则严禁使用
- 集群节点数必须为奇数:推荐3、5、7个节点,确保多数派机制正常工作,避免脑裂问题
- 严格控制事务大小:大事务会导致节点同步阻塞、认证超时,甚至被踢出集群,单个事务数据量必须控制在100MB以内,严禁执行大事务
- 所有业务表必须有显式主键:无主键的表会导致事务认证失败、同步异常,甚至引发节点宕机
- 禁用所有非事务引擎:MGR仅支持InnoDB引擎,其他引擎会导致数据不一致问题
- 节点间必须使用万兆网卡,同机房部署,网络延迟控制在1ms以内,严禁跨公网部署
- 配置合理的故障检测阈值:调整
group_replication_member_expel_timeout参数,避免网络抖动导致节点被误踢 - 开启自动重加入机制:配置
group_replication_autorejoin_tries参数,让故障节点自动尝试重新加入集群,减少人工介入
四、MyCat 分布式中间件高可用架构
MyCat是开源的分布式数据库中间件,核心能力是分库分表、SQL路由与读写分离,通过自身集群+后端MySQL高可用架构,实现端到端的全链路高可用。
4.1 底层核心原理
MyCat通过模拟MySQL协议,让业务应用将其视为一个标准的MySQL实例,业务的SQL请求发送到MyCat后,MyCat会根据预设的规则解析SQL、路由到后端对应的真实MySQL节点,执行完成后汇总结果返回给业务应用。其高可用能力分为两层:
- 接入层高可用:MyCat自身通过多节点集群部署,搭配HAProxy+Keepalived实现负载均衡与VIP漂移,单个MyCat节点故障不会影响业务可用性
- 数据层高可用:后端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&serverTimezone=Asia/Shanghai" user="root" password="Root@123456">
<readHost host="hostS1" url="jdbc:mysql://192.168.1.101:3306/user_db_1?useSSL=false&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&serverTimezone=Asia/Shanghai" user="root" password="Root@123456">
<readHost host="hostS2" url="jdbc:mysql://192.168.1.103:3306/user_db_2?useSSL=false&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 优劣势分析
核心优势
- 一站式解决分库分表+读写分离+高可用需求,当单库性能达到瓶颈时,无需额外引入其他组件
- 业务零侵入:业务代码无需修改,只需将数据库连接地址改为MyCat的VIP,即可实现分库分表与高可用
- 灵活的路由策略:支持哈希、范围、一致性哈希等多种分库分表规则,可适配不同的业务场景
- 全链路高可用保障:接入层与数据层均有高可用设计,单个MyCat节点或MySQL节点故障都不会影响业务
- 强大的SQL管控能力:支持SQL防火墙、慢SQL拦截、流量控制等能力,提升数据库的安全性与稳定性
核心劣势
- 架构复杂度极高:引入了MyCat、HAProxy、Keepalived等多个组件,运维成本大幅提升,需要同时掌握中间件与数据库的运维知识
- 性能损耗明显:SQL经过MyCat的解析、路由、结果汇总,会产生10%-20%的性能损耗,复杂查询的损耗更高
- SQL兼容性有限:对复杂SQL、存储过程、函数、触发器的支持不完善,部分SQL需要改造后才能适配
- 分布式事务风险:分库分表场景下,MyCat的弱XA事务存在数据不一致风险,强XA事务性能极差
- 社区活跃度下降:MyCat1.6已停止维护,MyCat2的社区活跃度与文档完善度不足
4.5 生产落地核心注意事项
- 严禁过度分库分表:单表数据量低于500万行、单库数据量低于1TB时,无需分库分表,避免引入不必要的架构复杂度
- 合理选择分表键:分表键必须是业务高频查询的字段,避免跨库JOIN、跨库分页查询,此类操作会导致性能急剧下降
- 后端MySQL必须搭配高可用架构:MyCat本身不提供数据高可用能力,后端MySQL必须使用主从或MGR架构,避免数据单点故障
- 生产环境禁用分布式事务:通过业务设计规避跨库事务,避免使用MyCat的XA事务,防止数据不一致与性能问题
- 严格管控SQL规范:严禁使用不带分表键的查询、跨库JOIN、子查询、大分页查询,此类SQL会导致MyCat全表扫描
- MyCat必须集群部署:至少2个MyCat节点,通过HAProxy实现负载均衡,避免MyCat单点故障
- 配置合理的心跳检测:后端节点心跳间隔设置为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要求不高 | 金融、支付等对数据一致性要求极高的业务 | 超大规模数据量,需分库分表的业务 |
六、生产级选型决策矩阵
没有完美的高可用架构,只有最适合业务场景的方案,基于业务特征的选型决策如下:
- 优先选择主从+Keepalived的场景
- 业务规模中小,QPS低于1万,单库数据量低于1TB
- 运维团队规模小,DBA技术能力有限
- 对RPO要求不高,允许极端场景下少量数据丢失
- 业务对数据库性能要求极高,不能接受过高的性能损耗
- 优先选择MGR的场景
- 金融、支付、证券等对数据一致性要求极高,RPO必须为0的业务
- 7*24小时不间断服务,对故障自愈能力要求高,不能接受人工介入的业务
- 有完善的DBA团队,具备分布式数据库运维能力
- 节点同机房部署,网络延迟极低
- 优先选择MyCat的场景
- 单表数据量超过1000万行,单库数据量超过5TB,单库性能达到瓶颈
- 业务需要分库分表,同时需要读写分离和高可用能力
- 业务代码无法大幅改造,需要零侵入的分库分表方案
- 有完善的中间件与DBA运维团队,能支撑复杂架构的运维
七、生产落地通用避坑指南
无论选择哪种高可用架构,以下核心规则必须严格遵守,才能保障架构的稳定性与可用性:
- 监控体系必须先行:搭建全链路监控体系,覆盖数据库层(连接数、QPS、主从延迟、锁等待)、高可用组件层(节点状态、心跳状态)、系统层(CPU、内存、磁盘IO、网络),核心指标必须配置电话告警
- 备份是最后的救命稻草:制定完善的备份策略,每天一次全量物理备份,每小时一次增量备份,实时备份binlog到异地存储,每月至少执行一次备份恢复演练,确保备份可用
- 故障切换演练常态化:每月至少执行一次故障切换演练,覆盖主库宕机、从库故障、网络分区等场景,每次演练后复盘优化流程
- 数据一致性定期校验:每周使用pt-table-checksum等工具校验节点间的数据一致性,发现不一致及时修复
- 版本与配置规范:必须使用MySQL 8.0最新稳定版,开启GTID模式,binlog设置为ROW格式,启用双1配置(sync_binlog=1,innodb_flush_log_at_trx_commit=1),从库开启super_read_only
- 网络安全规范:所有数据库与高可用组件必须部署在内网,禁止暴露公网,数据库用户限制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可提供一站式的解决方案。无论选择哪种方案,最终都要回归到业务本身,以保障数据安全与业务连续性为核心目标,同时控制架构复杂度,避免过度设计。