MySQL高可用生产落地全解:主从同步、MGR集群、读写分离从原理到实战

简介: 本文系统讲解MySQL高可用三大核心:主从复制(含GTID、增强半同步实战)、MGR原生集群(单主模式部署、自动选主、脑裂防护)及读写分离(应用层/ProxySQL方案)。涵盖RTO/RPO指标、故障根因分析、全场景最佳实践与容灾预案,助你构建稳定、高性能、可扩展的生产级高可用体系。

引言

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 核心组件详解

  1. binlog二进制日志:主库上记录所有数据修改操作的日志,是主从复制的数据源。生产环境必须使用ROW行级格式,记录每一行数据的修改前后状态,彻底避免主从不一致问题。
  2. Dump线程:主库上的后台线程,当从库建立连接后,读取binlog日志并发送给从库,每个从库对应一个独立的Dump线程。
  3. IO线程:从库上的后台线程,连接主库并接收binlog事件,写入本地relaylog中继日志。
  4. 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 环境前置准备

  1. 两台服务器关闭防火墙与SELinux,配置主机名与hosts解析
  2. 两台服务器时间同步,误差不超过1s
  3. 两台服务器安装相同版本的MySQL 8.0.36,初始化完成后启动服务

2.3.2 主库配置与权限设置

  1. 主库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

  1. 重启主库MySQL服务,创建复制专用用户

CREATE USER 'repl'@'%' IDENTIFIED BY 'Repl@2024#Demo';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;

  1. 主库数据备份(若已有业务数据)

mysqldump -uroot -p --single-transaction --master-data=2 --all-databases > all_db_backup.sql

2.3.3 从库配置与同步搭建

  1. 从库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

  1. 重启从库MySQL服务,导入主库备份数据(若有)

mysql -uroot -p < all_db_backup.sql

  1. 从库配置主从同步

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;

  1. 启动从库同步

START REPLICA;

2.3.4 同步状态验证

  1. 查看从库同步状态

SHOW REPLICA STATUS\G

核心验证项:

  • Replica_IO_Running: Yes
  • Replica_SQL_Running: Yes
  • Seconds_Behind_Source: 0
  • Retrieved_Gtid_SetExecuted_Gtid_Set与主库一致
  1. 验证半同步状态

SHOW STATUS LIKE 'Rpl_semi_sync%';

核心验证项:Rpl_semi_sync_master_clients值为1,代表半同步连接正常。

2.4 生产故障排查与解决方案

2.4.1 主从同步延迟根因与优化

同步延迟是生产环境最常见的问题,核心根因与优化方案如下:

  1. SQL线程单线程重放瓶颈:MySQL 5.6之前SQL线程为单线程,大事务或高并发写入场景下重放速度跟不上主库。
  • 优化方案:开启8.0并行复制,配置slave_parallel_type=LOGICAL_CLOCKslave_parallel_workers=CPU核心数slave_preserve_commit_order=ON
  1. 大事务导致的延迟:主库执行超大事务(如批量删除百万级数据),binlog传输与重放耗时过长。
  • 优化方案:大事务拆分为多个小事务,单事务修改行数不超过1000行
  1. 从库硬件性能不足:从库CPU、IO性能弱于主库,无法跟上主库写入速度。
  • 优化方案:从库硬件配置不低于主库,关闭从库查询缓存,优化慢查询
  1. 无主键表导致的重放缓慢:ROW格式下,无主键表的修改操作会触发全表扫描,重放效率极低。
  • 优化方案:所有表必须设置主键,推荐使用自增主键或雪花ID主键

2.4.2 事务冲突处理

从库重放事务时出现主键冲突、唯一键冲突,导致SQL线程停止,报错Error_code: 1062

  1. 根因:主从库同时写入数据、从库数据手动修改、binlog事件重复重放
  2. 临时解决方案(GTID模式):

-- 停止同步
STOP REPLICA;
-- 设置跳过冲突的GTID事务,替换为实际报错的GTID
SET GTID_NEXT='aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:1001';
-- 空事务跳过
BEGIN;COMMIT;
-- 恢复自动GTID
SET GTID_NEXT='AUTOMATIC';
-- 重启同步
START REPLICA;

  1. 根治方案:禁止从库写入数据,配置从库read_only=ONsuper_read_only=ON,仅复制用户拥有超级权限。

