MySQL 生产级备份与恢复全攻略:全量 / 增量 / 逻辑 / 物理备份深度拆解 + 误删数据秒级恢复实战

前言

数据是企业业务的核心资产,MySQL备份与恢复体系不是可有可无的运维操作,而是数据安全的最后一道防线。面对误操作、硬件故障、机房灾难、勒索攻击等各类风险,没有合格的备份体系,数据丢失往往是不可逆的。

一、MySQL备份体系的核心分类与底层逻辑

很多开发者对备份分类存在认知混淆,首先划清两个正交的核心维度边界:

  • 备份数据范围划分:全量备份、增量备份(含差异备份)

  • 备份实现方式划分:逻辑备份、物理备份

两个维度互不包含,全量备份可以是逻辑或物理形式,增量备份同理。

1.1 全量备份 vs 增量备份:范围维度的底层逻辑

1.1.1 全量备份

定义:对指定的MySQL实例、数据库、数据表,在某个时间点生成完整的数据快照,包含该时间点的所有有效数据。 底层逻辑:无论数据是否在上次备份后发生变更,都会完整备份当前的全量数据。 核心特性:

  • 恢复链路最短,单份备份集即可完成恢复,无需依赖其他备份文件

  • 备份体积大,备份耗时久,对数据库资源占用更高

  • 备份集独立,不存在依赖链断裂的风险

1.1.2 增量备份

定义:仅备份上一次备份(全量/增量)完成后发生变更的数据,核心依赖InnoDB的LSN(日志序列号)与MySQL的binlog实现。 这里明确两个极易混淆的子类型:

  • 增量备份(Incremental Backup):基于上一次任意类型备份(全量/增量)的变更数据,备份体积最小,备份速度最快,但恢复时需要依赖完整的备份链路(全量+所有增量),链路中任意一份备份损坏,整个恢复流程失败。

  • 差异备份(Differential Backup):基于上一次全量备份的变更数据,备份体积随时间递增,恢复时仅需全量备份+最新的差异备份,链路风险低,恢复速度更快。

底层核心:InnoDB的每个数据页都有一个唯一的LSN,这是一个单调递增的64位整数,标记数据页的最后修改时间。增量备份时,仅备份LSN大于上一次备份结束LSN的数据页,无需扫描全量数据,实现高效备份。

1.2 逻辑备份 vs 物理备份:实现维度的底层拆解

这是MySQL备份最核心的分类,直接决定备份的性能、恢复速度、适用场景,必须从底层讲透。

1.2.1 逻辑备份

本质:备份的是数据的逻辑内容,而非物理存储文件。核心是通过MySQL协议,从存储引擎读取数据,转换成SQL语句(CREATE/INSERT)、CSV文本等逻辑格式,写入备份文件。 核心实现原理:

  1. 与MySQL建立客户端连接,通过SQL接口获取表结构、数据、存储过程、触发器、事件等元数据

  2. 对InnoDB引擎,在RR隔离级别下开启事务,生成一致性快照,全程无锁备份

  3. 逐行读取表数据,转换成INSERT语句或结构化文本,写入备份文件

  4. 记录备份时刻的binlog位置与GTID,用于后续时间点恢复 主流工具:mysqldump(MySQL原生)、mydumper(高性能多线程)、select into outfile

1.2.2 物理备份

本质:直接备份MySQL数据库的物理存储文件,包括InnoDB的表空间文件(.ibd)、共享表空间(ibdata1)、redo log、undo log、系统表空间、配置文件等,完全绕过MySQL的SQL层,直接操作磁盘文件。 按备份时数据库的运行状态,分为三类:

  • 冷备:数据库完全关闭,直接复制所有物理文件,备份一致性100%,无性能影响,但业务必须停机,生产环境极少使用

  • 温备:数据库运行中,全局锁表禁止写入,复制物理文件,备份期间业务只读,对业务影响大,仅适用于只读从库

  • 热备:数据库正常运行,读写不受影响,全程无锁备份,生产环境唯一推荐的物理备份方式,核心依赖InnoDB的崩溃恢复机制实现 主流工具:Percona XtraBackup(开源免费)、MySQL Enterprise Backup(企业版付费)

1.2.3 核心特性对比
对比维度 逻辑备份 物理备份
底层原理 备份SQL/数据逻辑内容,通过SQL层读取数据 直接备份磁盘物理文件,绕过SQL层
备份速度 单线程慢,多线程工具中等,需逐行读取转换 极快,多线程并行复制文件,仅受磁盘IO限制
恢复速度 慢,需逐行执行SQL,重建索引,大库恢复耗时数小时 极快,直接复制文件回数据目录,分钟级恢复TB级数据
备份粒度 极灵活,支持实例级、库级、表级、行级备份 支持实例级、库级、表级,行级备份不支持
锁机制 InnoDB引擎无锁(--single-transaction),MyISAM需锁表 热备全程无锁,不影响业务读写
跨版本兼容性 极高,跨MySQL版本、跨操作系统均可恢复 低,需与原数据库版本、操作系统、页大小严格匹配
备份集体积 小,仅备份有效数据,压缩比高 大,备份完整表空间,包含空闲页,需压缩优化
适用场景 小数据量备份、单表/单库恢复、数据迁移、版本升级 生产级全量/增量备份、大库快速恢复、灾备建设

