系统架构 · 数据复制与持久化

一台挂了数据不能升天: 复制的代价是副本不一致, 半同步是折中不是答案

数据复制与持久化 · 主从数据流 × 三种 ACK 时机 × 丢写事故 vs GTID 破局 ACK 的位置 = 丢不丢数据的分界线 · WAL 先日志后数据是地基 ① 数据流: binlog/WAL → 副本回放 MySQL 拆解: dump 线程 → IO 线程 → relay log → SQL 线程重放 写 binlog stream 网络抖 → 落后 3s Client commit 请求 Primary 主库 binlog (row): 变更事件流 WAL/redo: 先日志后数据 顺序追加 · 崩溃可重放 Replica-1 IO 线程: 拉 binlog relay log 落盘 SQL 线程按序重放 Replica-2 · 慢 3s IO 线程: 拉 binlog relay log 落盘 SQL 线程重放 (延迟 3s) 副本只认日志序 — relay log 按主库提交顺序重放, 复制延迟 = 副本追日志的进度差 GTID = server_uuid:n 全局唯一事务号: 副本上报执行位点, 主库知道它还缺哪段日志 ② 三种复制的 ACK 时机 — ★ = ACK 位置, 越靠右越安全也越慢 ★ 之后主库挂 → 那次写就可能丢 立即 ACK Client Primary Replica-1 Replica-2 异步: 主库落盘即 ACK — 最快; 未送达副本的写切主时蒸发 (ACKed write lost) ≥1 副本收到才 ACK Client Primary Replica-1 Replica-2 半同步: ≥1 副本收到 relay log 才 ACK — 多 1 个 RTT; timeout 到点自动降级回异步 全副本落盘才 ACK Client Primary Replica-1 Replica-2 同步: 全副本落盘才 ACK — 最慢副本决定延迟; 生产几乎不用纯同步, 用它的折中版 ③ 事故路 · 异步复制 + 直接切主 → ACKed write lost t1 下单 Primary 回 ACK OK binlog 尚未送达副本 t2 主库宕机 日志没到副本 这一单不在任何副本上 客户端却已收到成功 切主 Replica-1 提新主 历史天然缺一单 拓扑里没人知道缺 SELECT * FROM orders WHERE id=88; → Empty set (0.00 sec) 客户端 3 秒前刚收到"下单成功" — 数据已升天. 这不是备份问题, 是 ACK 时机问题 ④ 破局路 · GTID 比对补齐再切换 -- 提升前, 先等候选主把旧主 GTID 全部追平 (最多等 10s) SELECT WAIT_FOR_EXECUTED_GTID_SET( '3E11FA47-71CA-11E1-9E33-C80AA9429562:1-41207', 10); -- → Query OK 才准切; 未追平就提升 = 亲手制造数据丢失 -- 超时未追平则返回错误: 排查网络/单线程回放, 别硬切 追平 → 提主 → 校验 0 行丢失, 客户端无感 切换 checklist: 旧主 super_read_only → GTID 对账 → 提主 → 漂 VIP ● 事故链: 半同步超时静默降级 → 退回异步无人知 → 主库宕机直接切主 → 已 ACK 的订单在新主上 Empty set → 客诉刷屏 ● 破局链: 半同步 + 降级告警 · 切主前 WAIT_FOR_EXECUTED_GTID_SET · Redis min-replicas-to-write · Kafka acks=all + min.insync.replicas=2 · 延迟从库兜底误删 读法: ★ = ACK 时机 · rose = 丢写路径 · emerald = GTID 补齐 · WAL 先日志后数据是这一切的地基

机制视角 — 日志搬运

  • • 复制 = 日志搬运: binlog/WAL → relay log → 副本按序重放
  • • 三种复制的差异全在 ACK 时机: 同步 / 半同步 / 异步
  • • WAL 先日志后数据: 崩溃恢复与复制的共同地基
  • • 快照管"到某刻", 日志管"到任意点" (PITR)