2.4.3 主库宕机手动切换流程

  1. 确认主库已无法访问,停止从库同步

STOP REPLICA;
RESET MASTER;

  1. 从库提升为新主库,关闭只读模式

SET GLOBAL read_only=OFF;
SET GLOBAL super_read_only=OFF;

  1. 业务切换数据库连接地址到新主库
  2. 原主库恢复后,配置为新主库的从库,重新搭建同步

2.5 生产最佳实践

  1. 所有主从集群必须开启GTID模式,禁止使用基于文件位点的传统复制
  2. 生产环境必须使用增强半同步复制,rpl_semi_sync_master_timeout设置为1000ms,超时自动降级为异步复制,避免主库阻塞
  3. 从库必须开启read_only=ONsuper_read_only=ON,禁止手动写入数据
  4. 所有表必须设置主键,禁止无主键表进入生产环境
  5. binlog格式必须使用ROWbinlog_row_image=FULL,避免主从不一致
  6. 搭建主从监控体系,核心监控指标:同步状态、延迟时长、GTID差值、半同步状态

三、MGR(MySQL Group Replication):原生分布式高可用集群

MGR是MySQL 5.7.17+引入的原生分布式高可用解决方案,基于Paxos分布式共识协议实现,提供数据强一致性、自动故障检测、自动选主、故障自愈能力,是金融级核心业务的首选方案。

3.1 MGR核心原理与架构

3.1.1 核心架构与组件

MGR集群由多个节点组成,生产环境推荐3节点/5节点奇数节点架构,分为单主模式和多主模式两种运行模式。 核心组件:

  1. 共识层:基于Paxos协议实现,负责集群内消息广播、事务全局排序、多数派确认,只有获得多数派节点ACK的事务才能提交
  2. 故障检测模块:集群内节点定期交换心跳信息,当节点超过阈值未响应时,集群多数派投票判定节点异常,自动将异常节点踢出集群
  3. 冲突检测模块:基于事务写集合实现,并发事务修改同一行数据时,集群会检测冲突,先提交的事务生效,后提交的事务回滚
  4. 自动选主模块:单主模式下,主节点异常时,集群自动根据节点权重、GTID执行情况选举新的主节点,整个过程秒级完成

3.1.2 与传统主从复制的核心区别

特性 传统主从复制 MGR集群
一致性保障 最终一致,半同步仅降低丢失风险 强一致,多数派提交,RPO=0
故障切换 手动/第三方工具实现,分钟级 原生自动实现,秒级完成
脑裂防护 无原生支持,需第三方组件 原生基于多数派机制,彻底避免脑裂
节点扩展 手动配置,复杂度高 节点自动加入,自动同步数据
写入性能 单主写入,无额外共识开销 单主写入,有共识开销,性能损耗约10%-20%

3.2 两种运行模式详解

3.2.1 单主模式

集群中只有一个Primary主节点可读写,其余Secondary节点均为只读节点,是生产环境的首选模式。

  • 核心优势:无分布式事务冲突风险,运维复杂度低,数据一致性保障最高,兼容性与主从复制完全一致
  • 自动选主规则:主节点异常时,集群优先选择GTID执行最完整的节点,权重相同的情况下选择server_id最小的节点
  • 适用场景:绝大多数业务场景,尤其是核心交易系统、金融级业务

3.2.2 多主模式

集群中所有节点都可提供读写服务,写入会同步到所有节点,生产环境不推荐使用。

  • 核心限制:
  1. 不支持外键级联约束
  2. 不支持SERIALIZABLE隔离级别
  3. 大事务会导致集群阻塞,甚至节点踢出
  4. 并发修改同一行数据会导致大量事务回滚,性能急剧下降
  • 适用场景:极少写入、多地域就近读取的特殊业务场景

3.3 生产级3节点MGR单主集群部署实战

本次部署基于MySQL 8.0.36,3节点单主模式,开启强一致性保障。

