主从切换时事务丢失或重复的根源是复制状态未对齐、配置残留及人为干预;Seconds_Behind_Master=0不保证relay log执行完毕,需对比Exec_Master_Log_Pos与Read_Master_Log_Pos,GTID模式下须Retrieved_Gtid_Set与Executed_Gtid_Set完全一致。

主从切换时事务丢失或重复,根本不是“切换动作”本身导致的,而是复制链路状态没对齐、配置残留或人为干预引发的连锁反应。
为什么 Seconds_Behind_Master = 0 还会丢数据
这个值只反映 SQL 线程追上了 IO 线程读到的位置,并不保证所有 relay log 已执行完毕。尤其在启用并行复制(slave_parallel_workers > 0)时,SQL 线程可能已报告“追平”,但部分 worker 线程还在处理未提交的事务块。
- 必须配合
SHOW SLAVE STATUS\G中的Exec_Master_Log_Pos和Read_Master_Log_Pos对比:两者相等才代表 relay log 全部消费完成 - GTID 模式下更严格:要检查
Retrieved_Gtid_Set和Executed_Gtid_Set完全一致,且Retrieved_Gtid_Set不包含已PURGE的 GTID - 如果启用了
SQL_Delay,Seconds_Behind_Master可能为 0,但实际事务还在延迟队列里,切前必须确认SQL_Delay = 0
read_only 和 super_read_only 残留导致写入失败,间接引发重试重复
新主库若仍处于只读状态,应用写请求会直接报错 ERROR 1290 (HY000): The MySQL server is running with the --read-only option...。此时若客户端有自动重试逻辑(如连接池、ORM 的 retry 机制),就可能在 read_only 关闭后重复执行同一事务。
- 切换后第一件事不是改
server_id,而是查SELECT @@read_only, @@super_read_only;—— 两个都得是0 -
super_read_only = ON会锁死read_only的修改,必须先SET GLOBAL super_read_only = OFF;才能关read_only - 配置文件中残留
read_only=1是隐患,动态关闭只是临时补救;业务稳定后必须删掉或注释该行并重启
旧主库长事务未清理,新主库回放时触发唯一键冲突或主键重复
切换瞬间,旧主库上未提交的事务仍持有 MDL 锁或已写 binlog 但未刷盘。若这些事务含 INSERT/UPDATE,而新主库已接受新写入,后续回放 relay log 就可能撞上唯一索引或主键约束,报 Duplicate entry,SQL 线程中断。
- 切换前用
SELECT * FROM information_schema.INNODB_TRX WHERE TRX_STATE = 'RUNNING' ORDER BY TRX_STARTED;找出运行超 30 秒的事务 - 重点关注
TRX_MYSQL_THREAD_ID,避开io_thread、sql_thread等系统线程 ID 后再KILL - 避免用
SET GLOBAL sql_slave_skip_counter = 1(MySQL 8.0.23+ 已弃用),它跳过的是 event 级别,可能破坏事务原子性
客户端 DNS 缓存或连接池未刷新,造成“双写”或“写错库”
应用连的是 VIP 或域名,但切换后 DNS 缓存未过期(TTL 未生效)、连接池未主动断开重建,部分请求仍打到旧主库。此时旧主库已降为从库、read_only=ON,写失败;而新主库又收到了另一份相同请求 —— 表面看是重复,实则是路由错乱。
- 不要依赖 DNS TTL 值自动刷新,切换后应强制应用 reload 连接池(如 Spring Boot 的
/actuator/refresh) - VIP 漂移方案中,务必在漂移脚本末尾加入
sleep 2并验证nc -z old_master_ip 3306是否已断连 - 最稳妥的做法是在应用层加写库标识(如
/* WRITER */ INSERT ...),配合中间件识别路由,而非完全信任 DNS/VIP
真正危险的不是切换那几十秒,而是切换前后三分钟内没人盯住 SHOW PROCESSLIST、SHOW SLAVE STATUS\G 和应用错误日志。任何“自动切换成功”的提示,都不如一行 SELECT @@global.gtid_executed; 来得实在。


