行为视角 — 不一致的代价

  • • 有复制就有延迟, 有延迟就有旧读: 这是账单不是 bug
  • • 半同步只保证"收到", 不保证"已回放"; 静默降级是最大暗雷
  • • Leaderless 用 w+r>n 换无主并行写, 冲突自己解决
  • • Kafka: ISR 收缩 + min.insync.replicas 决定丢不丢

生产价值 — 先定 RPO

  • • RPO/RTO 写下来, 复制与备份方案都是推导不是拍脑袋
  • • 能恢复的才叫备份: 每月演练记录真实 RTO
  • • 延迟从库是误删的时间机器, 便宜又救命
  • • GTID 让切主从"凭感觉"变成"对完账再切"

💡 一句话理解

复制就像抄课堂笔记: 老师讲课(主库写日志), 同桌照抄(副本重放), 你交作业时老师讲没讲到那页(ACK 时机)决定了你俩笔记差多少。机制本质是用日志搬运换取数据冗余——代价是副本永远慢半拍, 所以"一台挂了切过去"之前必须回答: 新副本上有多少客户端已经确认过的写?

同步、异步、半同步不是三种技术, 是同一个问题的三个回答位置: 在主库落盘时答应、在副本收到时答应、还是在全部落盘时答应。答得越晚越安全、越慢。而 WAL(先写日志再改数据)是这一切的地基: 崩溃恢复靠它重放, 复制靠它搬运, 持久性靠它 fsync。

🧠 必知必会 必考 & 必会

