数据库迁移与冷热分表方案

停机迁移 / 双写迁移 / 千万级单表的冷热归档实践

Posted by BY on April 25, 2026

原始笔记开头是一段类似目录的纯文本(”停机迁移方案 / 双写迁移方案 / 其他 / 一、现状…“),紧接着才是正文,结构很乱。这里把它整理成”两类迁移方案”+”千万级表冷热分库实践”两大块,分节标号;MyBatis 片段、命令片段保持原样。

当前保留内容

1. 停机迁移方案

凌晨挂公告维护、停写、跑导数工具、改连接配置、上线。

最简单的方案:网站或 app 挂个公告,”0 点到早上 6 点进行运维,无法访问”。

接着到 0 点停机,系统停掉,没有流量写入,老的单库单表数据库静止。然后跑事先写好的一次性导数工具,把单库单表的数据读出来,写到分库分表里面去。

导数完了之后,修改系统的数据库连接配置,包括代码和 SQL 也许有修改,那就用最新的代码直接启动连到新的分库分表上去。

验证一下,OK 了,凌晨 4 点打个滴滴回家。

这个方案比较 low,谁都能干,下面看看高大上一点的方案。

2. 双写迁移方案

不停机、增删改双写、跑导数 + 数据校验循环,最后再切到只用新库。

提醒:

问题是,迁移的过程中,如动态修改配置,服务重启期间存在时间差,导致的数据不一致,所以后续仍然需要进一步的校验!!!
常见的mysql,单个表超过千万已经很多了,超过只能分库分表了!!!

这是常用的一种迁移方案,比较靠谱,不用停机,不用看凌晨 4 点的风景:

  1. 在线上系统里所有写库的地方,增删改操作都加上对新库的增删改,老库新库同时写。
  2. 系统部署之后,新库数据差太远,跑导数工具把老库读出来写新库;写时根据 gmt_modified 这类字段判断这条数据最后修改的时间——除非读出来的数据在新库里没有,或者比新库的数据新才会写。简言之:不允许用老数据覆盖新数据
  3. 导完一轮之后,可能数据还是不一致,再跑一轮自动校验,比对新老库每个表的每条数据;不一样的从老库读再次写。反复循环,直到两个库每个表完全一致。
  4. 数据完全一致后,基于”仅使用分库分表的最新代码”重新部署一次,就完成了切换,几乎没有停机时间。

3. 千万级单表的冷热分表实践

一、现状

MySQL 下,某 business 单表已近 2000 万且还在持续增加中,存在多个索引,有较高的查询压力。现业务端使用 guava cache 拦了一道,还能顶得住,但是后台管理系统的全量数据的分页排序查询比较慢,且将来会越来越慢。

二、目标

业务端 + admin 查询都快。

三、解决方案

  1. 基于实际情况(大家一定要根据实际情况来),把数据库拆为三个:
    • 热数据(老表):提供 CURD 操作,事务加锁等等,最大限度地不用更改原代码。
    • 半年内数据(history):只提供查询业务。
    • 半年之前数据(backup):归档,不提供业务查询,只支持手动查库(已跟产品沟通好)。
  2. 数据迁移采用公司统一任务调度平台,注册任务后调度执行,自带 WEB 管理页面,支持暂停、恢复、执行计划、日志查询。
  3. 由于历史数据过千万,需要上线前进行一次手动迁移,初始化数据:
    • history 表保存:7 天 ~ 半年,非进行状态的数据。
    • backup 表保存:半年前的,非进行状态的数据。
    • 删除 business 表中,7 天前的,非进行状态的数据。
  4. 后续每天凌晨定时任务迁移数据(迁移时注意:保证 ID 一致):
    • business → history:> 7 天,非进行状态的数据。
    • history → backup:> 半年,非进行状态的数据。
  5. admin 切到从库读。主从分离,避免从库读全量数据导致业务端查询缓慢。

四、踩坑

  1. 千万级带索引删除记录,记得不能一次性直接 delete。可以根据创建时间来,一次删除百万级数据,多分几次删除。否则容易出现假死、慢查询、kill 不掉执行 SQL。
  2. 注意初始化数据时,可能当天多次执行,所以加上”修改时间在当天前”的条件,这样多次执行也不会出现数据重复。
  3. 写批量插入 SQL 时:
    • 不要用函数:SQL 中使用函数极端消耗时间。
    • 不要用 #,要用 $:避免再次编译消耗时间,这里不用怕 SQL 注入,内部接口。
  <insert id="transferTradingOrderByIds">
      insert into ${toTableName} (<include refid="Base_Column_List"/>)
      select <include refid="Base_Column_List"/>
      from ${fromTableName}
      <where>
          id in
          <foreach collection="transferTradingOrderIds" item="id" separator="," open="(" close=")">
              ${id}
          </foreach>
     </where>
 </insert>

五、结果

  • 热表数据:一百万内,增删改查极快。
  • 历史数据:一千万内,查询快。
  • 归档数据:千万级以上,慢,但是业务不调用。

后续可补的方向

  • 双写阶段如何识别”新库无 / 老库新”的写入冲突,给出基于 binlog 的校验脚本骨架。
  • 冷热三表方案下,跨表查询(例如 admin 端历史检索)的统一查询层设计。
  • 评估 TiDB / 分库分表中间件(ShardingSphere)替代手工冷热分表的成本与收益。