3.3.1 环境前置准备

  1. 3台服务器关闭防火墙与SELinux,配置hosts解析,时间同步误差<1s
  2. 3台服务器安装相同版本的MySQL 8.0.36,初始化完成
  3. 服务器之间网络互通,开放3306(MySQL端口)、33061(MGR通信端口)
  4. 3台服务器配置相同的MySQL参数,仅server_id、本地通信地址不同

3.3.2 节点统一配置

3个节点的my.cnf核心配置,仅需修改server_idgroup_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 集群引导与节点加入

  1. 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';

  1. 主节点引导集群(仅第一个节点执行,仅执行一次)

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

  1. 查看集群状态,确认主节点正常加入

SELECT * FROM performance_schema.replication_group_members;

  1. 其余两个节点加入集群,直接执行启动命令

START GROUP_REPLICATION;

  1. 3个节点都执行完成后,再次查看集群状态,确认3个节点都为ONLINE状态

3.3.4 集群状态验证

  1. 查看集群节点状态

SELECT MEMBER_ID,MEMBER_HOST,MEMBER_PORT,MEMBER_STATE,MEMBER_ROLE FROM performance_schema.replication_group_members;

验证结果:3个节点MEMBER_STATE均为ONLINE,其中1个节点MEMBER_ROLEPRIMARY,其余两个为SECONDARY

  1. 查看主节点信息

SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME='group_replication_primary_member';

  1. 验证只读配置:从节点默认开启超级只读,无法写入数据,主节点可正常读写

3.4 生产故障处理与容灾机制

3.4.1 自动故障切换流程

单主模式下,主节点异常时,集群自动执行以下流程:

  1. 故障检测:集群节点心跳超时,多数派投票判定主节点异常,将其标记为UNREACHABLE
  2. 选主投票:集群从剩余ONLINE节点中,按照GTID执行进度、节点权重选举新的主节点
  3. 角色切换:新主节点关闭只读模式,提升为PRIMARY角色,其余从节点自动同步到新主节点
  4. 故障节点处理:原主节点恢复后,自动加入集群,变为SECONDARY角色 整个切换过程在30s内完成,业务仅会出现短暂的连接中断,无需人工介入。

3.4.2 节点异常退出与重新加入

节点异常退出后,状态变为ERROR或UNREACHABLE,重新加入步骤:

  1. 查看节点错误日志,定位异常根因并修复
  2. 节点执行重置命令

RESET MASTER;
STOP GROUP_REPLICATION;

  1. 重新启动集群复制

START GROUP_REPLICATION;

  1. 查看集群状态,确认节点重新加入并变为ONLINE状态

3.4.3 脑裂问题的预防与处理

MGR基于多数派投票机制,天然避免脑裂问题,只有获得多数派节点支持的分区才能正常提供服务。

  • 预防措施:
  1. 集群节点数必须为奇数,最少3节点
  2. 配置group_replication_unreachable_majority_timeout,少数派分区自动退出集群
  3. 跨机房部署时,保证主机房节点数占多数
  • 脑裂处理:少数派分区会自动设置为只读模式,恢复网络后,节点自动重新加入集群,同步数据后恢复正常。

3.4.4 大事务对集群的影响与优化

大事务是MGR集群的头号杀手,单事务过大时,会导致:

  1. 集群消息广播耗时过长,节点心跳超时,被踢出集群
  2. 事务冲突检测耗时过长,集群性能急剧下降
  3. 节点数据同步阻塞,出现延迟
  • 优化方案:
  1. 严格控制单事务大小,生产建议单事务修改行数不超过1000行,事务大小不超过100MB
  2. 批量操作拆分为多个小事务,分批执行
  3. 配置group_replication_transaction_size_limit限制最大事务大小,默认150MB,生产可调整为100MB

3.5 生产最佳实践

  1. 生产环境必须使用单主模式,禁止使用多主模式
  2. 集群节点数必须为奇数,推荐3节点,最大不超过9节点
  3. 所有节点硬件配置保持一致,网络延迟<1ms,禁止跨公网部署集群
  4. 严格控制事务大小,禁止大事务进入集群,批量操作必须拆分
  5. 关闭group_replication_start_on_boot,节点故障后手动确认再加入集群,避免数据异常
  6. 搭建集群监控体系,核心监控指标:节点状态、主节点角色、集群节点数、事务冲突数、复制延迟
  7. 定期进行故障演练,验证自动故障切换能力,确保容灾预案有效

