只要你的 MySQL 集群上过生产,大概率都撞到过"集群一致性问题"这个词。它通常不会以一条报错的形式出现,而是藏在业务反馈里:"读写分离刚改完数据,下一页又读到旧的""主备切换之后订单状态回退了""两边导出来的报表数字对不上"。我在排查群里见过不少朋友一听到一致性,第一反应就是换方案、加中间件,结果问题反而更多。这篇内容不教你怎么一步到位上强一致方案,而是把 MySQL 集群一致性问题的成因、常见方案的取舍、能落地验证的手段和日常运维的坑一次讲清楚。适合正在维护 MySQL 主从或集群结构的 DBA、后端开发,也适合被延迟和数据对不上问题折腾过的朋友。
1. 项目概述:要的到底是"一致"还是"可用"
1.1 集群不一致的四个典型现象
先说最直观的四个现象,很多团队都是在这些场景里第一次意识到一致性问题。
第一个是读写分离读到旧数据。应用写完后立刻去查,请求被路由到从库,返回的还是旧值。用户刷新一下又好了,但是对账或者客服侧就会看到"数据反复横跳"。这种问题最容易在刚上线读写分离时爆发。
第二个是主备切换丢事务。主库磁盘故障或者进程挂掉,DBA 把从库提升上来,结果发现最近几秒提交的订单记录没有了。故障演练时一对比 binlog 和业务日志,丢了事务,业务方立刻炸毛。这类问题在高可用脚本不完善时非常常见。
第三个是双主互相覆盖。两个节点同时接受写请求,分别更新同一行数据,没有冲突检测机制的话,后写入的会静默覆盖前者"旧版本"的变更。自增主键还可能在两边生成相同的值,导致同步直接报 duplicate key 中断。
第四个是数据校验不一致。拿校验工具全量比对主从,发现几百行甚至几千行差异,但打开延迟监控看 Seconds_Behind_Master 一直是 0。这就说明问题不是简单延迟,而是复制链路里有事务丢失、被跳过或者双写冲突长期积累。
这四个现象看着完全不一样,但根子上都指向同一个事实:数据从主节点传播到从节点,不是瞬间完成的,这条传播链路上任何一个环节出问题,都会放大不一致。
1.2 不一致问题从哪来:CAP 是底色
要理解 MySQL 主从集群的一致性,得先接受一个前提:MySQL 原生的主从复制模型,本身就不是分布式强一致方案。它默认走的是异步复制,主库提交事务成功后立刻返回客户端成功,然后后台再把 binlog 推给从库。这个设计换来的是高性能和高可用,代价就是数据一致性窗口。
你可以想象成两套账本。主库的记账员已经给客户签字确认"这笔钱到账了",分店的记账员还在路上,账本上没有这笔记录。客户去分店查余额,自然查不到。对一个记账系统来说,这个窗口可能只有几毫秒,对大多数业务没问题;但一旦延迟堆到秒级甚至分钟级,问题就显现了。
CAP 理论在这里就是底色。在分区发生时,一个分布式系统必须在一致性和可用性之间做选择。MySQL 集群默认用了大部分互联网业务最熟悉的组合:高可用 + 最终一致。同步复制、强一致方案都存在,但都要付出更高成本。所以第一步不是问"怎么做到绝对一致",而是问"业务能容忍多大的数据窗口"。这个问题想清楚,下面的方案选择才有方向。
2. 机制拆解:不一致是在哪一环节被放大的
2.1 主从复制链路的三个关键窗口
主从复制的完整链路可以分成四段:应用写入主库,主库把事务写进 binlog 并提交存储引擎;主库的 dump 线程把 binlog 传给从库;从库的 IO 线程接收并写入 relay log;从库的 SQL 线程异步回放 relay log,把数据落到从库引擎里。
链路里有三个窗口最值得盯。
第一个窗口是主库 binlog 的落盘窗口。MySQL 通过 sync_binlog 参数控制 binlog 刷盘策略。如果设为 0,MySQL 依赖操作系统刷盘,主库进程崩溃或整机宕机时,最后一批 binlog 可能没真正落到磁盘。机器重启后,这些事务就不见了,从库自然也没收到。对持久性要求高的场景,sync_binlog 必须设为 1。
第二个窗口是从库收到 relay log 但还没落盘。从库 IO 线程把日志写进 relay log 后,同样存在操作系统缓存问题。如果从库崩溃,relay log 可能丢失一部分,主库会重新推送,一般影响不大,但要留意磁盘写入能力不足导致的 IO 线程堆积。
第三个窗口是 SQL 线程回放,这是主从延迟的主要来源。SQL 线程把 relay log 里的事务一条条执行到从库引擎,它是一个独立的异步过程。从库可以开并行复制,但大事务、DDL、锁等待依然会造成回放阻塞,延迟就从这里长出来。
很多人以为主从延迟是网络慢,其实大部分时间都耗在回放上。主库执行一个 UPDATE 可能用了 100 毫秒,从库回放同一个大事务可能要几十秒。这期间主库还在继续提交新事务,relay log 越积越多,延迟就滚雪球一样变大。
2.2 半同步复制的原理与"伪一致"陷阱
异步复制的问题在于"主库说自己成功,但日志还没到从库"。半同步复制就是为了缩小这个窗口。
半同步的核心逻辑是:主库提交事务时,必须等待至少一个从库确认收到了 binlog,才能给客户端返回成功。注意关键词是"收到 binlog",不是"回放完成"。也就是说,从库的 relay log 里有了这个事务,半同步就认为 OK 了,哪怕这个事务还没执行到从库的数据文件里。
以常见的 AFTER_SYNC 模式为例,主库先把事务写进 binlog,然后等待从库 ACK,收到确认后再提交存储引擎。这种模式的好处是,即使主库在事务引擎提交前宕机,从库里也已经有了这条日志,提升从库后事务不会丢。这是 MySQL 5.7 之后的主推配置。
但半同步有一个知名陷阱:超时降级。rpl_semi_sync_master_timeout 默认是 10 秒,如果 10 秒内没有从库返回 ACK,主库会自动把复制模式降级为异步。降级之后,主库不再等待任何人,直接提交返回。这个降级动作本身是为了保证可用性,但它把一致性窗口瞬间又放大了。更头疼的是,如果不监控状态变量,整个团队根本不知道半同步已经静默失效。
所以对半同步的理解要到位:它防的是"提交后 binlog 一点没复制就丢日志",但不防"从库回放太慢导致读到旧数据",也不防"超过 timeout 后的降级窗口"。依赖半同步之前,先确认监控有没有覆盖降级事件。
2.3 组复制与分布式事务的适用边界
如果业务确实需要比半同步更强的一致性保证,很多团队的下一站是 MySQL Group Replication,也就是 MGR。
MGR 的底层是一套类 Paxos 的组共识协议,事务需要经过组内多数派节点确认才算真正提交。它的价值在于:每个节点对事务的顺序达成一致,任何一个节点上发生的写入,要么被组接受,要么被拒绝,配合自动选主机制,高可用切换时更不容易丢数据。
但 MGR 不是银弹。它要求网络稳定,节点间往返时延不能太高,跨机房部署时这个问题尤其明显。性能上,它需要每个事务都做一次组内协商,吞吐上限受最慢节点影响,不像异步复制那样主库写完就完事。多主模式可以接受多个节点写入,但冲突检测机制是"发现冲突直接回滚",应用层如果没有处理,线上就会冒出大量报错。
分布式事务 XA 是另一种思路,通过两阶段提交让多个 MySQL 实例的写入保持原子性。这里要给你一个非常直接的建议:能不用就别用,除非业务必须在一个事务里跨库更新。XA 的性能开销、协调者单点、故障恢复复杂度,都比普通事务高一个量级,很多团队上线后都要为此专门开发补偿机制。
所以你会发现,大部分高并发业务最终选的不是"绝对强一致",而是"异步复制 + 半同步缩小窗口 + 快速切换 + 定期校验修复"。这个组合在一致性和成本之间找到了一个相对务实的平衡点。
3. 实操实录:用最小集群验证一致性窗口
说了这么多原理,不实操验证一遍很难建立体感。下面用一主一从的最小集群,带你走一遍:搭建半同步环境、制造延迟、模拟故障切换,亲眼看看不一致是哪里来的。
3.1 搭建一套带 GTID 的半同步集群
先准备两台实例,主库和从库。我建议从一开始就打开 GTID,后面做切换和对比会非常方便。主库的 my.cnf 里至少要有这些参数:
server-id = 1 log_bin = /data/mysql/binlog binlog_format = ROW gtid_mode = ON enforce_gtid_consistency = ON sync_binlog = 1 innodb_flush_log_at_trx_commit = 1从库除了 server-id 换成 2,还需要加上:
server-id = 2 relay_log = /data/mysql/relaylog read_only = ON gtid_mode = ON enforce_gtid_consistency = ON从库配置 read_only 是必须的,防止误写入导致数据分叉。然后做主从关系建立,8.0 后 CHANGE MASTER TO 和 CHANGE REPLICATION SOURCE TO 都可以用,新环境建议用后者:
CHANGE REPLICATION SOURCE TO SOURCE_HOST='主库IP', SOURCE_PORT=3306, SOURCE_USER='repl', SOURCE_PASSWORD='密码', SOURCE_AUTO_POSITION=1; START REPLICA;基础复制跑通后,加载半同步插件,主库和从库都要执行:
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so'; INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';主库设置:
SET GLOBAL rpl_semi_sync_master_enabled = 1; SET GLOBAL rpl_semi_sync_master_timeout = 3000;从库设置:
SET GLOBAL rpl_semi_sync_slave_enabled = 1; STOP REPLICA; START REPLICA;然后可以通过状态变量确认半同步是否生效:
SHOW GLOBAL STATUS LIKE 'Rpl_semi_sync_master_status'; SHOW GLOBAL STATUS LIKE 'Rpl_semi_sync_master_clients';如果 Rpl_semi_sync_master_status 是 ON,说明主库正处半同步模式。整个过程不复杂,但要注意插件加载顺序和从库重启复制线程,很多人只改参数忘记重启,半同步一直没有真正激活。
3.2 制造主从延迟,实测读到的数据有多旧
集群稳定后,我们来人为制造一个延迟场景。延迟最容易被大事务放大,所以在主库执行一个大范围更新:
UPDATE 大表 SET remark = 'batch_update' WHERE id IN (SELECT id FROM 大表 LIMIT 2000000);这个大事务在从库回放期间,主库继续正常接收业务请求。此时我开一个循环脚本往主库插入带序号的数据,同时不停在从库查询最大 ID:
for i in $(seq 1 10000); do mysql -h主库 -e "INSERT INTO orders(user_id, amount) VALUES ($i, 100);" max_id=$(mysql -h从库 -N -e "SELECT MAX(id) FROM orders;") echo "inserted_on_master=$i, max_on_slave=$max_id" sleep 0.1 done结果会非常直观:主库已经插入到 5000,从库最大 ID 还停在 1200。这个差值就是业务读取到的数据延迟窗口。配合下面的状态查看,一眼就能看到复制已经在排队:
SHOW REPLICA STATUS\G -- 重点看 Seconds_Behind_Master、Retrieved_Gtid_Set、Executed_Gtid_SetRetrieved_Gtid_Set 是从库已经拉取到 relay log 的 GTID 集合,Executed_Gtid_Set 是已经回放完成的 GTID 集合。两个集合的差,就是真正造成数据滞后的部分。这里要专门提醒一句:Seconds_Behind_Master 不是实时靠时钟算出来的准确值,它依赖主从日志时间戳,遇到长时间未提交事务或者从库 SQL 线程处于半空闲时,它可能不准甚至为 NULL。所以排查时永远要结合 GTID 差值一起看。
这个实验做完,你就能准确告诉业务方:"当前从库最多落后主库 N 个事务,读写分离场景下用户最多会读到 N 个事务之前的数据。"有了这个可量化的窗口,评估一致性方案的收益就有依据了。
3.3 模拟故障切换,验证会不会丢数据
延迟实验做了,再做一次更刺激的:主库直接宕机,看切换后丢不丢数据。
先确保应用已经暂停写入。然后用 kill -9 直接杀主库进程,模拟非优雅宕机。这种模拟下,主库来不及刷盘、来不及通知从库,最接近事故现场。
此刻观察从库的状态,Relay 日志回放会慢慢追到 Relay_Master_Log_File 的末尾,但主库已经断开,不可能再推送新日志。手动提升从库前,先确认两个信息:
SHOW REPLICA STATUS\G -- 确认 Replica_IO_Running: No,Replica_SQL_Running: Yes,且 Executed_Gtid_Set 不再变化然后让从库脱离复制关系并开放可写:
STOP REPLICA; RESET REPLICA ALL; SET GLOBAL read_only = OFF;提升之后,回到旧主库的 binlog 里,找到宕机前最后一个提交事务的 GTID,再对比新主库实际的 gtid_executed,缺失的那部分事务,就是这次故障切换丢失的数据量。你可以在旧主库 binlog 上执行:
SHOW BINLOG EVENTS IN '旧主库最后的binlog文件' FROM 最后位置;这样就能精确数出哪些事务没有复制过去。半同步模式下,大部分刚提交的事务已经在从库 relay log 里,但因为 timeout 窗口的存在,最后一个或几个事务仍然可能丢失。这个实验在绝大多数环境里都能测出非预期的数据缺口,所以生产环境的切换脚本,绝不能只看"进程起来了"就算成功,必须做数据校验。
4. 常见问题排查与运维避坑
4.1 高频问题与排查动作速查表
下面这张表我整理自平时遇到的比较典型的线上问题,可以收藏起来当排查手册用。
| 现象 | 可能原因 | 优先排查动作 |
|---|---|---|
| Seconds_Behind_Master 长期为 NULL | 复制线程中断或状态异常 | 看 Replica_IO_Running / Replica_SQL_Running,再查看错误日志 |
| 延迟持续增长不收敛 | 大事务回放慢、从库负载高、并行复制线程不足 | SHOW PROCESSLIST 看回放线程当前执行语句,结合慢日志定位 |
| 半同步状态从 ON 变 OFF | 超过 rpl_semi_sync_master_timeout 自动降级 | 查 Rpl_semi_sync_master_no_sync_count,确认从库 IO 线程是否断开 |
| 复制中断,报错 duplicate key | 切换后 GTID 冲突或双主写入冲突 | 对比主从 gtid_executed,找重复事务来源,必要时重建复制 |
| relay log 损坏 | 磁盘异常或从库崩溃未恢复 | 从失败位置重置中继日志,或重新 CHANGE REPLICATION SOURCE TO |
| 数据校验不一致但延迟为 0 | binlog_format 不是 ROW、使用非确定性函数、有人手工改从库 | 校验 GTID 集合,检查 binlog_format 参数,核对白名单过滤规则 |
| 切换后丢事务 | 没开半同步、sync_binlog=0、从库没追平就开放写 | 切换前检查 Executed_Gtid_Set,切换后立即做行数校验 |
这里的核心思想只有一个:看到表面现象后不要急着解表,先用 GTID 集合把"主库有而从库没有的事务"找出来,再顺着这个集合去定位环节。
4.2 日常巡检的三个核心指标
日常运维中,我建议团队只盯三个核心指标,抓多了反而会分散注意力。
第一个是半同步状态。主库上持续观察:
SHOW GLOBAL STATUS LIKE 'Rpl_semi_sync_master_status';只要这个值为 OFF,就意味着集群已经退化到异步复制,必须立刻告警。同时关注计数器:
SHOW GLOBAL STATUS LIKE 'Rpl_semi_sync_master_no_sync_count';这个数字代表有多少事务是在降级状态下提交的,它越大,数据丢失风险越高。
第二个是延迟趋势。不要只看瞬时 Seconds_Behind_Master 是否为 0,要看延迟量的曲线走向。一次大促或者一轮批量任务,延迟可能从 0 冲到几十秒,只要它能逐步下降,恢复收敛就没有大问题;如果长时间横盘不降,甚至缓慢爬升,那多半有回放卡点。
第三个是 GTID 差值。在主从两边分别查 gtid_executed,用 GTID_SUBTRACT 计算差集:
SELECT GTID_SUBTRACT( (SELECT @@GLOBAL.gtid_executed ON 主库), (SELECT @@GLOBAL.gtid_executed ON 从库) );差集是否存在比差集大小更关键。只要有差集,就说明还有事务没到从库,延迟为 0 也不代表完全对齐。这比盯着一个秒数靠谱得多。
4.3 操作禁忌与经验提醒
再分享几条我个人踩过的坑,希望大家不要再踩。
第一条,不要把 binlog_format 改成 STATEMENT 来省空间。ROW 格式虽然日志量大,但回放结果确定。STATEMENT 格式下,存储过程、随机函数、带有不确定条件的事务在主从回放时可能产生完全不同的结果,校验工具跑出来的差异会让人怀疑人生。为了省点磁盘空间,引出一堆一致性事故,非常不划算。
第二条,不要在主库执行超大事务的同时做数据校验或者备份。大事务本身会拉高延迟,校验工具的读取和比对会额外消耗从库资源,两个动作叠加,延迟可能分分钟爬升到分钟级。我一般建议,大事务安排在业务低峰执行,校验任务和 DDL 错开,且单独观察延迟走势。
第三条,不要在没有追平复制的情况下对从库开放可写。故障切换时,如果应用接入了新的主库,而这个新主库的 relay log 还没完全回放,那任何写入都会混进"旧事务 + 新事务"的序列里,极难清理。所以提升从库前必须确认 Executed_Gtid_Set 已经追平至不能变化,最好用脚本固定校验而不是肉眼判断。
第四条,半同步不是万能的,防丢日志但不防丢提交。超时降级、从库回放落后、多节点网络分区,这些场景下半同步都不保证任何事。业务侧需要配套限流、降级和重试机制,而不是把宝全押在数据库上。
5. 一点个人体会与继续深挖的方向
做了这么多 MySQL 集群的一致性问题排查,我最大的体会是:不要一开始就追求绝对强一致,先量化业务真正能容忍的数据窗口。大部分读写分离业务,秒级别延迟可接受,那么"异步复制 + 半同步 + GTID 校验"这套组合已经足够;如果业务要求用户写后立即读到自己写的内容,那就在应用层把关键请求强制路由到主库,而不是指望从库实时追上;如果涉及跨机房容灾,才需要认真考虑 MGR 甚至异地多活架构。
这套东西后续可以扩展的方向还有不少,比如对 MGR 做一次分布式故障演练、写一套自动化的主从数据校验流水线、或者把半同步降级事件接入告警平台做实时感知。最后再给一个实用的小建议:找两台低配实例,每个月定期做一次主从切换演练,把切换脚本和数据校验流程跑熟,真出故障时,团队能少踩一半的坑。