二、备份方案落地实战

2.1 逻辑备份生产级落地

2.1.1 mysqldump 全量备份实战

mysqldump是MySQL原生自带的逻辑备份工具,无需额外安装,兼容性极强,适用于100GB以内的小库备份、单表恢复、数据迁移场景。

生产级全库备份命令:

复制代码
mysqldump -uroot -p'Your_Strong_Password' \
--single-transaction \
--master-data=2 \
--routines \
--triggers \
--events \
--hex-blob \
--default-character-set=utf8mb4 \
--databases db1 db2 \
--ignore-table=db1.ignore_table \
| gzip > /backup/mysql_full_backup_$(date +%Y%m%d_%H%M%S).sql.gz

核心参数详解:

  • --single-transaction:对InnoDB引擎开启一致性快照备份,基于RR隔离级别,全程不锁表,不影响业务读写,仅对InnoDB有效,MyISAM表仍需锁表

  • --master-data=2:在备份文件中注释记录备份时刻的binlog文件名与position位置,用于时间点恢复;=1则不注释,直接生成CHANGE MASTER语句,用于主从搭建

  • --routines/--triggers/--events:备份存储过程、函数、触发器、定时事件,默认不会备份这些内容,极易遗漏

  • --hex-blob:将BLOB、BINARY、VARBINARY类型数据以十六进制格式备份,避免字符集问题导致的数据损坏

  • --default-character-set=utf8mb4:指定字符集,避免乱码

  • --databases:指定要备份的数据库,不指定则备份整个实例

  • --ignore-table:忽略不需要备份的表,如大日志表、临时表

分库分表备份实例:

复制代码
# 单库备份
mysqldump -uroot -p'Your_Strong_Password' --single-transaction --master-data=2 --routines --triggers --events db1 | gzip > /backup/db1_backup_$(date +%Y%m%d).sql.gz

# 单表备份
mysqldump -uroot -p'Your_Strong_Password' --single-transaction --master-data=2 db1 user | gzip > /backup/db1_user_table_backup_$(date +%Y%m%d).sql.gz

备份恢复实例:

复制代码
# 全库恢复(先解压,再执行)
gzip -d mysql_full_backup_20240501_030000.sql.gz
mysql -uroot -p'Your_Strong_Password' < mysql_full_backup_20240501_030000.sql

# 单库恢复
mysql -uroot -p'Your_Strong_Password' -e "CREATE DATABASE IF NOT EXISTS db1 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;"
mysql -uroot -p'Your_Strong_Password' db1 < db1_backup_20240501.sql

# 单表恢复
mysql -uroot -p'Your_Strong_Password' db1 < db1_user_table_backup_20240501.sql
2.1.2 高性能多线程逻辑备份:mydumper

mysqldump是单线程执行,当数据库超过100GB时,备份耗时极长。mydumper是开源多线程逻辑备份工具,支持并行备份与恢复,性能是mysqldump的3-10倍,同时支持一致性备份、binlog位置记录、压缩等生产级特性。

生产级备份命令:

复制代码
mydumper -u root -p 'Your_Strong_Password' \
--threads=8 \
--database=db1 \
--rows=100000 \
--compress \
--triggers \
--routines \
--events \
--outputdir=/backup/mydumper_backup_$(date +%Y%m%d)

核心参数:

  • --threads:指定并行备份的线程数,通常设置为CPU核心数的1-2倍

  • --rows:将大表按行数切分成多个chunk,多线程并行备份,提升大表备份速度

  • --compress:开启备份文件压缩,大幅降低磁盘占用

  • --outputdir:备份文件输出目录,每个表生成单独的建表语句与数据文件

恢复命令(myloader,mydumper配套的多线程恢复工具):

复制代码
myloader -u root -p 'Your_Strong_Password' \
--threads=8 \
--directory=/backup/mydumper_backup_20240501 \
--database=db1 \
--overwrite-tables

2.2 物理备份落地

生产环境TB级大库的备份与恢复,必须使用物理备份,核心推荐Percona XtraBackup,开源免费,支持MySQL 8.0全版本,热备无锁,性能极强。

2.2.1 XtraBackup 全量热备实战

底层原理:

  1. 启动备份时,启动一个后台线程持续监控并复制redo log,确保备份期间所有数据变更都被记录

  2. 并行复制InnoDB的.ibd表空间文件与共享表空间

  3. 数据文件复制完成后,停止redo log复制,执行FLUSH TABLES WITH READ LOCK,复制MyISAM表与系统表元数据

  4. 解锁表,记录备份时刻的binlog位置与LSN,生成备份元数据文件

  5. 整个过程中,InnoDB表全程无锁,仅在复制MyISAM表时加全局读锁,对业务影响极小

全量备份命令:

复制代码
xtrabackup --user=root --password='Your_Strong_Password' \
--backup \
--target-dir=/backup/xtrabackup_full_$(date +%Y%m%d) \
--parallel=4 \
--compress \
--compress-threads=2

核心参数:

  • --backup:指定执行备份操作

  • --target-dir:备份文件输出目录,必须为空目录

  • --parallel:多线程并行复制数据文件,提升备份速度

  • --compress:开启备份文件压缩,使用qpress算法,压缩比极高

  • --compress-threads:压缩并行线程数