四、读写分离:高可用架构的性能扩展

读写分离是基于主从复制架构的性能扩展方案,核心是将写流量集中到主库,读流量分散到多个从库,解决读多写少场景下数据库的性能瓶颈。

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路由、连接池、故障自动剔除,是生产环境代理层方案的首选。

  1. 核心部署架构:2台ProxySQL节点+Keepalived实现代理层高可用,避免单点故障
  2. 核心配置步骤:
  • 安装ProxySQL,配置管理账号与监控账号
  • 配置主库与从库主机组,写组ID=10,读组ID=20
  • 配置监控模块,自动检测后端节点存活状态
  • 配置读写路由规则,SELECT语句路由到读组,SELECT ... FOR UPDATE、事务内语句路由到写组
  • 配置从库权重,实现读流量负载均衡
  1. 核心优势:对应用完全透明,应用无需修改任何代码,只需修改数据库连接地址为ProxySQL地址即可实现读写分离。

4.4 核心痛点与解决方案

4.4.1 主从延迟导致的数据不一致

这是读写分离最核心的痛点,主库写入数据后,从库同步有延迟,此时查询从库会读取到旧数据,导致业务异常。 解决方案:

  1. 强制主库读:对数据一致性要求极高的查询,强制路由到主库执行,如支付结果查询、订单状态更新后的查询
  2. 延迟感知路由:监控从库延迟,当延迟超过阈值时,自动将读流量切换到主库,延迟恢复后再切回从库
  3. 缓存方案:写入主库后,同时将数据写入缓存,查询时优先读取缓存,避免查询从库
  4. GTID一致性读:MySQL 8.0.22+支持GTID一致性读,指定GTID位点查询,确保从库已经重放对应事务后再返回结果

4.4.2 事务内读写的一致性处理

事务内的读操作如果路由到从库,会出现刚写入的数据读不到的问题,同时会破坏事务的隔离性。 解决方案:

  1. 事务内所有操作强制走主库:判断当前线程是否存在活跃事务,若存在,所有操作都路由到主库,本文代码示例已实现此逻辑
  2. 只读事务路由到从库:对于纯查询的只读事务,标注为只读,路由到从库执行,提升性能

4.4.3 从库负载均衡与故障剔除

多个从库场景下,需要实现读流量的负载均衡,同时当从库故障时,自动将其从读节点列表中剔除,避免业务异常。 解决方案:

  1. 应用层方案:通过负载均衡算法实现轮询/权重路由,结合健康检查定时检测从库状态,故障节点自动剔除
  2. 代理层方案:ProxySQL原生支持从库权重配置、故障自动检测与剔除,无需额外开发

4.5 生产最佳实践

  1. 读写分离的从库数量建议不超过5个,过多从库会导致主库Dump线程压力过大,主库性能下降
  2. 核心交易场景的查询必须强制走主库,非核心、非实时的查询路由到从库
  3. 必须搭建主从延迟监控体系,延迟超过阈值时触发告警,同时自动降级读流量到主库
  4. 禁止在从库执行大查询、慢查询,避免从库CPU打满导致同步延迟飙升
  5. 从库配置与主库保持一致,甚至高于主库,避免硬件性能瓶颈导致的延迟
  6. 定期进行从库故障演练,验证故障剔除与流量切换逻辑的有效性

五、生产级高可用架构整合与容灾预案

5.1 整合架构设计

生产环境最优的高可用架构为:MGR单主集群 + ProxySQL读写分离 + 全链路监控告警,架构图如下:

架构核心优势:

  • 数据强一致性:MGR集群基于Paxos协议,RPO=0,彻底避免数据丢失
  • 故障自动自愈:MGR自动故障切换,ProxySQL自动识别新主节点,无需人工介入,RTO<30s
  • 性能线性扩展:读流量可通过新增Secondary节点线性扩展
  • 运维成本低:原生组件,无第三方依赖,稳定性高

5.2 全链路监控体系

