数据迁移细节

数据迁移运用场景:

  • 重构项目但数据不能丢失,需要重新设计表结构
  • 单库拆分分库分表

数据备份工具

  • mysqldump:一个用于备份和恢复 MySQL 数据库的命令行工具。它允许用户导出 MySQL 数据库的结构、数据以及表之间的关系,以便在数据库发生问题时进行恢复。它是一个逻辑备份工具,导出的内容是一条条 SQL。
  • XtraBackup:它使用了 InnoDB 存储引擎的数据备份技术,支持增量备份和恢复,并且支持多主机备份和恢复。它是一个物理备份工具,相当于直接复制 InnoDB 的底层存储文件。

innodb_autoinc_lock_mode 是 InnoDB 引擎里面控制自增主键生成策略的参数,它有三个取值。

  • 0:使用表自增锁,但是锁在 INSERT 语句结束之后就释放了。

  • 1:使用表自增锁,如果是普通的 INSERT INTO VALUE 或者 INSERT INTO VALUES 语句,申请了主键就释放锁,而不是整个 INSERT 语句执行完毕才释放。如果是 INSERT SELECT 等语句,因为无法确定究竟要插入多少行,所以都是整个 INSERT 语句执行完毕才释放。

  • 2:使用表自增锁,所有的语句都是申请了主键就立刻释放。

一个高效、稳定的数据迁移方案