备份集预处理(Prepare): 这是物理备份恢复前必须执行的步骤,备份完成后,备份集里的数据文件是不一致的,因为备份期间数据在持续变更,redo log还未应用到数据文件。必须通过prepare操作,将redo log应用到数据文件,生成一致性的备份集。

复制代码
# 解压压缩备份集(如果开启了--compress)
xtrabackup --decompress --target-dir=/backup/xtrabackup_full_20240501 --parallel=4

# 执行prepare操作
xtrabackup --prepare --target-dir=/backup/xtrabackup_full_20240501

全量备份恢复:

复制代码
# 停止MySQL服务
systemctl stop mysqld

# 清空MySQL数据目录(必须提前备份原有数据,避免误操作)
rm -rf /var/lib/mysql/*

# 执行恢复操作
xtrabackup --copy-back --target-dir=/backup/xtrabackup_full_20240501

# 修改数据目录权限,必须为mysql用户所有,否则MySQL无法启动
chown -R mysql:mysql /var/lib/mysql

# 启动MySQL服务
systemctl start mysqld

--copy-back:复制备份文件到MySQL数据目录,保留原备份集;--move-back:移动备份文件到数据目录,原备份集被删除,磁盘空间不足时使用。

2.2.2 XtraBackup 增量备份实战

增量备份仅备份上一次备份后变更的数据页,备份速度极快,磁盘占用极小,是生产环境核心的备份补充手段。

增量备份完整流程实例:

  1. 周日凌晨执行全量基础备份

    xtrabackup --user=root --password='Your_Strong_Password' --backup --target-dir=/backup/xtrabackup_full_base

  2. 周一凌晨执行第一次增量备份,基于周日的全量备份

    xtrabackup --user=root --password='Your_Strong_Password'
    --backup
    --target-dir=/backup/xtrabackup_incr_1
    --incremental-basedir=/backup/xtrabackup_full_base

--incremental-basedir:指定上一次备份的目录,增量备份仅备份该目录之后变更的数据。

  1. 周二凌晨执行第二次增量备份,基于周一的增量备份

    xtrabackup --user=root --password='Your_Strong_Password'
    --backup
    --target-dir=/backup/xtrabackup_incr_2
    --incremental-basedir=/backup/xtrabackup_incr_1

增量备份集预处理与恢复: 增量备份的prepare操作和全量备份不同,分为两个阶段:

  1. 先对基础全量备份执行prepare,仅应用redo log,不回滚未提交事务(--apply-log-only)

    xtrabackup --prepare --apply-log-only --target-dir=/backup/xtrabackup_full_base

  2. 合并第一次增量备份到基础全量备份

    xtrabackup --prepare --apply-log-only
    --target-dir=/backup/xtrabackup_full_base
    --incremental-dir=/backup/xtrabackup_incr_1

  3. 合并第二次增量备份到基础全量备份(最后一次增量合并,去掉--apply-log-only,执行事务回滚)

    xtrabackup --prepare
    --target-dir=/backup/xtrabackup_full_base
    --incremental-dir=/backup/xtrabackup_incr_2

  4. 合并完成后,/backup/xtrabackup_full_base就是完整的一致性备份集,后续恢复步骤和全量备份完全一致。

2.3 生产级备份策略组合方案

生产环境最优的备份策略,是全量物理备份+增量物理备份+binlog实时备份的组合,兼顾备份效率、恢复速度、数据安全性,同时最小化对业务的影响。

标准生产备份策略:

  • 每周日凌晨03:00,执行XtraBackup全量物理备份,压缩加密后同步到异地存储

  • 周一至周六凌晨03:00,执行XtraBackup增量物理备份,基于前一天的增量备份

  • 开启binlog实时备份,通过mysqlbinlog --stop-never实时同步binlog到异地存储,确保数据零丢失

  • 备份集保留策略:全量备份保留30天,增量备份保留7天,binlog保留15天

  • 每月执行一次全量恢复演练,验证备份集的可用性

备份流程流程图:

生产级备份架构图:

2.4 binlog备份:时间点恢复的核心保障

binlog记录了MySQL所有数据变更的操作,是实现时间点恢复(PITR)的核心,也是增量备份的底层基础,生产环境必须开启并做好实时备份。

binlog核心配置(my.cnf):

复制代码
# 开启binlog
log_bin = /var/lib/mysql/mysql-bin
# binlog格式,必须设置为ROW,确保闪回与恢复的准确性
binlog_format = ROW
# 记录完整的行镜像,闪回必备
binlog_row_image = FULL
# 每次事务提交都同步binlog到磁盘,确保数据不丢失
sync_binlog = 1
# binlog过期时间,设置为15天,单位秒
binlog_expire_logs_seconds = 1296000
# 开启GTID,简化主从搭建与恢复操作
gtid_mode = ON
enforce_gtid_consistency = ON

binlog实时备份命令:

复制代码
nohup mysqlbinlog --read-from-remote-server --raw --stop-never \
--host=mysql主库IP --port=3306 --user=root --password='Your_Strong_Password' \
mysql-bin.000001 \
--result-file=/backup/binlog_backup/ > /backup/binlog_backup.log 2>&1 &

--stop-never:持续从主库同步binlog,实时备份,确保主库宕机时binlog不丢失。

三、数据误删的全场景快速恢复方案

数据误删是生产环境最高发的故障,针对不同的误操作场景,有对应的恢复方案,核心原则是:先冻结写入,再选择最优恢复方案,最小化数据丢失与业务停机时间

3.1 恢复前置准备(事前必须做好)

没有这些准备,误删后恢复难度极大,甚至无法恢复:

  1. 必须开启binlog,格式为ROW,binlog_row_image=FULL

  2. 落地完整的备份策略,定期验证备份集可用性

  3. 搭建1小时延迟从库,这是秒级恢复的核心神器

  4. 部署binlog闪回工具(binlog2sql、MyFlash)

  5. 开启MySQL 8.0的InnoDB回收站功能

3.2 场景一:DML行级误操作(DELETE/UPDATE 无WHERE/WHERE条件错误)

这是最高发的误操作场景,比如误执行了DELETE FROM user WHERE 1=1; 或者UPDATE user SET phone='123456' WHERE 1=1; 导致全表数据被修改/删除。

最优恢复方案:binlog闪回,分钟级恢复数据 核心原理:ROW格式的binlog记录了每行数据变更前的完整镜像与变更后的镜像,闪回工具可以基于binlog生成反向SQL,DELETE操作反向生成INSERT,UPDATE操作反向生成还原UPDATE,INSERT操作反向生成DELETE,实现数据恢复。

恢复完整流程实例:

  1. 立即冻结业务写入,避免新的操作覆盖binlog,可通过设置只读锁:

    FLUSH TABLES WITH READ LOCK;
    -- 注意:不要退出当前会话,退出后锁自动释放

  2. 查找误操作对应的binlog文件与位置:

    -- 查看当前所有binlog文件
    SHOW BINARY LOGS;
    -- 查看最近的binlog事件,定位误操作的时间点与position
    SHOW BINLOG EVENTS IN 'mysql-bin.000123' LIMIT 100;

  3. 使用binlog2sql工具生成反向SQL:

    先安装binlog2sql

    pip3 install binlog2sql

    生成误操作的原始SQL,确认位置

    python3 binlog2sql.py -h127.0.0.1 -P3306 -uroot -p'Your_Strong_Password'
    -d db1 -t user
    --start-file='mysql-bin.000123'
    --start-datetime='2024-05-01 10:00:00'
    --stop-datetime='2024-05-01 10:05:00'

    确认位置后,生成反向闪回SQL

    python3 binlog2sql.py -h127.0.0.1 -P3306 -uroot -p'Your_Strong_Password'
    -d db1 -t user
    --start-file='mysql-bin.000123'
    --start-position=123456
    --stop-position=123789
    --flashback > flashback_restore.sql

  4. 校验flashback_restore.sql中的SQL语句,确认无误后执行恢复:

    mysql -uroot -p'Your_Strong_Password' db1 < flashback_restore.sql

  5. 验证数据恢复完成后,解锁只读锁,恢复业务写入:

    UNLOCK TABLES;

3.3 场景二:DDL误操作(DROP TABLE/TRUNCATE TABLE/DROP DATABASE)

这类误操作破坏性极强,DDL操作不会记录行级binlog,binlog闪回无效,必须使用更高级的恢复方案。

3.3.1 最优方案:延迟从库秒级恢复

延迟从库是指通过MASTER_DELAY参数,设置从库的SQL线程延迟执行主库的事务,比如设置延迟3600秒(1小时),主库的误操作,要1小时后才会在从库执行,给了充足的应急时间。

延迟从库配置:

复制代码
-- 在从库执行,设置延迟1小时
CHANGE REPLICATION SOURCE TO SOURCE_DELAY=3600 FOR CHANNEL 'mysql_main';
-- 启动复制
START REPLICA;

恢复完整流程:

  1. 主库发生误删表操作后,立即登录延迟从库,停止SQL线程:

    STOP REPLICA SQL_THREAD;

  2. 查找误操作的GTID或position,确认事务还未在从库执行:

    -- 查看从库复制状态,确认延迟时间
    SHOW REPLICA STATUS\G
    -- 查看中继日志中的事件,定位误操作的GTID
    SHOW RELAYLOG EVENTS LIMIT 100;

  3. 跳过误操作的事务,启动SQL线程,同步到误操作之前的时间点:

    -- 基于GTID跳过误操作事务(推荐)
    SET GTID_NEXT='aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:12345';
    BEGIN;
    COMMIT;
    SET GTID_NEXT='AUTOMATIC';

    -- 启动SQL线程,同步剩余正常事务
    START REPLICA SQL_THREAD;

  4. 等待从库同步完成后,将被误删的表从从库导出,导回主库,完成恢复,全程分钟级完成,几乎无数据丢失。

3.3.2 备选方案:全量备份+binlog时间点恢复

如果没有延迟从库,使用该方案,核心是恢复到误操作之前的时间点。

恢复流程:

  1. 找到误操作之前最近的全量备份,执行全量恢复,恢复到备份时间点

  2. 基于备份文件中记录的binlog位置,重放binlog到误操作之前的position/时间点

    重放binlog,到误操作之前的position

    mysqlbinlog --start-position=107 --stop-position=123456 mysql-bin.000123 | mysql -uroot -p'Your_Strong_Password'

    基于时间点重放

    mysqlbinlog --start-datetime='2024-05-01 03:00:00' --stop-datetime='2024-05-01 10:02:00' mysql-bin.000123 mysql-bin.000124 | mysql -uroot -p'Your_Strong_Password'

3.3.3 MySQL 8.0 回收站快速恢复

MySQL 8.0.23及以上版本,支持InnoDB回收站,DROP TABLE/DROP DATABASE操作不会立即删除数据文件,而是移动到回收站,在保留时间内可以快速恢复。

回收站核心配置:

复制代码
# 开启回收站,默认ON
innodb_recycle_bin = ON
# 回收站数据保留时间,默认7天,单位秒
innodb_recycle_bin_retention = 604800

恢复实例:

  1. 查看回收站中的被删除表:

    SELECT * FROM information_schema.INNODB_RECYCLE_BIN;

  2. 执行恢复命令,将回收站中的表恢复到原库:

    -- 语法:CALL sys.innodb_recycle_bin_restore('回收站表名', '恢复后的表名');
    CALL sys.innodb_recycle_bin_restore('#sql-ib123-456789', 'user');

执行完成后,表即可恢复,包含所有数据,全程秒级完成。

3.4 场景三:表空间损坏/数据文件误删

这类故障是物理层面的损坏,比如rm -rf误删了.ibd文件,磁盘故障导致表空间损坏,核心恢复方案是物理备份恢复,或者InnoDB表空间导入。

单表空间导入恢复实例:

  1. 找到该表的建表语句,在新的实例中创建相同的表

    CREATE TABLE user (
    id BIGINT NOT NULL AUTO_INCREMENT,
    name VARCHAR(64) NOT NULL,
    PRIMARY KEY (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

  2. 丢弃新表的表空间:

    ALTER TABLE user DISCARD TABLESPACE;

  3. 将备份的.ibd文件复制到新表的数据目录,修改权限为mysql:mysql

  4. 导入表空间:

    ALTER TABLE user IMPORT TABLESPACE;

执行完成后,单表数据即可完整恢复。

误删数据恢复流程总览:

四、备份与恢复最佳实践与避坑指南

4.1 备份核心最佳实践

  1. 备份必须异地存储,绝对不能和数据库放在同一台服务器、同一个机房,避免机房故障、勒索攻击导致备份与数据同时丢失

  2. 备份后必须执行校验,不仅要校验备份文件的完整性,还要定期执行恢复演练,能成功恢复的备份才是有效的备份

  3. 优先在从库执行备份,避免备份操作占用主库的CPU、IO资源,影响业务性能

  4. 备份集必须加密,避免备份文件泄露导致数据安全风险

  5. 开启binlog的实时备份,确保RPO(恢复点目标)=0,数据零丢失

  6. 搭建延迟从库,作为应急恢复的核心手段,最小化误操作的影响

  7. 针对超大表,采用分库分表备份,避免单表备份耗时过长,影响一致性

4.2 高频踩坑避坑指南

  1. --single-transaction的DDL陷阱:使用mysqldump --single-transaction备份期间,不能执行DDL操作,否则会导致备份数据不一致,因为DDL会修改表结构,导致一致性快照失效

  2. 备份集未做prepare:XtraBackup备份后,必须执行prepare操作,否则备份集是不一致的,无法正常恢复

  3. binlog格式设置错误:binlog_format设置为STATEMENT或MIXED,导致无法闪回,生产环境必须设置为ROW,binlog_row_image=FULL

  4. 恢复后未修改权限:物理备份恢复后,必须修改数据目录的权限为mysql:mysql,否则MySQL无法启动

  5. 忽略元数据备份:备份时忘记备份存储过程、触发器、事件、用户权限,导致恢复后业务异常

  6. 备份保留时间过短:备份保留时间小于业务误操作的发现时间,导致无法找到有效的备份集恢复

  7. 未冻结写入就执行恢复:误操作后没有冻结业务写入,新的数据覆盖了binlog或数据页,导致恢复失败

五、Java备份管理模块实现

5.1 pom.xml核心依赖

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>
    <groupId>com.jam</groupId>
    <artifactId>mysql-backup-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>mysql-backup-demo</name>
    <description>MySQL Backup Management Demo</description>
    <properties>
        <java.version>17</java.version>
        <mybatis-plus.version>3.5.7</mybatis-plus.version>
        <springdoc.version>2.5.0</springdoc.version>
        <guava.version>33.1.0-jre</guava.version>
        <fastjson2.version>2.0.52</fastjson2.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>${springdoc.version}</version>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.32</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson2.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

5.2 数据库建表语句

复制代码
CREATE TABLE `backup_task` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `task_name` varchar(128) NOT NULL COMMENT '任务名称',
  `backup_type` tinyint NOT NULL COMMENT '备份类型:1-全量逻辑备份,2-全量物理备份,3-增量物理备份',
  `mysql_host` varchar(64) NOT NULL COMMENT 'MySQL主机地址',
  `mysql_port` int NOT NULL DEFAULT '3306' COMMENT 'MySQL端口',
  `mysql_username` varchar(64) NOT NULL COMMENT 'MySQL用户名',
  `mysql_password` varchar(256) NOT NULL COMMENT 'MySQL密码(加密存储)',
  `backup_databases` varchar(512) NOT NULL COMMENT '备份数据库列表,逗号分隔',
  `backup_path` varchar(512) NOT NULL COMMENT '备份文件存储路径',
  `cron_expression` varchar(64) NOT NULL COMMENT '定时任务cron表达式',
  `task_status` tinyint NOT NULL DEFAULT '1' COMMENT '任务状态:0-禁用,1-启用',
  `last_execute_time` datetime DEFAULT NULL COMMENT '上次执行时间',
  `last_execute_status` tinyint DEFAULT NULL COMMENT '上次执行状态:0-失败,1-成功',
  `last_backup_file` varchar(512) DEFAULT NULL COMMENT '上次备份文件路径',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除:0-未删除,1-已删除',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_task_name` (`task_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MySQL备份任务表';

5.3 实体类

复制代码
package com.jam.demo.entity;

import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;

/**
 * 备份任务实体类
 * @author ken
 */