生产环境必须搭建完整的监控体系,核心监控指标与告警阈值如下:

  1. 集群状态监控
  • MGR集群节点状态:节点非ONLINE状态立即告警
  • 主节点角色变更:主节点切换触发告警
  • 集群节点数:节点数少于预期立即告警
  1. 复制状态监控
  • 主从延迟:延迟超过1s告警,超过5s严重告警
  • 半同步状态:半同步降级为异步复制立即告警
  • GTID差值:主从GTID差值超过10告警
  1. 数据库性能监控
  • CPU使用率:超过80%告警,超过90%严重告警
  • 连接数使用率:超过80%告警
  • 慢查询数量:每分钟慢查询超过100条告警
  • 事务回滚率:超过1%告警,MGR集群事务冲突回滚需重点监控
  1. 高可用组件监控
  • ProxySQL节点存活状态:节点宕机立即告警
  • 后端节点健康状态:ProxySQL检测到后端节点故障告警
  • Keepalived虚拟IP切换:IP切换触发告警

5.3 容灾预案与故障演练

  1. 核心场景容灾预案
  • 主节点宕机:MGR自动切换主节点,ProxySQL自动识别新主节点,业务无感知,事后排查原主节点故障根因
  • 从节点宕机:ProxySQL自动将故障节点从读组剔除,读流量切换到其他从库,事后修复节点重新加入集群
  • ProxySQL单节点宕机:Keepalived自动将虚拟IP切换到存活节点,业务无感知,事后修复故障节点
  • 机房级故障:跨机房部署的集群,多数派机房正常时,集群自动剔除故障机房节点,业务正常运行;多数派机房故障时,手动切换到少数派机房,恢复服务
  1. 故障演练要求
  • 每月进行一次单节点故障演练,验证自动切换能力
  • 每季度进行一次机房级故障演练,验证容灾预案有效性
  • 每次演练后复盘优化,完善预案与监控体系

5.4 数据备份策略

高可用架构不能替代数据备份,必须搭建完整的备份体系:

  1. 全量备份:每天凌晨执行全量物理备份,使用xtrabackup工具,备份保留周期30天
  2. 增量备份:每6小时执行一次增量备份,配合全量备份实现时间点恢复
  3. binlog备份:binlog实时备份到异地存储,保留周期30天,支持任意时间点恢复
  4. 备份验证:每周进行一次备份恢复演练,验证备份的有效性,确保故障时可正常恢复数据

六、总结

MySQL高可用架构的核心目标是保障业务连续性与数据可靠性,没有万能的架构,只有最适合业务场景的架构。

  • 中小规模、非核心业务:主从复制+应用层读写分离架构,部署简单,运维成本低
  • 核心交易、金融级业务:MGR单主集群+ProxySQL读写分离架构,数据强一致,故障自动切换,满足高可用要求
目录
相关文章
|
11天前
|
人工智能 安全 Linux
【OpenClaw保姆级图文教程】阿里云/本地部署集成模型Ollama/Qwen3.5/百炼 API 步骤流程及避坑指南
2026年,AI代理工具的部署逻辑已从“单一云端依赖”转向“云端+本地双轨模式”。OpenClaw(曾用名Clawdbot)作为开源AI代理框架,既支持对接阿里云百炼等云端免费API,也能通过Ollama部署本地大模型,完美解决两类核心需求:一是担心云端API泄露核心数据的隐私安全诉求;二是频繁调用导致token消耗过高的成本控制需求。
5593 13
|
19天前
|
人工智能 JavaScript Ubuntu
5分钟上手龙虾AI!OpenClaw部署(阿里云+本地)+ 免费多模型配置保姆级教程(MiniMax、Claude、阿里云百炼)
OpenClaw(昵称“龙虾AI”)作为2026年热门的开源个人AI助手,由PSPDFKit创始人Peter Steinberger开发,核心优势在于“真正执行任务”——不仅能聊天互动,还能自动处理邮件、管理日程、订机票、写代码等,且所有数据本地处理,隐私完全可控。它支持接入MiniMax、Claude、GPT等多类大模型,兼容微信、Telegram、飞书等主流聊天工具,搭配100+可扩展技能,成为兼顾实用性与隐私性的AI工具首选。
22182 118