前言
数据是企业业务的核心资产,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文本等逻辑格式,写入备份文件。 核心实现原理:
-
与MySQL建立客户端连接,通过SQL接口获取表结构、数据、存储过程、触发器、事件等元数据
-
对InnoDB引擎,在RR隔离级别下开启事务,生成一致性快照,全程无锁备份
-
逐行读取表数据,转换成INSERT语句或结构化文本,写入备份文件
-
记录备份时刻的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 全量热备实战
底层原理:
-
启动备份时,启动一个后台线程持续监控并复制redo log,确保备份期间所有数据变更都被记录
-
并行复制InnoDB的.ibd表空间文件与共享表空间
-
数据文件复制完成后,停止redo log复制,执行FLUSH TABLES WITH READ LOCK,复制MyISAM表与系统表元数据
-
解锁表,记录备份时刻的binlog位置与LSN,生成备份元数据文件
-
整个过程中,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 增量备份实战
增量备份仅备份上一次备份后变更的数据页,备份速度极快,磁盘占用极小,是生产环境核心的备份补充手段。
增量备份完整流程实例:
-
周日凌晨执行全量基础备份
xtrabackup --user=root --password='Your_Strong_Password' --backup --target-dir=/backup/xtrabackup_full_base
-
周一凌晨执行第一次增量备份,基于周日的全量备份
xtrabackup --user=root --password='Your_Strong_Password'
--backup
--target-dir=/backup/xtrabackup_incr_1
--incremental-basedir=/backup/xtrabackup_full_base
--incremental-basedir:指定上一次备份的目录,增量备份仅备份该目录之后变更的数据。
-
周二凌晨执行第二次增量备份,基于周一的增量备份
xtrabackup --user=root --password='Your_Strong_Password'
--backup
--target-dir=/backup/xtrabackup_incr_2
--incremental-basedir=/backup/xtrabackup_incr_1
增量备份集预处理与恢复: 增量备份的prepare操作和全量备份不同,分为两个阶段:
-
先对基础全量备份执行prepare,仅应用redo log,不回滚未提交事务(--apply-log-only)
xtrabackup --prepare --apply-log-only --target-dir=/backup/xtrabackup_full_base
-
合并第一次增量备份到基础全量备份
xtrabackup --prepare --apply-log-only
--target-dir=/backup/xtrabackup_full_base
--incremental-dir=/backup/xtrabackup_incr_1 -
合并第二次增量备份到基础全量备份(最后一次增量合并,去掉--apply-log-only,执行事务回滚)
xtrabackup --prepare
--target-dir=/backup/xtrabackup_full_base
--incremental-dir=/backup/xtrabackup_incr_2 -
合并完成后,/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 恢复前置准备(事前必须做好)
没有这些准备,误删后恢复难度极大,甚至无法恢复:
-
必须开启binlog,格式为ROW,binlog_row_image=FULL
-
落地完整的备份策略,定期验证备份集可用性
-
搭建1小时延迟从库,这是秒级恢复的核心神器
-
部署binlog闪回工具(binlog2sql、MyFlash)
-
开启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,实现数据恢复。
恢复完整流程实例:
-
立即冻结业务写入,避免新的操作覆盖binlog,可通过设置只读锁:
FLUSH TABLES WITH READ LOCK;
-- 注意:不要退出当前会话,退出后锁自动释放 -
查找误操作对应的binlog文件与位置:
-- 查看当前所有binlog文件
SHOW BINARY LOGS;
-- 查看最近的binlog事件,定位误操作的时间点与position
SHOW BINLOG EVENTS IN 'mysql-bin.000123' LIMIT 100; -
使用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 -
校验flashback_restore.sql中的SQL语句,确认无误后执行恢复:
mysql -uroot -p'Your_Strong_Password' db1 < flashback_restore.sql
-
验证数据恢复完成后,解锁只读锁,恢复业务写入:
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;
恢复完整流程:
-
主库发生误删表操作后,立即登录延迟从库,停止SQL线程:
STOP REPLICA SQL_THREAD;
-
查找误操作的GTID或position,确认事务还未在从库执行:
-- 查看从库复制状态,确认延迟时间
SHOW REPLICA STATUS\G
-- 查看中继日志中的事件,定位误操作的GTID
SHOW RELAYLOG EVENTS LIMIT 100; -
跳过误操作的事务,启动SQL线程,同步到误操作之前的时间点:
-- 基于GTID跳过误操作事务(推荐)
SET GTID_NEXT='aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:12345';
BEGIN;
COMMIT;
SET GTID_NEXT='AUTOMATIC';-- 启动SQL线程,同步剩余正常事务
START REPLICA SQL_THREAD; -
等待从库同步完成后,将被误删的表从从库导出,导回主库,完成恢复,全程分钟级完成,几乎无数据丢失。
3.3.2 备选方案:全量备份+binlog时间点恢复
如果没有延迟从库,使用该方案,核心是恢复到误操作之前的时间点。
恢复流程:
-
找到误操作之前最近的全量备份,执行全量恢复,恢复到备份时间点
-
基于备份文件中记录的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
恢复实例:
-
查看回收站中的被删除表:
SELECT * FROM information_schema.INNODB_RECYCLE_BIN;
-
执行恢复命令,将回收站中的表恢复到原库:
-- 语法:CALL sys.innodb_recycle_bin_restore('回收站表名', '恢复后的表名');
CALL sys.innodb_recycle_bin_restore('#sql-ib123-456789', 'user');
执行完成后,表即可恢复,包含所有数据,全程秒级完成。
3.4 场景三:表空间损坏/数据文件误删
这类故障是物理层面的损坏,比如rm -rf误删了.ibd文件,磁盘故障导致表空间损坏,核心恢复方案是物理备份恢复,或者InnoDB表空间导入。
单表空间导入恢复实例:
-
找到该表的建表语句,在新的实例中创建相同的表
CREATE TABLE user (
id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(64) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -
丢弃新表的表空间:
ALTER TABLE user DISCARD TABLESPACE;
-
将备份的.ibd文件复制到新表的数据目录,修改权限为mysql:mysql
-
导入表空间:
ALTER TABLE user IMPORT TABLESPACE;
执行完成后,单表数据即可完整恢复。
误删数据恢复流程总览:

四、备份与恢复最佳实践与避坑指南
4.1 备份核心最佳实践
-
备份必须异地存储,绝对不能和数据库放在同一台服务器、同一个机房,避免机房故障、勒索攻击导致备份与数据同时丢失
-
备份后必须执行校验,不仅要校验备份文件的完整性,还要定期执行恢复演练,能成功恢复的备份才是有效的备份
-
优先在从库执行备份,避免备份操作占用主库的CPU、IO资源,影响业务性能
-
备份集必须加密,避免备份文件泄露导致数据安全风险
-
开启binlog的实时备份,确保RPO(恢复点目标)=0,数据零丢失
-
搭建延迟从库,作为应急恢复的核心手段,最小化误操作的影响
-
针对超大表,采用分库分表备份,避免单表备份耗时过长,影响一致性
4.2 高频踩坑避坑指南
-
--single-transaction的DDL陷阱:使用mysqldump --single-transaction备份期间,不能执行DDL操作,否则会导致备份数据不一致,因为DDL会修改表结构,导致一致性快照失效
-
备份集未做prepare:XtraBackup备份后,必须执行prepare操作,否则备份集是不一致的,无法正常恢复
-
binlog格式设置错误:binlog_format设置为STATEMENT或MIXED,导致无法闪回,生产环境必须设置为ROW,binlog_row_image=FULL
-
恢复后未修改权限:物理备份恢复后,必须修改数据目录的权限为mysql:mysql,否则MySQL无法启动
-
忽略元数据备份:备份时忘记备份存储过程、触发器、事件、用户权限,导致恢复后业务异常
-
备份保留时间过短:备份保留时间小于业务误操作的发现时间,导致无法找到有效的备份集恢复
-
未冻结写入就执行恢复:误操作后没有冻结业务写入,新的数据覆盖了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数据安全的根本。