@Data
@TableName("backup_task")
@Schema(description = "备份任务实体")
public class BackupTask {

    @Schema(description = "主键ID")
    @TableId(type = IdType.AUTO)
    private Long id;

    @Schema(description = "任务名称")
    private String taskName;

    @Schema(description = "备份类型:1-全量逻辑备份,2-全量物理备份,3-增量物理备份")
    private Integer backupType;

    @Schema(description = "MySQL主机地址")
    private String mysqlHost;

    @Schema(description = "MySQL端口")
    private Integer mysqlPort;

    @Schema(description = "MySQL用户名")
    private String mysqlUsername;

    @Schema(description = "MySQL密码")
    private String mysqlPassword;

    @Schema(description = "备份数据库列表,逗号分隔")
    private String backupDatabases;

    @Schema(description = "备份文件存储路径")
    private String backupPath;

    @Schema(description = "定时任务cron表达式")
    private String cronExpression;

    @Schema(description = "任务状态:0-禁用,1-启用")
    private Integer taskStatus;

    @Schema(description = "上次执行时间")
    private LocalDateTime lastExecuteTime;

    @Schema(description = "上次执行状态:0-失败,1-成功")
    private Integer lastExecuteStatus;

    @Schema(description = "上次备份文件路径")
    private String lastBackupFile;

    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    @Schema(description = "逻辑删除")
    @TableLogic
    private Integer deleted;
}

