【MySQL】备份与恢复

​ MySQL 能恢复到半个月内任意一秒的状态,这是怎么做到的?它依赖:全量备份(状态快照) + 增量备份(Binlog 重放)。

1. 全量备份

​ 备份数据库全部的文件、数据库表、配置文件等。

​ 进行全量备份前,应始终作如下准备工作:检查备份服务器的磁盘空间,保证其可用空间大于数据库所占容量;检查 MySQL 服务器超时时间,防止时间过小导致备份中断;检查 MySQL 数据包大小限制参数;检查备份用户是否被赋予正当的权限;始终保证在系统负载较低的情况下进行备份。

​ 全量备份的主要实现方法可以分为全量逻辑备份和全量物理备份。

1.1 全量逻辑备份

​ 全量逻辑备份就是在备份时生成可以重建数据库的 SQL 语句,恢复时将这些 SQL 逐句执行。由于备份的是标准 SQL,因此只要是兼容 SQL 的数据库都可以对其进行恢复(仅需少量语法调整)。并且其灵活性较高,可以选择备份整个数据库、单个数据库、单个表,甚至只备份表结构或只备份数据。但是其在备份和恢复的速度上都远逊色于物理备份,因此对于中小型数据库更加适用。全量逻辑备份的常见方式有如下几种:

  • mysqldump

    ​ 为了保证数据一致性,mysqldump 默认会在备份开始时对表加一个读锁。在此期间,表可读但不可写(温备份),对于大表或繁忙的数据库,这会导致业务中断。因此对于 InnoDB 表,我们一般在启动 mysqldump 时使用 --single-transaction 选项来开启一个事务,利用 MVCC 获取一个一致性的数据快照。此时,备份过程不会阻塞写入操作(热备份),同时保证了数据一致性。


    ​ 我们通过 mysqldump 日志来看看 --single-transaction 选项在背后做了哪些事:

    Connect backup_user@localhost on using SSL/TLS

    ​ 连接数据库服务器。

    FLUSH /*!40101 LOCAL */ TABLES

    ​ 关闭所有已打开的表,并清除表缓存。LOCAL 字段表示此时会立即清除未正在被使用的表缓存,而正在被使用的表将被标记为在下次使用后关闭,不会阻塞等待。这一步是为下一步的读锁做准备,使下一步需要等待的表尽量少。

    FLUSH TABLES WITH READ LOCK

    ​ 短暂加读锁。

    SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ

    ​ 设置隔离级别为可重复读。

    START TRANSACTION /*!40100 WITH CONSISTENT SNAPSHOT */

    ​ 以上都是准备工作,现在真正开启一个事务并创建一致性快照。在此事务中所有的 SELECT 操作都会看到这个一致的数据视图。

    SHOW MASTER STATUS

    ​ 这条语句在下面详细讲,因为涉及到了其他选项。

    UNLOCK TABLES

    ​ 释放全局读锁,锁仅持有了很短暂的时间。


    --source-data 是另外一个很重要的 mysqldump 选项,当我们使用这个选项时,在日志中就会看到 SHOW MASTER STATUS 这条语句。其用法包括:

    --source-data=1:将 CHANGE MASTER TO 语句以非注释形式写入备份文件。

    --source-data=2:将 CHANGE MASTER TO 语句以注释形式写入备份文件。

    ​ 例如:

    sql 复制代码
    -- 使用 --source-data=2 生成的备份文件开头:
    --
    -- Position to start replication or point-in-time recovery from
    --
    
    -- CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000003', MASTER_LOG_POS=107;
    
    /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
    /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;

    ​ 可以看出,它在备份中的关键作用是记录当前(备份开始时)二进制日志的坐标。这在基于时间点的恢复和主从复制中非常重要,它告诉了我们从节点该从哪里开始应用主节点的 binlog。在 mysqldump 日志中可以看出,SHOW MASTER STATUS 是在其持有全局读锁时执行的,这是为了确保其精确记录开始备份时的 binlog 坐标。

    ​ 由于这里是全量备份,可以考虑使用 --flush-logs 选项在备份开始前创建一个新的二进制日志文件在备份后写入,这样可以使日志边界更清晰,因为新的日志文件只包含备份后的操作。但如果是频繁的备份操作,就不宜使用这个选项了,会产生很多几乎为空的日志文件。

    ​ mysqldump 是单线程的,并且由于其逻辑备份特性,备份大型数据库时会比较慢。对于大型数据库,应考虑使用物理备份。

  • Mydumper

    ​ 第三方开源的多线程逻辑备份方案,通常用以替代 mysqldump。

  • MySQL Shell & util.dumpInstance

    ​ MySQL Shell 是 Oracle 官方维护的高级、跨平台的客户端,它不仅支持 SQL ,还支持 NoSQL(如文档存储)、JavaScript 和 Python 脚本。其备份组件 util.dumpInstance(备份整个 MySQL 实例)、util.schemas(备份单个数据库)是更现代的逻辑备份工具选择。

    ​ 其自带多线程备份以及多线程恢复功能, 可以直接替代 mysqldump。