主从复制 Primary-Replica
一主多从: 写走主、读走从; 主把变更写成日志, 副本拉日志重放。代价是副本滞后: 从库读到的永远是"过去的"主库。
-- MySQL 8.0.22+: 副本看自己追到哪了 (旧名 SHOW SLAVE STATUS)
SHOW REPLICA STATUS\G
-- 关键: Seconds_Behind_Source 是估算值, 靠它告警会漏报大延迟
-- → Replica_IO_Running / Replica_SQL_Running 双 Yes 才算活着
同步 / 异步 / 半同步 Sync · Async · Semi-sync
区别只在ACK 时机: 同步=全副本落盘(慢, 最安全)、异步=主落盘(快, 切主可能丢)、半同步=至少 K 个副本收到日志(折中)。注意半同步只保证"收到", 不保证"已回放"。
-- MySQL 半同步: 至少 1 副本收到 binlog 才向客户端返回 OK
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_master_timeout = 1000;  -- ms, 超时降级
-- 关键: 降级是"静默"的 — 不配告警, 半同步就名存实亡
多主 Multi-Primary
多个主互为副本都可写: 写容量翻倍, 但同一行两边同时改要靠冲突解决(LWW 等)。MySQL 双主 + 自增步长错开只是错开主键, 不是冲突解决。
-- 双主防主键冲突: 一边奇数一边偶数, 步长 2
SET @@GLOBAL.auto_increment_offset = 1;      -- A 库: 1,3,5...
SET @@GLOBAL.auto_increment_increment = 2;   -- B 库 offset=2
-- 关键: 错开自增只防主键撞, 防不了同行内容冲突 — 双主仍要业务避写
无主复制 Leaderless
Dynamo 系没有主: 客户端(或协调者)写 N 副本中任意 w 个、读 r 个, w+r>n 保证读写集合有交集; 反熵进程后台补齐差距。
n, w, r = 3, 2, 2   # Cassandra: QUORUM 级别读写
# 关键: w + r > n → 至少一个副本同时参与本次读写, 拿得到新值
# → 写 2/3 成功即返回; 读 2/3 版本冲突时按时间戳取最新
日志复制 Log Replication
三种格式: 语句复制(体积小, 但 NOW()/UUID() 不可重现)、行复制(默认, 精确但大事务日志爆炸)、混合。选错格式, 副本会"重放出另一个世界"。
-- MySQL 默认 row: 记录行变更而非 SQL 文本
SELECT @@binlog_format;   -- → ROW
-- 关键: UPDATE ... SET t=NOW() 在语句格式下副本重放结果不同
-- → 一切"不确定函数"都是语句复制的天敌
WAL 预写日志 Write-Ahead Log
改数据前先把变更顺序追加写进日志并落盘: 崩溃后重放 WAL 恢复到一致点。它同时是持久性的地基和复制的载体。
# PostgreSQL: 提交是否等 WAL 刷盘
synchronous_commit = on      # 默认: 本地刷盘才返回
# synchronous_commit = off    # 只丢最近约 3×wal_writer_delay 的提交, 不损坏
# 关键: off 是"可能丢最近几笔"但绝不损坏 — 可接受的削延迟手段
快照 Snapshot
某一时刻的完整数据视图: 恢复快但只到快照点; 配合日志(binlog/WAL)才能到任意时间点 (PITR)。快照必须一致性(同一时刻), 否则恢复出四不像。
# mysqldump: InnoDB 单事务一致性快照, 不锁全表
mysqldump --single-transaction --all-databases > full.sql
# 关键: --single-transaction 靠 MVCC 快照, 期间其他会话照常写
# → 恢复点 = 备份开始时刻, 再补 binlog 才能到任意点
检查点 Checkpoint
WAL 不能无限重放: 定期把内存脏页落盘、推进"已持久化位点", 之前的日志才能删。检查点太疏: 崩溃重放慢; 太密: 刷盘抢业务 IO。
-- MySQL: redo 总容量决定检查点节奏 (8.0.30+)
SHOW VARIABLES LIKE 'innodb_redo_log_capacity';
-- 关键: 检查点是"WAL 能删到哪"的进度条 — 崩溃恢复只重放检查点之后的日志
日志压缩 Log Compaction
Kafka compact: 同 key 只保留最新值, 老日志可清 — 让新消费者从任意 offset 重建全量状态(如 __consumer_offsets)。区别于按时间删除(delete)。
# Kafka 主题配置: key 只留最新值
cleanup.policy=compact
min.cleanable.dirty.ratio=0.5   # 脏区过半才触发, 别太勤
# 关键: compact 不是 delete — 同 key 旧值被清, key 本身永在
备份与恢复 Backup / Restore
备份的唯一定义是"能恢复的才算备份": 3-2-1(3 份、2 种介质、1 份异地) + 定期恢复演练。没演练过的备份 = 心理安慰。
# 每月把最近备份拉到隔离环境真恢复一次并跑校验 SQL
mysql -h restore-test < full.sql && mysqlbinlog --verify-binlog-checksum binlog.000123
# 关键: 演练记录恢复耗时 — 这就是 RTO 的真实数字, 不是拍脑袋
持久性 Durability
事务提交后数据"真的在盘上", 由 fsync 时机决定。MySQL 双 1(innodb_flush_log_at_trx_commit=1 + sync_binlog=1)是全持久; 改 0/2 换性能, 断电丢最近事务。
-- 双 1: 每次 commit 都 fsync redo + binlog — 全持久, 最慢
innodb_flush_log_at_trx_commit = 1
sync_binlog = 1
-- 关键: 改 2/0 提速明显, 但 OS 崩溃丢最近 1 秒事务 — 先问 RPO 再动
RPO / RTO
RPO=最多丢多久数据, RTO=多久恢复服务。两者定下来, 复制模式、备份频率、切换方式都是推导: RPO=0 → 半同步起步; RPO=5min → 异步+延迟从库即可。
# 一行写清 SLA, 贴在值班群
# RPO=0 (半同步) / RTO=3min (自动切主) / 延迟从库 1h (防误删兜底)
# 关键: RPO 没定义就上异步复制 — "丢了 2 分钟数据"就是扯皮开始
复制延迟 Replication Lag
副本落后主库的秒数/事务数。症状五花八门: 改完头像看旧图、读己之写失效。对策: 写后短窗读主、按 GTID 等追平、落后副本摘出读池。
-- ProxySQL: 副本落后超阈值自动摘出读池
UPDATE mysql_servers SET max_replication_lag=10 WHERE hostgroup_id=20;
LOAD MYSQL SERVERS TO RUNTIME;
-- 关键: 延迟超标的从库继续接读, 就是批量制造"改完没生效"工单

🏭 生产实战 real world

场景 1 · 半同步静默降级三个月, 切主丢 47 秒订单 (事故排查)