5.4 Mapper接口

复制代码
package com.jam.demo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.BackupTask;
import org.apache.ibatis.annotations.Mapper;

/**
 * 备份任务Mapper接口
 * @author ken
 */
@Mapper
public interface BackupTaskMapper extends BaseMapper<BackupTask> {
}

5.5 Service接口与实现

复制代码
package com.jam.demo.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.demo.entity.BackupTask;

/**
 * 备份任务服务接口
 * @author ken
 */
public interface BackupTaskService extends IService<BackupTask> {

    /**
     * 执行备份任务
     * @param taskId 任务ID
     * @return 备份执行结果
     */
    boolean executeBackup(Long taskId);
}

package com.jam.demo.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.demo.entity.BackupTask;
import com.jam.demo.mapper.BackupTaskMapper;
import com.jam.demo.service.BackupTaskService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 备份任务服务实现类
 * @author ken
 */
@Slf4j
@Service
public class BackupTaskServiceImpl extends ServiceImpl<BackupTaskMapper, BackupTask> implements BackupTaskService {

    private final PlatformTransactionManager transactionManager;

    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");

    public BackupTaskServiceImpl(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    @Override
    public boolean executeBackup(Long taskId) {
        if (ObjectUtils.isEmpty(taskId)) {
            log.error("备份任务ID不能为空");
            return false;
        }
        BackupTask backupTask = this.getById(taskId);
        if (ObjectUtils.isEmpty(backupTask)) {
            log.error("备份任务不存在,taskId:{}", taskId);
            return false;
        }
        if (backupTask.getTaskStatus() != 1) {
            log.error("备份任务已禁用,taskId:{}", taskId);
            return false;
        }

        boolean executeResult = false;
        String backupFilePath = "";
        try {
            // 构建备份命令
            String backupCommand = buildBackupCommand(backupTask);
            log.info("开始执行备份任务,taskId:{}, taskName:{}", taskId, backupTask.getTaskName());

            // 执行备份命令
            Process process = Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", backupCommand});
            int exitCode = process.waitFor();

            // 读取错误输出
            BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            StringBuilder errorMsg = new StringBuilder();
            String line;
            while ((line = errorReader.readLine()) != null) {
                errorMsg.append(line).append(System.lineSeparator());
            }
            errorReader.close();

            if (exitCode == 0) {
                log.info("备份任务执行成功,taskId:{}", taskId);
                executeResult = true;
                backupFilePath = buildBackupFileName(backupTask);
            } else {
                log.error("备份任务执行失败,taskId:{}, 错误信息:{}", taskId, errorMsg);
            }
        } catch (Exception e) {
            log.error("备份任务执行异常,taskId:{}", taskId, e);
        } finally {
            // 编程式事务更新任务执行状态
            updateTaskExecuteStatus(taskId, executeResult, backupFilePath);
        }
        return executeResult;
    }

    /**
     * 构建备份命令
     * @param backupTask 备份任务
     * @return 备份命令
     */
    private String buildBackupCommand(BackupTask backupTask) {
        String backupFileName = buildBackupFileName(backupTask);
        StringBuilder command = new StringBuilder();
        // 备份类型:1-全量逻辑备份,2-全量物理备份,3-增量物理备份
        switch (backupTask.getBackupType()) {
            case 1:
                // 全量逻辑备份,mysqldump
                command.append("mysqldump -u").append(backupTask.getMysqlUsername())
                        .append(" -p'").append(backupTask.getMysqlPassword()).append("'")
                        .append(" -h").append(backupTask.getMysqlHost())
                        .append(" -P").append(backupTask.getMysqlPort())
                        .append(" --single-transaction --master-data=2 --routines --triggers --events --hex-blob --default-character-set=utf8mb4")
                        .append(" --databases ").append(backupTask.getBackupDatabases())
                        .append(" | gzip > ").append(backupFileName);
                break;
            case 2:
                // 全量物理备份,xtrabackup
                command.append("xtrabackup --user=").append(backupTask.getMysqlUsername())
                        .append(" --password='").append(backupTask.getMysqlPassword()).append("'")
                        .append(" --host=").append(backupTask.getMysqlHost())
                        .append(" --port=").append(backupTask.getMysqlPort())
                        .append(" --backup --target-dir=").append(backupFileName)
                        .append(" --parallel=4 --compress");
                break;
            case 3:
                // 增量物理备份,需基于上一次备份
                command.append("xtrabackup --user=").append(backupTask.getMysqlUsername())
                        .append(" --password='").append(backupTask.getMysqlPassword()).append("'")
                        .append(" --host=").append(backupTask.getMysqlHost())
                        .append(" --port=").append(backupTask.getMysqlPort())
                        .append(" --backup --target-dir=").append(backupFileName)
                        .append(" --incremental-basedir=").append(backupTask.getLastBackupFile())
                        .append(" --parallel=4 --compress");
                break;
            default:
                throw new IllegalArgumentException("不支持的备份类型");
        }
        return command.toString();
    }

    /**
     * 构建备份文件名
     * @param backupTask 备份任务
     * @return 备份文件完整路径
     */
    private String buildBackupFileName(BackupTask backupTask) {
        String basePath = backupTask.getBackupPath();
        if (!basePath.endsWith("/")) {
            basePath += "/";
        }
        String timeStr = LocalDateTime.now().format(DATE_TIME_FORMATTER);
        return basePath + "mysql_backup_" + backupTask.getTaskName() + "_" + timeStr + (backupTask.getBackupType() == 1 ? ".sql.gz" : "");
    }

    /**
     * 更新任务执行状态
     * @param taskId 任务ID
     * @param executeResult 执行结果
     * @param backupFilePath 备份文件路径
     */
    private void updateTaskExecuteStatus(Long taskId, boolean executeResult, String backupFilePath) {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        TransactionStatus status = transactionManager.getTransaction(def);
        try {
            BackupTask updateTask = new BackupTask();
            updateTask.setId(taskId);
            updateTask.setLastExecuteTime(LocalDateTime.now());
            updateTask.setLastExecuteStatus(executeResult ? 1 : 0);
            if (StringUtils.hasText(backupFilePath)) {
                updateTask.setLastBackupFile(backupFilePath);
            }
            this.updateById(updateTask);
            transactionManager.commit(status);
        } catch (Exception e) {
            transactionManager.rollback(status);
            log.error("更新任务执行状态失败,taskId:{}", taskId, e);
        }
    }
}

5.6 Controller层

复制代码
package com.jam.demo.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.jam.demo.entity.BackupTask;
import com.jam.demo.service.BackupTaskService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 备份任务管理控制器
 * @author ken
 */
@Slf4j
@RestController
@RequestMapping("/api/backup/task")
@Tag(name = "备份任务管理", description = "MySQL备份任务的增删改查与执行管理")
public class BackupTaskController {