1.2 全量物理备份

​ 物理备份就是直接复制数据库的物理数据文件,由于其绕过了 MySQL Server 层,备份和恢复的速度都非常快。其缺点在于备份粒度较粗,并且恢复时依赖相同的硬件和 MySQL 版本。适用于数据量较大的数据库,追求最快的备份和恢复速度。

  • **Percona **

    ​ 最成熟的 MySQL 物理备份工具,由 Percona 公司开源且针对 InnoDB 实现热备份。

    ​ XtraBackup 的热备份实现原理与 mysqldump 完全不同,因为 XtraBackup 属于物理备份,它直接复制物理数据文件,无法依赖 MVCC 在备份开始时获取事务一致的视图。因此 XtraBackup 通过复制记录 Redo Log 来实现热备份,这个过程具体如下:

    ​ XtraBackup 在复制数据文件前记录 Redo Log 的当前 LSN,并使用一个后台线程持续追踪并复制 Redo Log 自备份开始后产生的新变化(必须复制走,因为 Redo Log 是循环日志,旧日志可能被新日志覆盖)。这些复制的 Redo Log 用于在后续重做备份过程中提交的事务。

    ​ XtraBackup 直接复制 InnoDB 的表空间文件(.ibd 和 ibdata)。由于不锁表,这些文件可能正在被修改,因此此时拷贝出来的数据文件可能包含未提交的事务,或者缺少已提交但尚未从日志刷新到数据文件的事务。这些事务依赖在上一步复制的 Redo Log 来重做,并使用 Undo Log 回滚。

    ​ 数据文件复制完成后,XtraBackup 短暂地为数据库加读锁,然后终止 Redo Log 的复制,并精确记录当前 Binlog 的坐标,之后解锁。

    ​ 备份完成后得到的数据文件需要经过一个独立的 "准备" 阶段,在这个阶段使用之前备份的 Redo Log 对 InnoDB 数据文件进行前滚(Redo)和回滚(Undo)。经过这个准备阶段,数据文件就达到了一个事务一致状态。使用 --apply-log 选项来开启准备阶段。

    ​ 需要注意的是,如果该全量备份后续还有增量备份(XtraBackup 也可以用于增量备份,后面会提到),使用 --apply-log 选项时必须搭配 --apply-log-only 选项,直到最后一条需要应用的增量备份。这表示在恢复数据时,直到所有目标数据都准备完毕,再使用 Undo Log 回滚未完成事务,这才是符合预期的。如果不加这个选项,那么前面的备份已经回滚了未完成事务,后续的备份可能包含这些事务的后续操作,这就会造成数据不一致。

    ​ 正确实例如下:

    bash 复制代码
    # 准备阶段
    xtrabackup --prepare --apply-log-only --target-dir=/path/to/full_backup
    xtrabackup --prepare --apply-log-only --target-dir=/path/to/full_backup \
        --incremental-dir=/path/to/inc1
    xtrabackup --prepare --apply-log-only --target-dir=/path/to/full_backup \
        --incremental-dir=/path/to/inc2
    xtrabackup --prepare --apply-log-only --target-dir=/path/to/full_backup \
        --incremental-dir=/path/to/inc3
    xtrabackup --prepare --target-dir=/path/to/full_backup
    
    # 开始恢复
    xtrabackup --copy-back --target-dir=/path/to/full_backup