基本步骤:

  1. 创建目标表

  2. 用源表的数据初始化目标表。

    • 使用源表的历史备份,基本上数据库都会有备份机制,那么你自然可以利用这些备份来初始化目标表的数据。
    • 源表导出数据,大部分情况下,使用 mysqldump 是不会出问题的,无非就是导出导入慢一些,而这也恰好是你刷亮点的地方。

    亮点:加快mysqldump导入和导出速度

    • 加快导出速度能做的事情并不多,主要就是开启 extended-insert 选项,将多行合并为一个 INSERT 语句。
    • 加快导入速度就可以做比较多的事情。
      1. 关闭唯一性检查和外键检查,源表已经保证了这两项,所以目标表并不需要检查。
      2. 关闭 binlog,毕竟导入数据用不着 binlog。
      3. 调整 redo log 的刷盘时机,把 innodb_flush_log_at_trx_commit 设置为 0(只刷在buffer中)。
  3. 执行一次校验,并且修复数据,此时用源表数据修复目标表数据。

    所有的表都需要有 update_time 这个字段,那么你在校验和修复的时候就可以采用增量的方案。因为只有 update_time 晚于你导出数据的那个时间点,才说明这一行的数据已经发生了变更。在修复的时候就直接用源表的数据覆盖掉目标表的数据

  4. 业务代码开启双写,此时读源表,并且先写源表,数据以源表为准。

    支持双写大体上有两个方向:侵入式和非侵入式两种:

    • 侵入式:侵入式方案就是直接修改业务代码。要求业务代码在写了源表之后再写目标表。但是侵入式方案是不太可行的,或者说代价很高。因为这意味着所有的业务都要检查一遍,然后修改。

    • 非侵入式:非侵入式一般和你使用的数据库中间件有关,比如说 ORM 框架。这一类框架一般会提供两种方式来帮你解决类似的问题。

      1. AOP(Aspect Oriented Program 面向切面编程)方案:不同框架有不同叫法,比如说可能叫做 interceptor、middleware、hook、handler、filter。这个方案的关键就是捕捉到发起的增删改调用,篡改为双写模式。
      2. 数据库操作抽象:可能叫做 Session、Connection、Connection Pool、Executor 等,就是将对源表的操作修改为双写模式。

      不管你采用哪个方案,你都要确保一个东西,就是双写可以在运行期随时切换状态,单写源表、先写源表、先写目标表、单写目标表都可以。

      • 大多数时候都是利用一个标记位,然后你可以通过配置中心或者接口直接修改它的值。

      正常面试官都可能会问到,如果在双写过程中,写入源表成功了,但是写入目标表失败了,该怎么办?

      • 在设计方案的时候,我考虑过在写入目标表失败的时候,发一个消息到消息队列,然后尝试修复数据。但是这个其实很难做到,因为我不知道该修复哪些数据。比如说一个 UPDATE 语句在目标表上执行失败,我没办法根据 UPDATE 语句推断出源表上哪些行被影响到了,除非可以拿到源库执行的sql。

      主键问题:

      • 在双写的时候比较难以处理的问题是自增主键问题。为了保持源表和目标表的数据完全一致,需要在源表插入的时候拿到自增主键的值,然后用这个值作为目标表插入的主键。
      • 在处理批量插入的时候要更加小心一些。正常来说,批量插入如果用的是 VALUES 语法,那么生成的主键是连续的,就可以从返回的最后一个主键推测出前面其他行的主键。即便 innodb_autoinc_lock_mode 取值是 2 也能保证这一点。但是如果用的是多个 INSERT INTO VALUE 语句,或者 INSERT SELECT 语句,这些语句生成的主键就可能不连续。在双写之前,就要先改造这一类的业务。
  5. 开启增量校验和数据修复,保持一段时间。

    增量校验基本上就是一边保持双写,一边校验最新修改的数据,如果不一致,就要进行修复。

    有两个方案:

    • 利用更新时间戳,比如说 update_time 这种列,利用更新时间戳的思路很简单,就是定时查询每一张表,然后根据更新时间戳来判断某一行数据有没有发生变化。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      // 伪代码
      for {
      // 执行查询
      // SELECT * FROM xx WHERE update_time >= last_time
      rows := findUpdatedRows()
      for row in rows {
      // 找到目标行,要用主键来找,用唯一索引也可以,看你支持到什么程度
      tgtRow = findTgt(row.id)
      if row != tgtRow {
      // 修复数据
      fix()
      }
      }
      // 用这一批数据里面最大的更新时间戳作为下一次的起始时间戳
      last_time = maxUpdateTime(row)
      // 睡眠一下
      sleep(1s)
      }

      注意点:

      • 注意源表需要软删除。

      • 校验和修复的时候都要小心主从同步的问题,如果校验和修复都使用从库的话,那么就会出现校验出错,或者修复出错的情况。按照道理来说,强制走主库就可以解决问题,但是这样对主库的压力是比较大的。

      • 所以我采用的是双重校验方案。第一次校验的时候读从库,如果发现数据不一致,再读主库,用主库的数据再校验一次。修复的时候就只能以主库数据为准。这种方案的基本前提是,主从延迟和数据不一致的情况是小概率的,所以最终会走到主库也是小概率的。

    • 利用 binlog:基于行的 binlog 模式

      在校验和修复的数据时候,我采用的是监听 binlog 的方案。binlog 只用于触发校验和修复这个动作,当我收到 binlog 之后,我会用 binlog 中的主键,去查询源表和目标表,再比较两者的数据。如果不一致,就用源表的数据去修复目标表。

      它虽然能够进一步减轻数据库查询的压力,但是实在过于复杂,得不偿失。所以不管是实践,还是面试,我都建议你不要使用这个方案。

  6. 切换双写顺序,此时读目标表,并且先写目标表,数据以目标表为准。

    引入这一步,是为了能够在切换到以目标表为准之前,有一个过渡阶段。也就是说,通过先写目标表,再写源表这种方式,万一发现数据迁移出现了问题,还可以回滚为先写源表,再写目标表,确保业务没有问题。

  7. 继续保持增量校验和数据修复。

    在切换了双写顺序之后,保持增量校验和修复是顺理成章的,方案和步骤 5 一样。不过步骤 5 的校验和修复都是以源表为准,那么在这一步,就是以目标表为准。

  8. 切换为目标表单写,读写都只操作目标表。

总结要点:

  • 不管什么先后顺序问题、什么并发问题,在修复的时候你永远用主表的最新数据去修复,绝对不会出问题。
  • 如果源表或者目标表本身也是分库分表的,那么无非就是查询、修复数据的时候使用对应的分库分表规则而已。
  • 整个方案在第八步之前,都是可以回滚的。但是一旦切换到第八步,就不可能回滚了。