    private final BackupTaskService backupTaskService;

    public BackupTaskController(BackupTaskService backupTaskService) {
        this.backupTaskService = backupTaskService;
    }

    @PostMapping
    @Operation(summary = "创建备份任务", description = "新增MySQL备份任务")
    public Boolean createTask(@RequestBody BackupTask backupTask) {
        if (ObjectUtils.isEmpty(backupTask)) {
            return false;
        }
        return backupTaskService.save(backupTask);
    }

    @PutMapping
    @Operation(summary = "更新备份任务", description = "修改备份任务配置")
    public Boolean updateTask(@RequestBody BackupTask backupTask) {
        if (ObjectUtils.isEmpty(backupTask) || ObjectUtils.isEmpty(backupTask.getId())) {
            return false;
        }
        return backupTaskService.updateById(backupTask);
    }

    @DeleteMapping("/{taskId}")
    @Operation(summary = "删除备份任务", description = "逻辑删除备份任务")
    public Boolean deleteTask(@Parameter(description = "任务ID") @PathVariable Long taskId) {
        if (ObjectUtils.isEmpty(taskId)) {
            return false;
        }
        return backupTaskService.removeById(taskId);
    }

    @GetMapping("/{taskId}")
    @Operation(summary = "查询备份任务详情", description = "根据ID查询备份任务信息")
    public BackupTask getTaskDetail(@Parameter(description = "任务ID") @PathVariable Long taskId) {
        if (ObjectUtils.isEmpty(taskId)) {
            return null;
        }
        return backupTaskService.getById(taskId);
    }