机房网卡抖动 → 半同步等 ACK 超时自动降级异步, 无告警; 两周后主库宕机切主, 客户端已确认的写蒸发。监控补课:

# my.cnf: 半同步开启 + 超时 1s
plugin-load = "rpl_semi_sync_master=semisync_master.so"
rpl_semi_sync_master_enabled = 1
rpl_semi_sync_master_timeout = 1000
# 监控必须盯的两个状态: 降级即告警
SHOW GLOBAL STATUS LIKE 'Rpl_semi_sync_master_status';  -- OFF = 已降级!
SHOW GLOBAL STATUS LIKE 'Rpl_semi_sync_master_no_tx';   -- 降级期异步提交笔数
-- 关键: no_tx 增长就报警 — 这段时间 ACKed 的写切主时可能蒸发

加告警后当月抓到 3 次降级, 每次都在 2 分钟内恢复半同步, 再没出现过切主丢写。

场景 2 · Redis 读写分离, 主挂瞬间独写 (min-replicas 底线)

主库与副本断链后仍在接受写, 切换回去就冲突。redis.conf 两条底线参数把"孤立主库"关进笼子:

# redis.conf: 主从 + 数据安全底线
replicaof 10.0.0.1 6379
min-replicas-to-write 1     # 健康副本 <1 时, 主库拒绝写
min-replicas-max-lag 10     # 副本延迟 >10s 不算健康
replica-read-only yes
# 关键: 没有 min-replicas-to-write, 主库失联副本后还在"独写"
# — 分区恢复后两份"主"数据对不齐, 只能人工比对

场景 3 · Kafka 支付消息单副本, broker 重启丢 2 万笔 (正确用法模板)

topic 建成单副本 + acks=1, broker 重启丢日志段。不丢消息的最低配三件套, 一次配齐:

# producer 侧: ISR 全部确认才算成功
acks=all
enable.idempotence=true          # 幂等生产者, 重试不重复
retries=2147483647
delivery.timeout.ms=120000
# broker/主题侧: 3 副本, 至少 2 个在 ISR 才可写
replication.factor=3
min.insync.replicas=2
# 关键: acks=all + rf=3 + min.insync.replicas=2 — 少一个都可能丢
# ISR 不足 2 时生产者报 NotEnoughReplicasException, 这是保护不是故障

场景 4 · 断电后库起不来? WAL 恢复演练日每年省一次大事故

把"崩溃恢复"变成年度演练: 在 staging 真停电一次, 量出真实 RTO 与日志重放行为:

# PostgreSQL 崩溃恢复演练: 模拟断电 (不走正常 shutdown)
pg_ctl -D /data/pg stop -m immediate
pg_ctl -D /data/pg start
# 观察日志 — 这两行就是 WAL 在救你:
# LOG: database system was interrupted; last known up at ...
# LOG: database system was not properly shut down; automatic recovery in progress
# 关键: 演练记录恢复耗时 = 真实 RTO; 双 1 配置与省 IO 配置分别测一遍

场景 5 · 3-2-1 备份用脚本管起来: 份数 / 介质 / 异地 (部署型)

单机单介质备份在机房级事故面前等于零。每日全量 + 异地对象存储, 账号最小权限:

# 每日: xtrabackup 全量到本地 → rclone 到对象存储 (另一介质+异地)
xtrabackup --backup --target-dir=/backup/$(date +%F) --parallel 4
rclone copy /backup/$(date +%F) oss:db-backup/$(date +%F) --transfers 8
# 保留策略: 本地 7 天 / 异地 35 天; 每月 staging 真恢复一次
# 备份账号只给最小权限 — 备份文件能拖库, 账号也能拖库
# 关键: 3 份、2 种介质、1 份异地 — 任何一条缺失都不叫 3-2-1

场景 6 · DBA 误删表, 1 小时前的延迟从库救了全公司

误删后 binlog 已 purge, 常规恢复只能回天到昨晚备份; 专用延迟从库把 RTO 从"彻夜奋战"变 10 分钟:

-- 专用延迟从库: 故意落后 3600s, 专治误删/误 UPDATE
CHANGE REPLICATION SOURCE TO SOURCE_DELAY = 3600;  -- 8.0.22+ (旧名 MASTER_DELAY)
-- 事故时: 停复制保住现场 → 从延迟库回灌误删的表 → 恢复制
STOP REPLICA;
-- 关键: 延迟从库不接任何读流量, 只当"时间机器"
-- → 1h 内的误删, 比翻 binlog 逐事务前滚快一个数量级

场景 7 · 快照 + binlog 串成 PITR, 精确恢复到误删前 1 秒 (事故排查)

全量快照停在昨晚 23:00, 误删发生在今天 09:47 — 中间全靠 binlog 前滚, 停在事故点之前:

# 1) 恢复昨晚全量
mysql -h restore < full_20260925.sql
# 2) binlog 前滚, --stop-position 卡在 DROP 语句之前的 pos
mysqlbinlog --start-datetime="2026-09-26 09:00:00" \
  --stop-position=104857600 binlog.000451 | mysql -h restore
# 关键: pos 从 SHOW BINLOG EVENTS 里人工核对 DROP 前的位置
# 前提: 全量之后的所有 binlog 都在 — 没归档 binlog 就没有 PITR

场景 8 · 北京写上海读, 跨机房异步复制的取舍清单 (架构配置)

机房间 RTT 25ms, 半同步跨机房等 ACK 会拖死写性能; 三条底线把延迟和风险都锁住:

# 1) 半同步只在本机房内做: 跨机房等 ACK 会被 RTT 拖死写性能
rpl_semi_sync_master_wait_point = AFTER_SYNC   # 5.7+: 先落日志再等副本
# 2) 机房间 binlog 压缩传输 (8.0.20+)
binlog_transaction_compression = ON
#    旧版本用 slave_compressed_protocol=1 (8.0.23 起已废弃)
# 3) 跨机房 lag 单独告警: 本机房秒级, 机房间分钟级是常态, 阈值分开
# 关键: 机房间链路永远按"随时会断"设计 — 上海读服务要能独立降级

场景 9 · 延迟告警恒为 0, 用户却在骂"改完没生效" (监控型)

Seconds_Behind_Source 空闲时恒 0、大事务重放时不准, 告警形同虚设。用 pt-heartbeat 原理: 让数据自己报告延迟:

-- 主库每秒: 心跳表写入一个时间戳 (随 binlog 复制下去)
UPDATE heartbeat SET ts = NOW(6) WHERE id = 1;
-- 副本侧: 用"数据里的时间"与本地时钟做差 = 真实端到端延迟
SELECT TIMESTAMPDIFF(MICROSECOND, ts, NOW(6)) / 1000000.0 AS true_lag_sec;
-- 关键: 副本自报的秒数不可信 — 空闲时恒 0, 不代表它追得上突发写

场景 10 · __consumer_offsets 无限膨胀, log compaction 保住重建能力 (配置型)

offset 提交日志只增不减, 磁盘月涨 800GB; compact 让同 key 只留最新值, 且新消费者能重放出完整状态:

# Kafka 内部主题 __consumer_offsets: 必须紧凑, 否则提交日志无限涨
cleanup.policy=compact
segment.ms=86400000              # 段 1 天滚动, compact 以段为单位
min.cleanable.dirty.ratio=0.5    # 脏区过半才压, 默认值够用
delete.retention.ms=86400000     # tombstone 保留 1 天, 别设太短
# 关键: compact 让新消费者组重放全量 key 状态 — tombstone 被过早清掉, 状态就断代

⚠️ 编码注意与常见坑 pitfalls