2. 增量备份

​ 增量备份只备份⾃上次备份以来发⽣变化的数据。可以是上次全量备份以后,也可以是上次增量备份以后。

2.1 增量逻辑备份

  • Binlog

    ​ MySQL 的增量逻辑备份主要依赖 Binlog 来实现,这也是本篇文章介绍的重点。Binlog 属于 MySQL Server 层,无论使用什么存储引擎,只要发生表数据更新,就会产生 Binlog。


    Binlog 的三种格式:

    ​ statement 格式直接记录不加任何处理的 SQL 语句,这样做的问题是,在恢复数据时,很多 SQL 中函数的运算结果会与原库不一致,比如 now()、UUID() 等。我们一般使用 row 格式来规避上述问题,row 格式不再记录原始 SQL 语句了,而是记录操作的具体数据的前后镜像,类似这样:

    tex 复制代码
    ### UPDATE test.users
    ### WHERE
    ###   @1=1 (@1: id=1)
    ###   @2='John' (@2: name='John') 
    ###   @3=500 (@3: balance=500)
    ###   @4='active' (@4: status='active')
    ### SET
    ###   @1=1 (@1: id=1)
    ###   @2='John' (@2: name='John')
    ###   @3=600 (@3: balance=600)
    ###   @4='active' (@4: status='active')

    ​ 可以看出 row 是一种记录数据行变化的格式,恢复时服务器据此信息直接操作行数据。这也是 MySQL 默认的 binlog 格式。这种格式的缺点是空间占用较大,因为一条 SQL 可能修改成千上万行,而 row 格式会记录这成千上万行的数据,不过为了数据的绝对准确性,这样的空间消耗是可以接受的。

    ​ row 格式是二进制的,statement 是文本格式的,这是因为 row 记录的是结构化的行数据,使用二进制更高效和精确,但不直接可读,而 statement 记录的就是 SQL 语句,所以理所当然使用了文本格式。

    ​ 这表示我们无法直接用 mysqlbinlog 像看 SQL 一样轻松地看懂 row 格式的 binlog,虽然 -v 选项可以将其翻译成伪 SQL,但失去了原始的 SQL 信息。

    ​ 还有一种 mixed 格式,这种格式下 MySQL 会判断该条 SQL 是否可能引起数据不一致,如果可能就使用 row,如果不可能就使用 statement,但是 MySQL 的判断不一定非常准确,可能存在漏网之鱼,所以我们还是使用 row 格式。


    Binlog 的刷盘策略:

    ​ 当且仅当事务提交时,才将整个事务的 Binlog 一次持久化到磁盘日志文件中,此前 Binlog 维护在线程私有的 Binlog Cache 中。这表示 Binlog 是严格以完整的事务为单位进行写入的,每个线程拥有独立的缓冲区保证事务之间不会交叉记录(事务原子性写入)。

    ​ 这样的设计与 Redo Log 有较大不同,Redo Log 的缓冲区是公有的,其持久化并不严格以事务为单位。也就是说磁盘上的 Redo Log 文件中并不保证记录的都是完整的事务,崩溃恢复时,依赖 Undo Log 来回滚那些不完整的事务。

    ​ 为什么会有这样的设计差异呢?因为其设计目标完全不同。Redo Log 是一种物理日志,它更底层,面向数据页,它存在的目的只有一个,就是以最快速度将页面恢复到某个状态,至于这个状态中哪些事务有效,由 Undo Log 来判定。它面向的是快速灾救场景,需要保证速度,其日志内容在对应的数据页持久化后就没用了。而 Binlog 是一种逻辑日志,更上层,恢复它需要 MySQL 服务器的参与,因为其本质就是备份的一份 SQL 指令集。如果事务都是混在一起的,MySQL 服务器根本无法运行成功。

    sync_binlog 参数用以配置 Binlog 的刷盘策略:

    sync_binlog=0 事务提交时,只调用 write 将日志写入 OS 的 page cache,不主动调用 fsync 刷盘,由 OS 决定何时刷盘。此时意味着我们无法承受 OS 级别的故障。

    sync_binlog=1 事务提交时手动调用 fsync 刷盘,最大程度保证数据安全。

    sync_binlog=N 积累 N 个事务提交时才手动调用 fsync 刷盘,这表示 N 个事务可能因 OS 级别的故障而丢失。


    两阶段提交:

    ​ 两阶段提交是为了保证 Redo Log 和 Binlog 的数据一致性。

    ​ 事务提交时,Redo Log Buffer 持久化后事务状态被标记为 prepare。Binlog Cache 持久化后会发送一个信号给 InnoDB,告知其 Binlog 写入成功。InnoDB 收到信号后将事务状态正式标记为 commit。

    ​ 应用两阶段提交后,在崩溃恢复时,Redo Log 中如果某事务状态依然是 prepare,并且没有对应的 Binlog,那意味着该事务没有写入 Binlog,则 InnoDB 会回滚该事务。如果事务状态是 prepare,但是有对应的 Binlog,那意味着 Binlog 写入成功了,只是信号没有传递到 InnoDB,此时 InnoDB 不会回滚该事务。


    Binlog 恢复:

    ​ 使用 mysqlbinlog 客户端来解析指定范围内的所有日志,并生成一个包含对应 SQL 语句的文件。再将生成的 SQL 文件导入到已恢复全量备份的数据库中即可。

    bash 复制代码
    # 将 binlog 重放到指定时间点
    mysqlbinlog \
      --start-datetime="2023-10-01 00:00:00" \
      --stop-datetime="2023-10-15 20:30:15" \
      /path/to/binlog.000001 /path/to/binlog.000002 ... > binlog_replay.sql
    
    # 执行恢复
    mysql -u root -p < binlog_replay.sql