    @GetMapping("/page")
    @Operation(summary = "分页查询备份任务列表", description = "分页获取备份任务列表")
    public Page<BackupTask> getTaskPage(
            @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNum,
            @Parameter(description = "每页条数") @RequestParam(defaultValue = "10") Integer pageSize,
            @Parameter(description = "任务名称") @RequestParam(required = false) String taskName) {
        Page<BackupTask> page = new Page<>(pageNum, pageSize);
        LambdaQueryWrapper<BackupTask> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(ObjectUtils.isEmpty(taskName), BackupTask::getTaskName, taskName)
                .orderByDesc(BackupTask::getCreateTime);
        return backupTaskService.page(page, queryWrapper);
    }

    @PostMapping("/execute/{taskId}")
    @Operation(summary = "手动执行备份任务", description = "立即执行指定的备份任务")
    public Boolean executeBackupTask(@Parameter(description = "任务ID") @PathVariable Long taskId) {
        return backupTaskService.executeBackup(taskId);
    }

    @PostMapping("/execute/batch")
    @Operation(summary = "批量执行备份任务", description = "批量执行指定的备份任务")
    public Boolean batchExecuteBackupTask(@Parameter(description = "任务ID列表") @RequestBody List<Long> taskIdList) {
        if (CollectionUtils.isEmpty(taskIdList)) {
            return false;
        }
        for (Long taskId : taskIdList) {
            backupTaskService.executeBackup(taskId);
        }
        return true;
    }
}

5.7 定时任务调度类

复制代码
package com.jam.demo.scheduler;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jam.demo.entity.BackupTask;
import com.jam.demo.service.BackupTaskService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.support.CronExpression;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.time.LocalDateTime;
import java.util.List;

/**
 * 备份任务定时调度器
 * @author ken
 */
@Slf4j
@Component
public class BackupTaskScheduler {

