100w大表0停机回滚:我们为什么放弃Undo Log,选择表名切换?

0 阅读5分钟

一、背景:一次“改不好就影响登录”的同步任务

最近接到一个线下数据同步需求,需要同步三類核心数据:

  • 组织数据
  • 人员数据
  • 门店数据

同步逻辑不是简单的“新增”,而是会对历史数据进行修改。更关键的是,这七张表直接关联用户登录权限——一旦数据被改错,可能导致大批人员无法正常登录。

这种“改了就不能错、错了就得秒级恢复”的场景,让我们在上线前必须回答一个问题:

如果同步逻辑有问题,数据乱了,怎么快速、安全地回滚?

二、选型:四个方案,我们为什么选第四个

涉及七张表,最大一张表数据量 100w+。跟团队和 DBA 讨论后,一共评估了四个方案。

方案一:集群快照

做法:同步前对整个 MySQL 集群打一个快照,出问题时整体回滚到快照点。

问题:集群规模很大,而本次只改动七张表。为七张表把整个集群回滚,代价太大,而且会影响其他正常业务。

结论:❌ 不适用

方案二:导出备份 + 重新导入

做法:同步前用 mysqldumpSELECT INTO OUTFILE 导出七张表数据,回滚时清空原表再重新导入。

问题:100w 数据的导入导出不是瞬间能完成的。回滚窗口越长,业务不可用风险越大。万一晚上出问题,等数据导完,业务已经停了十几分钟。

结论:❌ 不适用

方案三:数据恢复工具(基于 Undo Log)

做法:该工具能够生成被修改的表在一段时间执行的sql的反向执行sql,其实就是Undo Log

Undo Log反向操作(逻辑日志)UPDATE 记录:如果正向是 name='B',反向就是 name='A'

看起来很美,但实际踩到坑了

  • 这次同步会涉及几十万行数据变更,意味着要生成几十万条反向 SQL
  • 现有工具是通过浏览器展示和分发这些 SQL 的
  • 几十万条 SQL 在浏览器端直接卡死,连复制都困难,更别说执行

工具本身设计不是为这种“大数据量反向回滚”场景准备的,在这个量级下基本不可用。

结论:❌ 不适用

方案四:备份表 + 表名切换(最终选择)

核心思路:不改原表,先建备份表;回滚时不修数据,只换表名

具体操作分两步走:

第一步:同步前——全量备份

-- 1. 创建备份表(结构与原表完全一致)
CREATE TABLE tb_store_backup LIKE tb_store;
CREATE TABLE tb_oppo_store_backup LIKE tb_oppo_store;
CREATE TABLE tb_oppo_sale_user_backup LIKE tb_oppo_sale_user;
CREATE TABLE tb_oppo_store_sale_user_backup LIKE tb_oppo_store_sale_user;
CREATE TABLE tb_store_sale_user_backup LIKE tb_store_sale_user;
CREATE TABLE tb_store_sale_user_multi_backup LIKE tb_store_sale_user_multi;
CREATE TABLE tb_offline_user_backup LIKE tb_offline_user;

-- 2. 备份数据(以门店表为例)
INSERT INTO tb_store_backup SELECT * FROM tb_store;
INSERT INTO tb_oppo_store_backup SELECT * FROM tb_oppo_store;
INSERT INTO tb_oppo_sale_user_backup SELECT * FROM tb_oppo_sale_user;
INSERT INTO tb_oppo_store_sale_user_backup SELECT * FROM tb_oppo_store_sale_user;
INSERT INTO tb_store_sale_user_backup SELECT * FROM tb_store_sale_user;
INSERT INTO tb_store_sale_user_multi_backup SELECT * FROM tb_store_sale_user_multi;
INSERT INTO tb_offline_user_backup SELECT * FROM tb_offline_user;

第二步:需要回滚时——表名切换

核心操作就是一条 RENAME TABLE,这是纯元数据操作,毫秒级完成:

RENAME TABLE 
    tb_store TO tb_store_dirty,
    tb_store_backup TO tb_store;

如果要一次处理多张表,MySQL 的 RENAME TABLE 也支持原子性操作:

RENAME TABLE 
    tb_store TO tb_store_dirty,
    tb_store_backup TO tb_store,
    tb_oppo_store TO tb_oppo_store_dirty,
    tb_oppo_store_backup TO tb_oppo_store;
    -- 其他表依此类推

image.png 为什么这个方案最适合我们?

维度说明
回滚速度表名切换是元数据操作,毫秒级完成,业务几乎无感知
数据安全性原表被重命名为 _dirty,数据不删除,留作现场保留
操作确定性不依赖 binlog 解析、不生成反向 SQL,没有“工具失效”的风险
大表友好100w 数据量下,备份 INSERT 可以接受;回滚时完全不关心数据量大小

当然也有缺点:

  • 需要手动维护备份表和原表的对应关系

  • 回滚脚本需要提前准备好,不能临时写

  • 备份期间磁盘空间会翻倍

三、最终结果

本次上线后数据同步正常,没有触发回滚。但备份方案是完整的,真要回滚也能做到秒级切换。

四、一点总结

从这次选型中,我们得到几个比较实在的经验:

  1. 工具好用,但不一定适合你的量级 Undo Log / 反向 SQL 的思路很优雅,但几十万条 SQL 通过浏览器执行,在生产环境就是不可用的。不要在生产环境用工具做它没承诺过能做的事情。
  2. 大表回滚,“修数据”不如“换门牌” 100w 的表,不管是重新导入还是逐条回滚,都是跟数据量成正比的成本。而表名切换的成本是 O(1),与数据量无关。
  3. 备份方案要和回滚窗口匹配 问自己一个问题:如果凌晨三点出问题,我能几分钟内恢复?导出导入做不到,集群快照太重,反向 SQL 太慢,而表名切换可以。
  4. 选型不是选“最好的技术”,而是选“最合适当前约束的” 我们不是不知道有更优雅的方案,而是在数据量、时间窗口、工具能力这几个约束条件下,方案4是唯一能打满分的。