双主切换时主键冲突须从源头拦截:强制旧主只读、回收应用用户写权限、中间件关闭旧writeHost;因分布式锁无法阻断binlog重放,需停SQL线程并确认relay log清空后才完成切换。

主从切换时双写导致的主键冲突怎么拦
双写不是“会不会发生”的问题,而是“什么时候触发”的问题。只要两个节点都开启 INSERT 权限且未做隔离,auto_increment 就大概率撞车。最直接的拦截方式是在数据库层堵住源头:
- 切换前强制停掉旧主库的写入:执行
SET GLOBAL read_only = ON;(注意:需排除 super 权限用户,否则仍可写) - 检查所有应用连接池配置,确认没有硬编码指向旧主库 IP 或 DNS 缓存未刷新
- 在中间件(如 MyCat、ShardingSphere)中关闭旧
writeHost,并设置switchType=1(强一致性切换) - 不依赖
read_only单一手段——它对 SUPER 用户无效,必须配合权限回收:REVOKE INSERT, UPDATE, DELETE ON *.* FROM 'app_user'@'%';
为什么用分布式锁也拦不住 binlog 重放阶段的覆盖
分布式锁只管“写请求入口”,但拦不住已落盘的 binlog 在切换后被从库重放。典型场景是:旧主挂了,新主刚切上,旧主残留的 binlog 还在 relay log 里,SQL 线程继续执行,结果把新主刚写的行又删/改了一遍。
- 锁的作用域仅限于应用发起的新请求,对 MySQL 内部的复制线程完全无效
- 即使你在业务层加了 Redis 锁,
UPDATE t SET v='x' WHERE id=1在旧主上执行成功后,这条语句仍会通过 binlog 同步到新主,并再次执行 - 真正要干预的是复制流本身:可在新主启用
replicate_ignore_db或临时停掉 SQL 线程:STOP SLAVE SQL_THREAD;,等确认无残留 relay log 再启 - 更稳妥的做法是切换后立刻查
SHOW SLAVE STATUS\G中的Relay_Log_File和Relay_Log_Pos,确认已无 pending 事件
状态机设计必须覆盖的三个临界态
主从切换不是原子操作,中间必然存在“半切换”窗口。状态机若只定义 PRIMARY/STANDBY 两态,一定会漏掉关键过渡行为。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
-
SWITCHING_TO_PRIMARY:此时新主已接受写入,但旧主的 binlog 还未清空,必须拒绝所有跨分片事务(避免 XA 分布式事务卡在 prepare 阶段) -
WAITING_FOR_REPLAY_CLEAR:新主已启,但 SQL 线程仍在重放 relay log,此时只允许幂等写(如INSERT ... ON DUPLICATE KEY UPDATE),禁止DELETE和非幂等UPDATE -
DEGRADED_PRIMARY:检测到Seconds_Behind_Master > 30且持续 2 次心跳失败,自动降级为只读,防止脏读扩散;该态需主动上报监控,不能静默
binlog_format = ROW 下的 where 条件失效陷阱
很多人以为 ROW 格式能靠 before image 避免误覆盖,实际在双写场景下非常脆弱。MySQL 不校验 where 条件是否匹配,只按主键或唯一索引定位行。
- 旧主执行
UPDATE t SET name='v1' WHERE id=1 AND name='v0'→ binlog 记录完整 before/after image - 新主上同一行已被改成
name='v2',但 binlog 重放时仍会把name改成'v1',因为它是按id=1定位的,不校验name='v0' - 验证方法:解析 relay log:
mysqlbinlog --base64-output=decode-rows -v mysql-relay-bin.000001 | grep -A5 "### UPDATE",看是否含@2='v0'(before)和@2='v1'(after) - 规避方式只有两个:要么全量使用
INSERT ... ON DUPLICATE KEY UPDATE,要么在应用层加版本号字段 +WHERE version = ?,让冲突暴露在应用层
真实环境里最容易被忽略的,是 relay log 清理时机与状态机跃迁的错位——状态机说“已切换完成”,但 SQL 线程还在 replay 上一分钟的事件。这时候任何“最终一致”的假设都会崩塌。

