坑 1 · 异步复制直接当高可用切主 — 症状: 主库宕机切主后, 客户端已 ACK 的订单消失. 原因: 最新日志没到副本, 新主没有这批写. 正解: 半同步 + 切主前 GTID 比对。
-- 错: 默认异步 → 挂前 2s 的 ACKed 写不在副本上, 切主即丢
-- 对: rpl_semi_sync_master_enabled=1 + 切前 WAIT_FOR_EXECUTED_GTID_SET
坑 2 · 半同步超时降级回异步无告警 — 症状: 半同步名存实亡三个月才发现. 原因: timeout 到点自动降级, 不影响运行无人感知. 正解: 监控 Rpl_semi_sync_master_status + no_tx 告警。
-- 错: 只看 Replica_IO/SQL_Running 双 Yes → 降级看不出来
-- 对: Rpl_semi_sync_master_status=OFF 或 no_tx 增长 → 电话告警
坑 3 · 备份从未做过恢复演练 — 症状: 真出事才发现备份损坏/缺表, RTO 无穷大. 原因: "备份任务 SUCCESS"≠"能恢复". 正解: 每月隔离环境真恢复 + 校验。
# 错: 备份绿灯 300 天 → 出事当天解压报 corrupt block
# 对: 每月 restore + checksum 校验 + 记录真实耗时作为 RTO
坑 4 · Kafka 单副本或 min.insync.replicas=1 — 症状: broker 重启丢一段消息. 原因: rf=1 无冗余; rf=3 但 min.insync=1 时 acks=all 退化成单副本语义. 正解: rf=3 + min.insync=2 + acks=all。
# 错: min.insync.replicas=1 → ISR 缩到 1 也放行, acks=all 形同虚设
# 对: replication.factor=3 + min.insync.replicas=2 + acks=all
坑 5 · WAL 与数据放同一块盘 — 症状: 批量导入时 WAL 盘写满, 库 PANIC 拒写. 原因: WAL 顺序写与数据随机写抢同一块盘的 IO 与容量. 正解: WAL 独立 NVMe 卷, 容量留 2 倍余量。
# 错: pg_wal 和 base 同盘 → checkpoint 风暴, WAL 满 PANIC 拒写
# 对: pg_wal 单独 NVMe 卷 + 容量 ≥ 2×max_wal_size
坑 6 · 层叠复制延迟逐级放大 — 症状: replica-of-replica 链上第三级落后 10 分钟. 原因: 延迟逐级叠加, 上游卡下游全卡. 正解: 副本直连主库 (星型), 扩读用多副本不用级联。
# 错: 主 → 从1 → 从2 → 从3, 每级 2s 延迟叠加
# 对: 从库全部直连主库; IO 压力用 binlog 传输压缩缓解
坑 7 · 主从 server-id 冲突 — 症状: 从库 IO 忽断忽连, 数据就是不更新. 原因: 两台 server_id 相同, 主库把它们当同一台反复踢. 正解: 全拓扑 server_id 唯一 + 启动校验。
-- 错: 两台从库 server_id=1 → 主库上互踢, 复制时断时续
-- 对: server_id 由 IP 后两段生成, CMDB 校验全局唯一
坑 8 · 大事务阻塞复制 — 症状: 一次 DELETE 800 万行, 从库延迟飙到 40 分钟. 原因: 主库 30s 并行跑完, 从库单线程重放要几十分钟. 正解: 大 DML 分批执行。
-- 错: DELETE FROM logs WHERE created_at < '2025-01-01';  -- 800 万行一锅端
-- 对: 循环 DELETE ... LIMIT 5000 直到影响行数为 0, 主从都从容
坑 9 · 快照过频 IO 抖动 — 症状: 每 15 分钟一次快照, 业务 P99 抖 3 倍. 原因: 快照/刷盘集中抢 IO. 正解: 全量日级 + binlog 增量分钟级, PITR 粒度不降。
# 错: 每小时 xtrabackup 全量 → 磁盘 util 常年 90%+
# 对: 每日 1 全量 + binlog 实时归档, 恢复粒度照样到秒
坑 10 · 误删后 binlog 已被 purge — 症状: 想精确恢复, 发现 binlog.000451 已被过期清理. 原因: 本地保留期短且无归档. 正解: binlog 实时归档到远端。
-- 错: binlog_expire_logs_seconds=259200 (3天), 误删第 5 天才发现
-- 对: mysqlbinlog --raw --stop-never 实时流到远端; 本地 3 天远端 35 天
坑 11 · 复制账号权限过大 — 症状: 备份机被拖, 攻击者用复制账号全库拉走. 原因: 复制账号给了 ALL PRIVILEGES. 正解: 只给 REPLICATION SLAVE, REPLICATION CLIENT。
-- 错: GRANT ALL PRIVILEGES ON *.* TO 'repl'@'%';
-- 对: GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'10.%';
坑 12 · 只备份数据不备份 binlog — 症状: 快照恢复成功, 但快照之后的订单永远找不回. 原因: PITR 需要连续 binlog, 只备了全量. 正解: 全量 + binlog 归档是一对, 缺一不可。
# 错: crontab 只有一行 xtrabackup, binlog 三天本地自清理
# 对: xtrabackup --backup + mysqlbinlog --raw --stop-never 流式归档远端
坑 13 · replica 只读没锁死, 被运维直写 — 症状: 主从数据"对不上", 切主后数据丢. 原因: read_only 挡不住有 SUPER 权限的账号. 正解: super_read_only=1 + 副本账号最小化。
-- 错: SET GLOBAL read_only=1;  -- DBA 账号有 SUPER, 照样写进去
-- 对: SET GLOBAL super_read_only=1;  -- 连 SUPER 一起挡住
坑 14 · 切换后双主互抄 — 症状: 旧主网络恢复后"复活", 与新主互为主从写穿. 原因: 旧主没被隔离降级, 复制拓扑自环. 正解: 切换即对旧主 STOP 复制 + super_read_only。
-- 错: 旧主网络恢复自动追上 → 双主互抄, 自增 ID 飞速内耗
-- 对: 切换脚本第一步: 旧主 STOP REPLICA + SET GLOBAL super_read_only=1
坑 15 · RPO 没定义就上异步 — 症状: 丢 2 分钟数据后业务与 DBA 甩锅. 原因: 选型时没人问"能丢多少". 正解: 先写清 RPO/RTO, 再倒推复制与备份。
# 错: 架构评审没人提 RPO → 默认异步, 出事才知道丢了 90s 订单
# 对: SLA 声明 RPO=0 → 半同步起步; RPO=5min → 异步+延迟从库
坑 16 · 压缩快照损坏无法校验 — 症状: zstd 备份解压到 87% 报 CRC 错, 且无校验清单. 原因: 存储/传输静默损坏, 压缩包没带 checksum. 正解: 备份后立即生成 sha256 清单同传, 恢复前先验。
# 错: backup.tar.zst 直接入库 → 出事时 Invalid checksum, 解不开
# 对: sha256sum backup.tar.zst > backup.sha 同传; 恢复前 sha256sum -c
坑 17 · 复制过滤导致库不全 — 症状: 按库过滤的从库被提升主, 上线才发现"半个库". 原因: replicate-do-db 让副本不是全量镜像. 正解: 热备副本全量复制, 过滤库单独建链路。
-- 错: replicate-do-db=report → 副本缺业务库, 提升即灾难
-- 对: 热备副本全量复制; 报表库单独链路 + 永不参与选主
坑 18 · 延迟从库被当只读库直连 — 症状: 用户投诉数据"倒退"了 1 小时. 原因: 延迟从库混进读池被路由到. 正解: 独立 hostgroup 不进负载均衡 + 出现业务查询即告警。
-- 错: 延迟 1h 的从库混进读池 → 用户读到 1 小时前的订单
-- 对: 独立 hostgroup + 监控: 延迟从库出现业务 QPS 立即告警
坑 19 · 快照期间表锁 — 症状: 备份窗口 20 分钟, 订单写入全堵. 原因: 未加 --single-transaction, FLUSH TABLES WITH READ LOCK 锁全库. 正解: InnoDB + 单事务导出; 锁窗口放低峰。
# 错: mysqldump 裸跑 → FLUSH TABLES WITH READ LOCK 全库只读
# 对: mysqldump --single-transaction --set-gtid-purged=OFF (InnoDB)
坑 20 · 备份窗口撞业务高峰 — 症状: 每晚 8 点备份, 8 点档大促 P99 飙升. 原因: 全量备份 IO/CPU 与高峰重叠. 正解: 错峰 + 限速 (ionice/throttle) + 在专用延迟从库上备份。
# 错: 0 20 * * * xtrabackup → 晚高峰抢 IO
# 对: 02:30 窗口 + ionice -c3 + xtrabackup --throttle=100