2.2 增量物理备份

  • Percona XtraBackup

    ​ XtraBackup 也可以进行增量备份,它通过比较数据页的 LSN 来识别该页数据是否发生变化。如果 LSN 大于上次备份的 LSN,说明该页发生了新的修改,重新备份该页,否则则直接跳过。

    ​ 具体的备份过程和 XtraBackup 的全量备份一致,都是靠一个后台线程持续复制 Redo Log 来实现热备份。

相关推荐
q***7482 小时前
数据库高安全—openGauss安全整体架构&安全认证
数据库·安全·架构
l***37092 小时前
redis info 详解
数据库·redis·缓存
Hello.Reader2 小时前
Flink DataStream API 打包使用 MySQL CDC 连接器
大数据·mysql·flink
小蜗牛爱远行3 小时前
mysql导入中文乱码问题
数据库·mysql
TangDuoduo00053 小时前
【SQLite3 C语言接口】
数据库·sqlite
-大头.3 小时前
Redis内存碎片深度解析:从动态整理到核心运维实践
运维·数据库·redis
一 乐4 小时前
健康打卡|健康管理|基于java+vue+的学生健康打卡系统设计与实现(源码+数据库+文档)
android·java·数据库·vue.js·spring boot·微信小程序
ghie90904 小时前
使用Java实现用户的注册和登录流程
java·数据库·oracle
while(1){yan}5 小时前
MYSQL索引的底层数据结构
数据结构·数据库·mysql