    private final BackupTaskService backupTaskService;

    public BackupTaskScheduler(BackupTaskService backupTaskService) {
        this.backupTaskService = backupTaskService;
    }

    /**
     * 每分钟检查一次待执行的备份任务
     */
    @Scheduled(cron = "0 * * * * ?")
    public void scheduleBackupTask() {
        // 查询所有启用的备份任务
        LambdaQueryWrapper<BackupTask> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(BackupTask::getTaskStatus, 1);
        List<BackupTask> taskList = backupTaskService.list(queryWrapper);
        if (CollectionUtils.isEmpty(taskList)) {
            return;
        }

        LocalDateTime now = LocalDateTime.now();
        for (BackupTask task : taskList) {
            try {
                CronExpression cronExpression = CronExpression.parse(task.getCronExpression());
                LocalDateTime nextExecuteTime = cronExpression.next(task.getLastExecuteTime() == null ? task.getCreateTime() : task.getLastExecuteTime());
                // 判断是否到达执行时间
                if (nextExecuteTime != null && !nextExecuteTime.isAfter(now)) {
                    log.info("定时触发备份任务,taskId:{}, taskName:{}", task.getId(), task.getTaskName());
                    backupTaskService.executeBackup(task.getId());
                }
            } catch (Exception e) {
                log.error("解析备份任务cron表达式异常,taskId:{}, cron:{}", task.getId(), task.getCronExpression(), e);
            }
        }
    }
}

总结

MySQL备份与恢复体系的核心,是"事前预防、事中应急、事后验证"。没有完美的备份方案,只有最贴合业务场景的方案,核心是平衡备份成本、恢复速度、数据安全性三大核心指标。

对于企业而言,最昂贵的不是备份的存储成本,而是数据丢失带来的业务损失。构建一套完善的备份体系,定期执行恢复演练,确保每一份备份都能正常恢复,才是守护MySQL数据安全的根本。

相关推荐
薛定谔的悦2 小时前
BMS Modbus RTU实现:从帧结构到寄存器映射的完整工程
linux·数据库·bms
The_Second_Coming2 小时前
MySQL 5.7 学习笔记
笔记·学习·mysql
light blue bird2 小时前
主从执行端动机模块工序协同组件
jvm·数据库·.net·桌面端
SPC的存折2 小时前
(自用)LNMP-Redis-Discuz5.0部署指南-openEuler24.03-测试环境
linux·运维·服务器·数据库·redis·缓存
二等饼干~za8986682 小时前
云罗 GEO 优化系统源码厂家测评报告
大数据·网络·数据库·人工智能·django
堕落年代2 小时前
Spring 事务提交顺序深度解析:从踩坑到理解原理
数据库·spring·oracle
xcjbqd03 小时前
Python中Pandas如何将DataFrame写入MySQL_使用to_sql函数
jvm·数据库·python
ZOOOOOOU3 小时前
智慧社区云对讲门禁系统架构设计:中优云联免布线、全免费核心功能技术解析
数据库·人工智能·架构·边缘计算
yzp-3 小时前
Spring 三级缓存 ---- 简单明了豆包版
java·mysql·spring