引言
MySQL作为互联网行业最主流的关系型数据库,其高可用架构直接决定了业务的连续性与数据可靠性。本文从底层核心原理出发,完整覆盖主从复制、MGR原生集群、读写分离三大核心模块,故障根因分析、全场景最佳实践,帮助读者从零搭建稳定、高性能、可扩展的MySQL高可用体系。
一、MySQL高可用核心基础与选型逻辑
1.1 高可用核心指标
高可用的本质是通过架构设计,最大限度减少服务不可用时间,核心衡量指标有两个:
- RTO(恢复时间目标) :故障发生后,服务恢复正常的最长可接受时间,直接决定业务中断时长
- RPO(恢复点目标) :故障发生后,可接受的最大数据丢失量,直接决定数据一致性保障能力
生产环境核心诉求:核心交易场景RTO<30s、RPO=0;非核心场景RTO<5min、RPO<30s。
1.2 主流高可用方案对比与选型
| 方案类型 | 核心原理 | RTO | RPO | 核心优势 | 核心局限 | 适用场景 |
|---|---|---|---|---|---|---|
| 主从复制 | 基于binlog的主库到从库的数据同步,手动/自动切换 | 分钟级 | 异步模式有数据丢失 | 部署简单、运维成本低、兼容性强 | 故障切换需人工介入、一致性保障弱 | 中小规模业务、非核心交易系统 |
| MGR集群 | 基于Paxos分布式共识协议的原生多节点集群,自动故障切换 | 秒级 | 0 | 原生支持、数据强一致、自动选主、故障自愈 | 部署运维复杂度高、对大事务敏感 | 核心交易系统、金融级高可用要求场景 |
| 共享存储方案 | 多节点共享同一份存储数据,故障秒级切换 | 秒级 | 0 | 无数据同步开销、切换无数据丢失 | 存储单点风险、成本极高 | 传统企业级核心系统 |
生产环境选型优先级:核心交易场景优先选择MGR单主集群;中小规模、非核心场景选择主从复制+读写分离架构。
二、主从复制:生产级落地全流程
主从复制是MySQL高可用体系的基础,也是读写分离、数据备份的核心载体。
2.1 主从复制底层原理
主从复制的核心是基于binlog日志的事件重放,整个流程由3个核心线程协同完成,全程无锁、异步执行。
2.1.1 核心组件详解
- binlog二进制日志:主库上记录所有数据修改操作的日志,是主从复制的数据源。生产环境必须使用
ROW行级格式,记录每一行数据的修改前后状态,彻底避免主从不一致问题。 - Dump线程:主库上的后台线程,当从库建立连接后,读取binlog日志并发送给从库,每个从库对应一个独立的Dump线程。
- IO线程:从库上的后台线程,连接主库并接收binlog事件,写入本地relaylog中继日志。
- SQL线程:从库上的后台线程,读取relaylog中的事件并在从库重放,完成数据同步。
2.1.2 GTID全局事务标识
GTID是MySQL 5.6+引入的全局事务ID,格式为server_uuid:transaction_id,每个提交的事务对应一个全局唯一的GTID。
- 核心价值:彻底解决传统复制基于文件位点的痛点,主从切换、故障恢复时无需手动查找同步位点,自动定位缺失的事务。
- 生产强制要求:所有主从集群必须开启GTID模式,避免运维故障。
2.2 三种同步模式详解
2.2.1 异步复制
MySQL默认的同步模式。主库执行完事务并提交后,立即返回客户端结果,无需等待从库接收binlog事件。
- 优势:性能损耗极小,主库性能几乎不受影响
- 劣势:主库宕机时,未同步到从库的事务会丢失,RPO无法保障
- 适用场景:非核心业务、对数据一致性要求低的场景
2.2.2 半同步复制
在异步复制的基础上增加了一致性保障。主库执行完事务后,需等待至少1个从库接收binlog并写入relaylog后,再向客户端返回提交结果。
- 优势:大幅降低数据丢失风险,只有主库和所有从库同时宕机才会丢失数据
- 劣势:增加了事务响应延迟,主库性能受网络和从库性能影响
- 适用场景:对数据一致性有要求、可接受轻微性能损耗的业务
2.2.3 增强半同步复制
MySQL 5.7+引入的优化方案,也是8.0默认的半同步模式。核心优化是将等待时机从AFTER_COMMIT调整为AFTER_SYNC:
- 原半同步
AFTER_COMMIT:主库先提交事务到存储引擎,再等待从库ACK,存在主库提交后宕机、从库未收到事务导致的主备数据不一致问题 - 增强半同步
AFTER_SYNC:主库先将binlog刷盘,等待从库ACK后,再提交事务到存储引擎,彻底解决了数据不一致问题,生产环境必须使用此模式。
2.3 生产级主从集群部署实战
本次部署基于MySQL 8.0.36,1主1从架构,开启GTID与增强半同步复制。
2.3.1 环境前置准备
- 两台服务器关闭防火墙与SELinux,配置主机名与hosts解析
- 两台服务器时间同步,误差不超过1s
- 两台服务器安装相同版本的MySQL 8.0.36,初始化完成后启动服务
2.3.2 主库配置与权限设置
- 主库
my.cnf核心配置
[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
port=3306
default_authentication_plugin=mysql_native_password
max_connections=2000
gtid_mode=ON
enforce_gtid_consistency=ON
server_id=100
log_bin=/var/lib/mysql/mysql-bin
binlog_format=ROW
binlog_row_image=FULL
binlog_expire_logs_seconds=604800
log_slave_updates=ON
binlog_checksum=CRC32
plugin_load_add = semisync_master.so
rpl_semi_sync_master_enabled=ON
rpl_semi_sync_master_wait_for_slave_count=1
rpl_semi_sync_master_wait_point=AFTER_SYNC
rpl_semi_sync_master_timeout=1000
innodb_buffer_pool_size=16G
innodb_log_file_size=4G
innodb_flush_log_at_trx_commit=1
sync_binlog=1
2. 重启主库MySQL服务,创建复制专用用户
CREATE USER 'repl'@'%' IDENTIFIED BY 'Repl@2024#Demo';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
3. 主库数据备份(若已有业务数据)
mysqldump -uroot -p --single-transaction --master-data=2 --all-databases > all_db_backup.sql
2.3.3 从库配置与同步搭建
- 从库
my.cnf核心配置
[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
port=3306
default_authentication_plugin=mysql_native_password
max_connections=2000
gtid_mode=ON
enforce_gtid_consistency=ON
server_id=101
log_bin=/var/lib/mysql/mysql-bin
binlog_format=ROW
binlog_row_image=FULL
binlog_expire_logs_seconds=604800
log_slave_updates=ON
binlog_checksum=CRC32
plugin_load_add = semisync_slave.so
rpl_semi_sync_slave_enabled=ON
slave_parallel_type=LOGICAL_CLOCK
slave_parallel_workers=8
slave_preserve_commit_order=ON
innodb_buffer_pool_size=16G
innodb_log_file_size=4G
innodb_flush_log_at_trx_commit=1
sync_binlog=1
2. 重启从库MySQL服务,导入主库备份数据(若有)
mysql -uroot -p < all_db_backup.sql
3. 从库配置主从同步
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='192.168.1.100',
SOURCE_USER='repl',
SOURCE_PASSWORD='Repl@2024#Demo',
SOURCE_PORT=3306,
SOURCE_AUTO_POSITION=1;
4. 启动从库同步
START REPLICA;
2.3.4 同步状态验证
- 查看从库同步状态
SHOW REPLICA STATUS\G
核心验证项:
Replica_IO_Running: YesReplica_SQL_Running: YesSeconds_Behind_Source: 0Retrieved_Gtid_Set与Executed_Gtid_Set与主库一致
- 验证半同步状态
SHOW STATUS LIKE 'Rpl_semi_sync%';
核心验证项:Rpl_semi_sync_master_clients值为1,代表半同步连接正常。
2.4 生产故障排查与解决方案
2.4.1 主从同步延迟根因与优化
同步延迟是生产环境最常见的问题,核心根因与优化方案如下:
-
SQL线程单线程重放瓶颈:MySQL 5.6之前SQL线程为单线程,大事务或高并发写入场景下重放速度跟不上主库。
- 优化方案:开启8.0并行复制,配置
slave_parallel_type=LOGICAL_CLOCK、slave_parallel_workers=CPU核心数,slave_preserve_commit_order=ON
- 优化方案:开启8.0并行复制,配置
-
大事务导致的延迟:主库执行超大事务(如批量删除百万级数据),binlog传输与重放耗时过长。
- 优化方案:大事务拆分为多个小事务,单事务修改行数不超过1000行
-
从库硬件性能不足:从库CPU、IO性能弱于主库,无法跟上主库写入速度。
- 优化方案:从库硬件配置不低于主库,关闭从库查询缓存,优化慢查询
-
无主键表导致的重放缓慢:ROW格式下,无主键表的修改操作会触发全表扫描,重放效率极低。
- 优化方案:所有表必须设置主键,推荐使用自增主键或雪花ID主键
2.4.2 事务冲突处理
从库重放事务时出现主键冲突、唯一键冲突,导致SQL线程停止,报错Error_code: 1062。
- 根因:主从库同时写入数据、从库数据手动修改、binlog事件重复重放
- 临时解决方案(GTID模式):
-- 停止同步
STOP REPLICA;
-- 设置跳过冲突的GTID事务,替换为实际报错的GTID
SET GTID_NEXT='aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:1001';
-- 空事务跳过
BEGIN;COMMIT;
-- 恢复自动GTID
SET GTID_NEXT='AUTOMATIC';
-- 重启同步
START REPLICA;
3. 根治方案:禁止从库写入数据,配置从库read_only=ON、super_read_only=ON,仅复制用户拥有超级权限。
2.4.3 主库宕机手动切换流程
- 确认主库已无法访问,停止从库同步
STOP REPLICA;
RESET MASTER;
2. 从库提升为新主库,关闭只读模式
SET GLOBAL read_only=OFF;
SET GLOBAL super_read_only=OFF;
3. 业务切换数据库连接地址到新主库 4. 原主库恢复后,配置为新主库的从库,重新搭建同步
2.5 生产最佳实践
- 所有主从集群必须开启GTID模式,禁止使用基于文件位点的传统复制
- 生产环境必须使用增强半同步复制,
rpl_semi_sync_master_timeout设置为1000ms,超时自动降级为异步复制,避免主库阻塞 - 从库必须开启
read_only=ON、super_read_only=ON,禁止手动写入数据 - 所有表必须设置主键,禁止无主键表进入生产环境
- binlog格式必须使用
ROW,binlog_row_image=FULL,避免主从不一致 - 搭建主从监控体系,核心监控指标:同步状态、延迟时长、GTID差值、半同步状态
三、MGR(MySQL Group Replication):原生分布式高可用集群
MGR是MySQL 5.7.17+引入的原生分布式高可用解决方案,基于Paxos分布式共识协议实现,提供数据强一致性、自动故障检测、自动选主、故障自愈能力,是金融级核心业务的首选方案。
3.1 MGR核心原理与架构
3.1.1 核心架构与组件
MGR集群由多个节点组成,生产环境推荐3节点/5节点奇数节点架构,分为单主模式和多主模式两种运行模式。 核心组件:
- 共识层:基于Paxos协议实现,负责集群内消息广播、事务全局排序、多数派确认,只有获得多数派节点ACK的事务才能提交
- 故障检测模块:集群内节点定期交换心跳信息,当节点超过阈值未响应时,集群多数派投票判定节点异常,自动将异常节点踢出集群
- 冲突检测模块:基于事务写集合实现,并发事务修改同一行数据时,集群会检测冲突,先提交的事务生效,后提交的事务回滚
- 自动选主模块:单主模式下,主节点异常时,集群自动根据节点权重、GTID执行情况选举新的主节点,整个过程秒级完成
3.1.2 与传统主从复制的核心区别
| 特性 | 传统主从复制 | MGR集群 |
|---|---|---|
| 一致性保障 | 最终一致,半同步仅降低丢失风险 | 强一致,多数派提交,RPO=0 |
| 故障切换 | 手动/第三方工具实现,分钟级 | 原生自动实现,秒级完成 |
| 脑裂防护 | 无原生支持,需第三方组件 | 原生基于多数派机制,彻底避免脑裂 |
| 节点扩展 | 手动配置,复杂度高 | 节点自动加入,自动同步数据 |
| 写入性能 | 单主写入,无额外共识开销 | 单主写入,有共识开销,性能损耗约10%-20% |
3.2 两种运行模式详解
3.2.1 单主模式
集群中只有一个Primary主节点可读写,其余Secondary节点均为只读节点,是生产环境的首选模式。
- 核心优势:无分布式事务冲突风险,运维复杂度低,数据一致性保障最高,兼容性与主从复制完全一致
- 自动选主规则:主节点异常时,集群优先选择GTID执行最完整的节点,权重相同的情况下选择server_id最小的节点
- 适用场景:绝大多数业务场景,尤其是核心交易系统、金融级业务
3.2.2 多主模式
集群中所有节点都可提供读写服务,写入会同步到所有节点,生产环境不推荐使用。
-
核心限制:
- 不支持外键级联约束
- 不支持SERIALIZABLE隔离级别
- 大事务会导致集群阻塞,甚至节点踢出
- 并发修改同一行数据会导致大量事务回滚,性能急剧下降
-
适用场景:极少写入、多地域就近读取的特殊业务场景
3.3 生产级3节点MGR单主集群部署实战
本次部署基于MySQL 8.0.36,3节点单主模式,开启强一致性保障。
3.3.1 环境前置准备
- 3台服务器关闭防火墙与SELinux,配置hosts解析,时间同步误差<1s
- 3台服务器安装相同版本的MySQL 8.0.36,初始化完成
- 服务器之间网络互通,开放3306(MySQL端口)、33061(MGR通信端口)
- 3台服务器配置相同的MySQL参数,仅server_id、本地通信地址不同
3.3.2 节点统一配置
3个节点的my.cnf核心配置,仅需修改server_id、group_replication_local_address为对应节点的值
[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
port=3306
default_authentication_plugin=mysql_native_password
max_connections=2000
gtid_mode=ON
enforce_gtid_consistency=ON
server_id=100
log_bin=/var/lib/mysql/mysql-bin
binlog_format=ROW
binlog_row_image=FULL
log_slave_updates=ON
binlog_checksum=NONE
master_info_repository=TABLE
relay_log_info_repository=TABLE
relay_log_recovery=ON
slave_preserve_commit_order=ON
transaction_write_set_extraction=XXHASH64
plugin_load_add='group_replication.so'
group_replication_group_name="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
group_replication_start_on_boot=OFF
group_replication_local_address= "192.168.1.100:33061"
group_replication_group_seeds= "192.168.1.100:33061,192.168.1.101:33061,192.168.1.102:33061"
group_replication_ip_allowlist="192.168.1.0/24"
group_replication_single_primary_mode=ON
group_replication_enforce_update_everywhere_checks=OFF
group_replication_member_weight=50
group_replication_unreachable_majority_timeout=30000
innodb_buffer_pool_size=16G
innodb_log_file_size=4G
innodb_flush_log_at_trx_commit=1
sync_binlog=1
配置说明:
group_replication_group_name:集群唯一标识,必须为合法UUID,所有节点一致group_replication_local_address:节点MGR通信地址,每个节点不同group_replication_group_seeds:集群所有节点的通信地址,所有节点一致group_replication_single_primary_mode=ON:开启单主模式group_replication_unreachable_majority_timeout=30000:节点不可达30s后自动退出集群,避免脑裂
3.3.3 集群引导与节点加入
- 3个节点重启MySQL服务,创建集群复制用户
SET SQL_LOG_BIN=0;
CREATE USER 'mgr_repl'@'%' IDENTIFIED BY 'Mgr@2024#Demo';
GRANT REPLICATION SLAVE ON *.* TO 'mgr_repl'@'%';
GRANT BACKUP_ADMIN ON *.* TO 'mgr_repl'@'%';
FLUSH PRIVILEGES;
SET SQL_LOG_BIN=1;
CHANGE REPLICATION SOURCE TO SOURCE_USER='mgr_repl', SOURCE_PASSWORD='Mgr@2024#Demo' FOR CHANNEL 'group_replication_recovery';
2. 主节点引导集群(仅第一个节点执行,仅执行一次)
SET GLOBAL group_replication_bootstrap_group=ON;
START GROUP_REPLICATION;
SET GLOBAL group_replication_bootstrap_group=OFF;
3. 查看集群状态,确认主节点正常加入
SELECT * FROM performance_schema.replication_group_members;
4. 其余两个节点加入集群,直接执行启动命令
START GROUP_REPLICATION;
5. 3个节点都执行完成后,再次查看集群状态,确认3个节点都为ONLINE状态
3.3.4 集群状态验证
- 查看集群节点状态
SELECT MEMBER_ID,MEMBER_HOST,MEMBER_PORT,MEMBER_STATE,MEMBER_ROLE FROM performance_schema.replication_group_members;
验证结果:3个节点MEMBER_STATE均为ONLINE,其中1个节点MEMBER_ROLE为PRIMARY,其余两个为SECONDARY
- 查看主节点信息
SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='group_replication_primary_member';
3. 验证只读配置:从节点默认开启超级只读,无法写入数据,主节点可正常读写
3.4 生产故障处理与容灾机制
3.4.1 自动故障切换流程
单主模式下,主节点异常时,集群自动执行以下流程:
- 故障检测:集群节点心跳超时,多数派投票判定主节点异常,将其标记为UNREACHABLE
- 选主投票:集群从剩余ONLINE节点中,按照GTID执行进度、节点权重选举新的主节点
- 角色切换:新主节点关闭只读模式,提升为PRIMARY角色,其余从节点自动同步到新主节点
- 故障节点处理:原主节点恢复后,自动加入集群,变为SECONDARY角色 整个切换过程在30s内完成,业务仅会出现短暂的连接中断,无需人工介入。
3.4.2 节点异常退出与重新加入
节点异常退出后,状态变为ERROR或UNREACHABLE,重新加入步骤:
- 查看节点错误日志,定位异常根因并修复
- 节点执行重置命令
RESET MASTER;
STOP GROUP_REPLICATION;
3. 重新启动集群复制
START GROUP_REPLICATION;
4. 查看集群状态,确认节点重新加入并变为ONLINE状态
3.4.3 脑裂问题的预防与处理
MGR基于多数派投票机制,天然避免脑裂问题,只有获得多数派节点支持的分区才能正常提供服务。
-
预防措施:
- 集群节点数必须为奇数,最少3节点
- 配置
group_replication_unreachable_majority_timeout,少数派分区自动退出集群 - 跨机房部署时,保证主机房节点数占多数
-
脑裂处理:少数派分区会自动设置为只读模式,恢复网络后,节点自动重新加入集群,同步数据后恢复正常。
3.4.4 大事务对集群的影响与优化
大事务是MGR集群的头号杀手,单事务过大时,会导致:
- 集群消息广播耗时过长,节点心跳超时,被踢出集群
- 事务冲突检测耗时过长,集群性能急剧下降
- 节点数据同步阻塞,出现延迟
-
优化方案:
- 严格控制单事务大小,生产建议单事务修改行数不超过1000行,事务大小不超过100MB
- 批量操作拆分为多个小事务,分批执行
- 配置
group_replication_transaction_size_limit限制最大事务大小,默认150MB,生产可调整为100MB
3.5 生产最佳实践
- 生产环境必须使用单主模式,禁止使用多主模式
- 集群节点数必须为奇数,推荐3节点,最大不超过9节点
- 所有节点硬件配置保持一致,网络延迟<1ms,禁止跨公网部署集群
- 严格控制事务大小,禁止大事务进入集群,批量操作必须拆分
- 关闭
group_replication_start_on_boot,节点故障后手动确认再加入集群,避免数据异常 - 搭建集群监控体系,核心监控指标:节点状态、主节点角色、集群节点数、事务冲突数、复制延迟
- 定期进行故障演练,验证自动故障切换能力,确保容灾预案有效
四、读写分离:高可用架构的性能扩展
读写分离是基于主从复制架构的性能扩展方案,核心是将写流量集中到主库,读流量分散到多个从库,解决读多写少场景下数据库的性能瓶颈。
4.1 读写分离核心逻辑
4.1.1 核心原理
MySQL主从架构中,主库承担所有写操作,从库同步主库数据并承担读操作。通过路由层将SQL语句分类:
- 写操作(INSERT/UPDATE/DELETE/SELECT ... FOR UPDATE):路由到主库执行
- 读操作(普通SELECT):路由到从库执行,多个从库之间做负载均衡
- 强一致读操作:强制路由到主库执行,避免主从延迟导致的数据不一致
4.1.2 适用与不适用场景
- 适用场景:读多写少业务,读流量占比超过70%,如电商商品查询、资讯内容展示、用户信息查询等
- 不适用场景:读写均衡、写多读少业务,对数据一致性要求极高的实时交易场景,主从延迟敏感的业务
4.2 主流方案对比与选型
4.2.1 应用层方案
在应用代码中通过动态数据源、AOP切面实现读写路由,代表实现为MyBatisPlus动态数据源。
- 优势:无额外中间件、部署简单、性能损耗极小、路由规则灵活定制
- 劣势:与应用代码耦合,多应用无法统一管理,从库故障需应用发布调整
4.2.2 代理层方案
在应用与数据库之间部署代理中间件,中间件实现SQL解析、读写路由、负载均衡、故障自动剔除,代表产品为ProxySQL、MaxScale、ShardingSphere-Proxy。
- 优势:对应用完全透明、多应用统一管理、支持复杂路由规则、从库故障自动剔除、可平滑扩容
- 劣势:额外的运维成本、有一定的性能损耗、需要保障代理中间件的高可用
4.2.3 生产选型建议
- 单应用、中小规模团队:优先选择应用层动态数据源方案,运维成本低,快速落地
- 多应用、大规模集群、企业级场景:优先选择ProxySQL代理层方案,统一管理,可扩展性强
4.3 生产级读写分离落地实战
4.3.1 方案一:基于MyBatisPlus的应用层动态数据源实现
基于Spring Boot 3.2.4、MyBatisPlus 3.5.6、JDK 17实现,通过AOP切面+自定义注解实现读写路由,事务内读强制走主库。
1. pom.xml核心依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/>
</parent>
<groupId>com.jam</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<druid.version>1.2.23</druid.version>
<guava.version>32.1.3-jre</guava.version>
<fastjson2.version>2.0.52</fastjson2.version>
<springdoc.version>2.5.0</springdoc.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
2. 数据源上下文管理
package com.jam.demo.datasource;
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";
/**
* 设置数据源类型
* @param dataSourceType 数据源类型标识
*/
public static void setDataSourceType(String dataSourceType) {
CONTEXT_HOLDER.set(dataSourceType);
}
/**
* 获取当前线程数据源类型
* @return 数据源类型标识
*/
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}
/**
* 清除当前线程数据源类型
*/
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
3. 动态数据源路由实现
package com.jam.demo.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
4. 自定义只读注解
package com.jam.demo.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ReadOnly {
}
5. AOP切面实现数据源切换
package com.jam.demo.aspect;
import com.jam.demo.annotation.ReadOnly;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.ObjectUtils;
@Slf4j
@Aspect
@Component
public class ReadOnlyDataSourceAspect implements Ordered {
@Pointcut("@annotation(com.jam.demo.annotation.ReadOnly)")
public void readOnlyPointCut() {
}
@Around("readOnlyPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
boolean isTransactionActive = TransactionSynchronizationManager.isActualTransactionActive();
MethodSignature signature = (MethodSignature) point.getSignature();
ReadOnly readOnly = signature.getMethod().getAnnotation(ReadOnly.class);
if (!ObjectUtils.isEmpty(readOnly) && !isTransactionActive) {
DynamicDataSourceContextHolder.setDataSourceType(DynamicDataSourceContextHolder.SLAVE_DATASOURCE);
}
try {
return point.proceed();
} finally {
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
6. MyBatisPlus配置类
package com.jam.demo.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.jam.demo.datasource.DynamicRoutingDataSource;
import com.jam.demo.datasource.DynamicDataSourceContextHolder;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.context.properties.ConfigurationProperties;
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 org.springframework.transaction.support.TransactionTemplate;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
@MapperScan(basePackages = "com.jam.demo.mapper")
public class MybatisPlusConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource() {
return new DruidDataSource();
}
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
public DataSource slaveDataSource() {
return new DruidDataSource();
}
@Bean
@Primary
public DataSource dynamicDataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DynamicDataSourceContextHolder.MASTER_DATASOURCE, masterDataSource());
targetDataSources.put(DynamicDataSourceContextHolder.SLAVE_DATASOURCE, slaveDataSource());
DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
return dynamicDataSource;
}
@Bean
public MybatisSqlSessionFactoryBean sqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(dynamicDataSource());
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"));
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
sessionFactory.setConfiguration(configuration);
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setBanner(false);
sessionFactory.setGlobalConfig(globalConfig);
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
sessionFactory.setPlugins(interceptor);
return sessionFactory;
}
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}
@Bean
public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
return new TransactionTemplate(transactionManager);
}
}
7. application.yml配置
spring:
datasource:
druid:
master:
url: jdbc:mysql://192.168.1.100:3306/demo_db?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: Root@2024#Demo
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
slave:
url: jdbc:mysql://192.168.1.101:3306/demo_db?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: Root@2024#Demo
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
springdoc:
swagger-ui:
path: /swagger-ui.html
8. 业务代码实现
实体类
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;
@Data
@TableName("t_user")
@Schema(name = "用户实体", 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 = "13800138000")
private String phone;
@Schema(description = "创建时间")
private LocalDateTime createTime;
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}
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
public interface UserMapper extends BaseMapper<User> {
}
Service层
package com.jam.demo.service;
import com.jam.demo.annotation.ReadOnly;
import com.jam.demo.entity.User;
import com.jam.demo.mapper.UserMapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.Maps;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.util.Map;
@Slf4j
@Service
@Tag(name = "用户服务", description = "用户相关业务处理")
public class UserService extends ServiceImpl<UserMapper, User> {
private final TransactionTemplate transactionTemplate;
public UserService(TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
@Operation(summary = "新增用户", description = "新增用户信息,路由至主库")
public boolean addUser(User user) {
if (ObjectUtils.isEmpty(user) || !StringUtils.hasText(user.getUserName())) {
return false;
}
Map<String, Object> params = Maps.newHashMap();
params.put("user_name", user.getUserName());
if (baseMapper.selectByMap(params).size() > 0) {
log.warn("用户已存在,用户名:{}", user.getUserName());
return false;
}
return transactionTemplate.execute(status -> {
try {
return save(user);
} catch (Exception e) {
status.setRollbackOnly();
log.error("新增用户失败", e);
return false;
}
});
}
@ReadOnly
@Operation(summary = "根据ID查询用户", description = "查询用户信息,路由至从库")
public User getUserById(Long userId) {
if (ObjectUtils.isEmpty(userId)) {
return null;
}
return getById(userId);
}
}
Controller层
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 org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
@Tag(name = "用户接口", description = "用户相关接口")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping("/add")
@Operation(summary = "新增用户", description = "新增用户信息接口")
public ResponseEntity<Boolean> addUser(@RequestBody User user) {
return ResponseEntity.ok(userService.addUser(user));
}
@GetMapping("/get/{userId}")
@Operation(summary = "查询用户", description = "根据用户ID查询用户信息接口")
public ResponseEntity<User> getUserById(@Parameter(description = "用户ID") @PathVariable Long userId) {
return ResponseEntity.ok(userService.getUserById(userId));
}
}
4.3.2 方案二:ProxySQL代理层读写分离部署
ProxySQL是一款高性能的MySQL代理中间件,原生支持读写分离、SQL路由、连接池、故障自动剔除,是生产环境代理层方案的首选。
-
核心部署架构:2台ProxySQL节点+Keepalived实现代理层高可用,避免单点故障
-
核心配置步骤:
- 安装ProxySQL,配置管理账号与监控账号
- 配置主库与从库主机组,写组ID=10,读组ID=20
- 配置监控模块,自动检测后端节点存活状态
- 配置读写路由规则,SELECT语句路由到读组,SELECT ... FOR UPDATE、事务内语句路由到写组
- 配置从库权重,实现读流量负载均衡
-
核心优势:对应用完全透明,应用无需修改任何代码,只需修改数据库连接地址为ProxySQL地址即可实现读写分离。
4.4 核心痛点与解决方案
4.4.1 主从延迟导致的数据不一致
这是读写分离最核心的痛点,主库写入数据后,从库同步有延迟,此时查询从库会读取到旧数据,导致业务异常。 解决方案:
- 强制主库读:对数据一致性要求极高的查询,强制路由到主库执行,如支付结果查询、订单状态更新后的查询
- 延迟感知路由:监控从库延迟,当延迟超过阈值时,自动将读流量切换到主库,延迟恢复后再切回从库
- 缓存方案:写入主库后,同时将数据写入缓存,查询时优先读取缓存,避免查询从库
- GTID一致性读:MySQL 8.0.22+支持GTID一致性读,指定GTID位点查询,确保从库已经重放对应事务后再返回结果
4.4.2 事务内读写的一致性处理
事务内的读操作如果路由到从库,会出现刚写入的数据读不到的问题,同时会破坏事务的隔离性。 解决方案:
- 事务内所有操作强制走主库:判断当前线程是否存在活跃事务,若存在,所有操作都路由到主库,本文代码示例已实现此逻辑
- 只读事务路由到从库:对于纯查询的只读事务,标注为只读,路由到从库执行,提升性能
4.4.3 从库负载均衡与故障剔除
多个从库场景下,需要实现读流量的负载均衡,同时当从库故障时,自动将其从读节点列表中剔除,避免业务异常。 解决方案:
- 应用层方案:通过负载均衡算法实现轮询/权重路由,结合健康检查定时检测从库状态,故障节点自动剔除
- 代理层方案:ProxySQL原生支持从库权重配置、故障自动检测与剔除,无需额外开发
4.5 生产最佳实践
- 读写分离的从库数量建议不超过5个,过多从库会导致主库Dump线程压力过大,主库性能下降
- 核心交易场景的查询必须强制走主库,非核心、非实时的查询路由到从库
- 必须搭建主从延迟监控体系,延迟超过阈值时触发告警,同时自动降级读流量到主库
- 禁止在从库执行大查询、慢查询,避免从库CPU打满导致同步延迟飙升
- 从库配置与主库保持一致,甚至高于主库,避免硬件性能瓶颈导致的延迟
- 定期进行从库故障演练,验证故障剔除与流量切换逻辑的有效性
五、生产级高可用架构整合与容灾预案
5.1 整合架构设计
生产环境最优的高可用架构为:MGR单主集群 + ProxySQL读写分离 + 全链路监控告警,架构图如下:
架构核心优势:
- 数据强一致性:MGR集群基于Paxos协议,RPO=0,彻底避免数据丢失
- 故障自动自愈:MGR自动故障切换,ProxySQL自动识别新主节点,无需人工介入,RTO<30s
- 性能线性扩展:读流量可通过新增Secondary节点线性扩展
- 运维成本低:原生组件,无第三方依赖,稳定性高
5.2 全链路监控体系
生产环境必须搭建完整的监控体系,核心监控指标与告警阈值如下:
-
集群状态监控
- MGR集群节点状态:节点非ONLINE状态立即告警
- 主节点角色变更:主节点切换触发告警
- 集群节点数:节点数少于预期立即告警
-
复制状态监控
- 主从延迟:延迟超过1s告警,超过5s严重告警
- 半同步状态:半同步降级为异步复制立即告警
- GTID差值:主从GTID差值超过10告警
-
数据库性能监控
- CPU使用率:超过80%告警,超过90%严重告警
- 连接数使用率:超过80%告警
- 慢查询数量:每分钟慢查询超过100条告警
- 事务回滚率:超过1%告警,MGR集群事务冲突回滚需重点监控
-
高可用组件监控
- ProxySQL节点存活状态:节点宕机立即告警
- 后端节点健康状态:ProxySQL检测到后端节点故障告警
- Keepalived虚拟IP切换:IP切换触发告警
5.3 容灾预案与故障演练
-
核心场景容灾预案
- 主节点宕机:MGR自动切换主节点,ProxySQL自动识别新主节点,业务无感知,事后排查原主节点故障根因
- 从节点宕机:ProxySQL自动将故障节点从读组剔除,读流量切换到其他从库,事后修复节点重新加入集群
- ProxySQL单节点宕机:Keepalived自动将虚拟IP切换到存活节点,业务无感知,事后修复故障节点
- 机房级故障:跨机房部署的集群,多数派机房正常时,集群自动剔除故障机房节点,业务正常运行;多数派机房故障时,手动切换到少数派机房,恢复服务
-
故障演练要求
- 每月进行一次单节点故障演练,验证自动切换能力
- 每季度进行一次机房级故障演练,验证容灾预案有效性
- 每次演练后复盘优化,完善预案与监控体系
5.4 数据备份策略
高可用架构不能替代数据备份,必须搭建完整的备份体系:
- 全量备份:每天凌晨执行全量物理备份,使用xtrabackup工具,备份保留周期30天
- 增量备份:每6小时执行一次增量备份,配合全量备份实现时间点恢复
- binlog备份:binlog实时备份到异地存储,保留周期30天,支持任意时间点恢复
- 备份验证:每周进行一次备份恢复演练,验证备份的有效性,确保故障时可正常恢复数据
六、总结
MySQL高可用架构的核心目标是保障业务连续性与数据可靠性,没有万能的架构,只有最适合业务场景的架构。
- 中小规模、非核心业务:主从复制+应用层读写分离架构,部署简单,运维成本低
- 核心交易、金融级业务:MGR单主集群+ProxySQL读写分离架构,数据强一致,故障自动切换,满足高可用要求