半夜两点被电话叫醒,第一句话是“核心表被清了,能恢复吗?”——做过数据库运维的人,大概都经历过这种心跳骤停的时刻。而那一瞬间,你翻遍备份目录能找到的东西,往往就是一份 mysqldump 导出的 SQL 文件。它是最古老、最朴素、也最可靠的备份手段。mysqldump 可以用一行命令把整个数据库变成文本,也可以精准到只导出一张表的某个 WHERE 条件下的数据。但很多人对它的认知停留在“备份数据库”这一步,遇到多库、增量、主从、压缩、跨机迁移这类高频场景就不知道该加什么参数。这篇内容我就把自己这些年用 mysqldump 处理过的 12 个高频场景完整拆解一遍,从参数原理到恢复验证,全部按实操流程来,适合 DBA、后端开发、运维以及所有自己管数据库的独立开发者。
1. mysqldump 的备份机制:搞清楚它产出的到底是什么
很多人在用 mysqldump 之前,根本没想过它到底是怎么工作的。简单说,mysqldump 是一个逻辑备份工具,它不直接拷贝数据库文件,而是通过客户端协议连接 MySQL,把表结构、数据、视图、触发器、存储过程等信息转换成一条条 SQL 语句,输出到一个文本文件里。恢复的时候,只要把这个文件喂给 mysql 客户端重新执行一遍,就能重建出库和表,并插入数据。
这就带来了 mysqldump 的两大特点:兼容性好,SQL 是标准文本,换 MySQL 版本、换云厂商、换操作系统都能用;速度相对物理备份慢,因为导出和导入都是逐条执行 SQL,数据量越大幅度越明显。所以对于几十 GB 以内的库,mysqldump 是首选;对于几百 GB 甚至 TB 级以上的库,通常要评估 Percona XtraBackup 这类物理备份工具,或者考虑别的方案。
这里有一个关键概念必须搞清楚:一致性快照。
如果你在业务高峰期直接执行 mysqldump 而不加任何事务参数,导出的数据很可能是不一致的。比如 A 表和 B 表有关联关系,导出 A 表时用户还没下单,导出 B 表时用户刚下单成功,备份出来的 A、B 数据就对不上。InnoDB 引擎下解决这个问题靠的是 --single-transaction 参数,它在导出开始时开启一个 REPEATABLE READ 事务,利用 MVCC 多版本控制机制,让 mysqldump 看到的是事务开启那一刻的数据库快照,后续业务写入的数据不会影响本次导出内容。加了这个参数之后,备份期间不需要锁表,线上业务可以正常读写。
MyISAM 引擎的表不支持事务,--single-transaction 对它无效。默认情况下 mysqldump 对 MyISAM 表会使用 --lock-tables,也就是在导出某个库的所有表时,先对要导出的表批量加 READ LOCAL 锁,导完再释放。如果你用混合引擎的表,InnoDB 表靠事务保证一致性,MyISAM 表靠锁表保证一致性,两者混用要注意。
另一个容易踩坑的地方是 binlog。mysqldump 导出的是导出时刻的快照数据,从备份完成到数据库故障发生之间的增量变化,它管不了。所以在真实的生产环境里,物理全量备份、逻辑全量备份和 binlog 增量备份是配合使用的。mysqldump 从来不是单纯的“备份工具”,它更像是一个数据快照采集器,负责把某一时刻的数据固化成文本,方便你随时回放。
搞清楚了这一点,后面所有场景的参数选择就都有了依据:你需要一致性的快照就开 --single-transaction,你需要跨机器的可移植文件就用默认的 SQL 文本格式,你需要搭建从库就加 --master-data,这些不是玄学,是底层机制决定的合理选择。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与参数速查:先跑通最小可用命令
2.1 连接参数和权限要求
mysqldump 本质上是一个客户端工具,连接参数和 mysql 客户端一模一样。最常见的写法是:
bash复制mysqldump -h127.0.0.1 -P3306 -uroot -p --single-transaction testdb > /data/backup/testdb.sql
-h指定主机,如果是本地连接也可以省略;-P指定端口,默认 3306;-u指定用户;-p指定密码,注意-p和密码之间不能有空格,直接写成-p你的密码,或者只写-p然后回车,交互式输入密码。实际用脚本做定时任务时,推荐把密码写进~/.my.cnf配置文件的[client]段,避免密码出现在进程列表里。
权限方面,mysqldump 至少需要以下权限:
| 权限 | 用途 |
|---|---|
| SELECT | 读取表数据 |
| SHOW VIEW | 读取视图定义 |
| TRIGGER | 读取触发器定义 |
| LOCK TABLES | 对 MyISAM 表加锁(如果用 --single-transaction 且所有表都是 InnoDB,可能不需要,但建议保留) |
| RELOAD | 使用 --flush-logs 或 --master-data 时刷新日志 |
| PROCESS | 使用 --master-data 时获取二进制日志位置信息 |
如果你用 root 账号做备份,这些权限天然就有,但生产环境的最佳实践是单独创建一个备份账号,最小权限原则:
sql复制CREATE USER 'backup'@'localhost' IDENTIFIED BY '备份密码';
GRANT SELECT, SHOW VIEW, TRIGGER, LOCK TABLES, RELOAD, PROCESS ON *.* TO 'backup'@'localhost';
2.2 核心参数速查表
我整理了一份日常最常用的参数速查表,按使用频率排序,方便你随时对照:
| 参数 | 作用 | 典型使用场景 |
|---|---|---|
--single-transaction |
InnoDB 一致性快照,不锁表 | 在线备份 InnoDB 表 |
--master-data=2 |
输出 CHANGE MASTER TO 语句并注释,附带 binlog 位置 | 搭建从库、做增量备份起点 |
--source-data=2 |
MySQL 8.0.26 之后 --master-data 的别名 |
新版本搭建从库 |
--set-gtid-purged=OFF |
控制导出文件是否包含 GTID_PURGED 信息 | 跨集群恢复、避免 GTID 冲突 |
--databases |
指定多个数据库,导出文件包含 CREATE DATABASE 和 USE | 多库备份 |
--all-databases |
导出全部数据库 | 全库迁移 |
--no-data |
只导出表结构,不导出数据 | 环境初始化、建表脚本 |
--no-create-info |
只导出数据,不导出建表语句 | 数据迁移 |
--where='条件' |
按条件过滤行 | 导出部分数据 |
--tables |
指定导出表 | 单表备份 |
--opt |
默认开启的一组优化参数 | 常规备份 |
--compress |
客户端与服务器间压缩传输 | 跨网络备份 |
--hex-blob |
二进制字段以十六进制输出 | 避免 BLOB 乱码 |
--column-statistics=0 |
不导出列统计信息 | 从 MySQL 8.0 导出并导入低版本或部分云数据库 |
--opt 这一个参数很值得单独说。它在旧版本里叫 --opt,实际上是一组参数的快捷方式,包含 --add-drop-table(导出前先 DROP 已有的同名表)、--add-locks(每个表导出前后加 LOCK TABLES 和 UNLOCK TABLES)、--create-options(保留建表时的 ENGINE、CHARSET 等信息)、--extended-insert(多行合并成一条 INSERT)、--quick(逐行读取,避免一次性加载到内存)、--lock-tables(导出前锁表)等。MySQL 5.7 之后默认开启了 --opt,所以你直接执行 mysqldump,实际使用的就是这一套优化组合。了解这一点能帮你理解为什么不同版本的导出文件格式会有差异。
3. 12 个高频场景逐一拆解:命令、参数和背后的逻辑
3.1 场景 1:单库全量备份——最基础的保命操作
单库备份是所有人都会遇到的第一课。命令长这样:
bash复制mysqldump -uroot -p --single-transaction --set-gtid-purged=OFF testdb > /data/backup/testdb_$(date +%F).sql
这里为什么加 --single-transaction 前面讲过,是为了拿到一致性快照。--set-gtid-purged=OFF 是很多人在 MySQL 5.7 及以上版本导入时踩坑的源头,先解释一下:开启 GTID 的实例会在导出文件里自动加上 SET @@GLOBAL.GTID_PURGED 语句,如果导入的目标库也开了 GTID,且历史上从没执行过这条语句,导入时就会报错,内容大致是 GTID_PURGED can only be set when GTID_EXECUTED is empty。对普通备份恢复来说,我们绝大多数情况下不关心 GTID 信息,直接关掉最省事。
单库备份还有一个常见需求:导出后要能直接恢复到另一个库。默认导出的文件里没有 CREATE DATABASE 语句,恢复时你需要先手动建库再导入。如果你希望文件自带建库语句,恢复时不用预先建库,那就加 --databases:
bash复制mysqldump -uroot -p --single-transaction --databases testdb > /data/backup/testdb_with_create.sql
加了 --databases 之后,导出文件里会多出 CREATE DATABASE testdb 和 USE testdb 两行,恢复的时候直接整体导入就行。我个人建议:如果只是备份某几个业务库,习惯性加上 --databases,让备份文件更完整,恢复时更省脑子。
3.2 场景 2:多库同时导出——一条命令解决多个库
遇到要备份多个库的时候,不用写脚本循环,直接在命令后面列库名就行:
bash复制mysqldump -uroot -p --single-transaction --databases db1 db2 db3 > /data/backup/multiple_dbs.sql
注意这里 --databases 后面跟的库数量没有限制,但所有库名必须跟在参数后面。导出文件的特征依然是包含每个库的 CREATE DATABASE 和 USE 语句,恢复时同样是一条命令全部搞定。
多库导出和单库导出的细微差别在于:如果不加 --databases,即便你写了多个库名,mysqldump 也只会导出第一个库;加了 --databases 之后才会把每个库都导出来。这个细节值得记一下,不然脚本里很容易出现“备份了 3 个库,实际只有第一个库有数据”的尴尬。
3.3 场景 3:全库备份——含系统库的完整镜像
全库备份的命令:
bash复制mysqldump -uroot -p --single-transaction --all-databases --set-gtid-purged=OFF > /data/backup/all_databases.sql
这个方案会导出所有数据库,包括 mysql、sys、performance_schema、information_schema 这些系统库。好处是备份完整,恢复后直接就是一个可用的实例;坏处是文件体积大,恢复时耗时长。
这里要提醒一点:information_schema 和 performance_schema 里的数据是运行时的动态信息,mysqldump 实际上不会导出这两个库的业务数据,performance_schema 的表在恢复时通常只会重建空表结构,information_schema 甚至不会出现在导出文件里。所以全库备份的实际组成是“所有业务库 + mysql 系统库 + sys 库”。如果你只是想迁移业务数据,全库备份并不是最优解,反而可能因为 mysql 库里的授权账号信息、历史日志表内容带来额外的不确定性。但如果你要复制整个实例、做灾难恢复演练,全库备份是最省心的方案。
3.4 场景 4:单表备份——只导出你关心的那张表
当某张表特别大、备份全库太耗时,或者你只需要某一张表的数据做分析时,可以只导出这张表:
bash复制mysqldump -uroot -p --single-transaction testdb orders --where="create_time >= '2024-01-01'" > /data/backup/orders_2024.sql
这里需要特别注意:单表导出时,库名后面直接跟表名,不需要 --tables 参数,mysqldump 会把第一个参数当作库名,把库名后面跟着的参数当作表名。如果加了 --databases,反而会把表名识别成库名,导致报错。
单表备份的恢复同样很直接:
bash复制mysql -uroot -p testdb < /data/backup/orders_2024.sql
目标库里如果没有这张表,会自动创建;如果已有同名表,--opt 默认的 --add-drop-table 会先 DROP 掉旧表再重建。所以误恢复同名表会覆盖当前数据,执行导入前务必确认方向和目标。
3.5 场景 5:按条件导出——用 WHERE 过滤你真正需要的数据
上面的命令里已经用到了 --where,这是做部分数据导出时最灵活的方案。常见用法:
bash复制# 导出指定日期范围的订单
mysqldump -uroot -p testdb orders --where="create_time >= '2024-01-01' AND create_time < '2024-02-01'" > /data/backup/orders_202401.sql
# 导出指定用户的数据
mysqldump -uroot -p testdb user_behavior --where="user_id IN (1001, 1002, 1003)" > /data/backup/user_behavior_special.sql
# 导出分页区间数据(比如大数据量抽样)
mysqldump -uroot -p testdb logs --where="id >= 1000000 AND id <= 2000000" > /data/backup/logs_part.sql
--where 的语法就是普通 SQL WHERE 子句,支持所有你能写的条件表达式。但有一个性能问题要提前注意:mysqldump 执行 --where 时,会全程扫描整张表,逐行判断条件是否满足,不会用到你那个看似很精准的索引。表越大越慢,因为它本质上是把整张表 SELECT 出来数据后过滤,而不是走索引只读需要的数据。所以条件导出的建议是:仅在特殊需求下使用,日常备份还是走全表更稳妥。
3.6 场景 6:只备份表结构——初始化环境、迁移表 DDL
建测试环境、给开发同事一套不含数据的表结构脚本,这类场景需要 --no-data:
bash复制mysqldump -uroot -p --no-data testdb > /data/backup/testdb_schema.sql
这个参数会把建库、建表、视图、触发器、存储过程、事件等结构信息完整导出,但一条 INSERT 都不会有。实测下来,这个文件对表多、结构复杂的库特别有价值,可以作为基线结构脚本,用于对比环境间的表结构差异:
bash复制diff testdb_schema.sql production_schema.sql
另外,如果你想限制只导出某些表的结构,可以用 --tables 加表名列表:
bash复制mysqldump -uroot -p --no-data testdb --tables orders users > /data/backup/orders_users_schema.sql
注意这里一旦使用 --tables,库名后面接表名的顺序和上面场景 4 的顺序完全一致。
3.7 场景 7:只备份数据——迁移数据到同构环境
和上面相反,如果目标环境已经有表结构,只需要把数据搬过去,用 --no-create-info 就行:
bash复制mysqldump -uroot -p --no-create-info --single-transaction testdb > /data/backup/testdb_data_only.sql
导出的文件里不包含 CREATE TABLE,只有 INSERT 语句和锁表语句。恢复时目标表必须提前存在,字段结构必须兼容,否则插入会失败。
这个参数在做“把测试环境数据同步到另一个测试环境”“按周导出业务明细数据”这类场景非常实用。不过我遇到过一个坑:如果目标表结构比源表多了一些非空字段且没有默认值,导入会立刻报错,所以执行前一定要先比对结构。
3.8 场景 8:压缩备份——给磁盘和网络双重减负
数据库文本文件有一个特点:压缩率很高,通常能压到原来的 1/10 左右。逻辑备份产出的是 SQL 文本,重复性内容多,压缩后体积大幅减小。常见组合是 mysqldump 和 gzip 用管道相连:
bash复制mysqldump -uroot -p --single-transaction testdb | gzip > /data/backup/testdb_$(date +%F).sql.gz
恢复的时候先解压再导入:
bash复制gunzip < /data/backup/testdb_$(date +%F).sql.gz | mysql -uroot -p testdb
或者一步到位使用 zcat:
bash复制zcat /data/backup/testdb_$(date +%F).sql.gz | mysql -uroot -p testdb
gzip 压缩等级默认是 6,实测大多数场景下已经足够。追求更高压缩率可以用 gzip -9,但压缩时间会明显增加。这里有个选型建议:如果备份机磁盘充足,我更倾向于先用 gzip 默认级别;如果网络传输是瓶颈,就提到 -9 或者用 zstd 替代:
bash复制mysqldump -uroot -p --single-transaction testdb | zstd -q -3 -o /data/backup/testdb_$(date +%F).sql.zst
zstd 的压缩速度和解压速度都比 gzip 更好,但从通用性角度,gzip 依然是所有 Linux 服务器的标配,交付给别人的备份文件,用 gzip 永远没错。
3.9 场景 9:跨服务器迁移——管道直传,无需中间文件
把 A 机的库迁移到 B 机,最直接的方式是走管道,不需要在 A 机留一份文件再到 B 机导入:
bash复制mysqldump -hA机地址 -uroot -p --single-transaction --databases testdb | mysql -hB机地址 -uroot -p testdb
数据不落地,少了一次磁盘读写,实测对中断网环境最友好。但需要确认两点:A 机能连通 B 机的 MySQL 端口,且两边 MySQL 版本、字符集兼容,最好是同大版本。
如果不放心管道直传,也可以分两步走,先导出再导入,这样中间多一个文件方便排查问题:
bash复制# 第一步:源机导出
mysqldump -h源机 -uroot -p --single-transaction --databases testdb > /tmp/testdb.sql
# 第二步:拷贝到目标机后导入
mysql -h目标机 -uroot -p < /tmp/testdb.sql
3.10 场景 10:为主从复制准备一致性快照
搭建主从复制时,需要先拿到主库的一个一致性的数据快照,同时还要知道这个快照对应的 binlog 位置。--master-data=2 就是专门干这个的:
bash复制mysqldump -uroot -p --single-transaction --master-data=2 --databases testdb > /data/backup/testdb_masterdata.sql
执行之后,导出文件里会多出两行类似这样的注释:
sql复制-- CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000123', MASTER_LOG_POS=456789;
=2 的意思是把这条 CHANGE MASTER 语句作为注释写进文件,方便你做从库搭建时手动读取位置;如果写成 =1,它就不是注释,而是会被直接执行的语句。
在 GTID 模式下,--master-data 还会自动带上 GTID 信息。这时候从库执行完备份导入后,需要设置 MASTER_AUTO_POSITION=1 来同步,而不是手动填 binlog 文件名和位置。
这里提醒一下:MySQL 8.0.26 开始,官方把 --master-data 重命名成了 --source-data,旧参数依然可用但会有弃用警告。新写的脚本建议直接用 --source-data=2。
有了这个快照,从库搭建步骤就变成:导入数据文件 → 配置复制源 → START SLAVE / START REPLICA。整个过程的核心价值就是:你拿到的不只是一份数据,还有数据对应的 binlog 位置,主从从这里开始同步,两侧数据就能准确对接。
3.11 场景 11:分库分表的循环备份脚本
生产环境通常不止一个库,这时候手动敲命令就不现实了,写一个循环脚本把库和表都遍历一遍:
bash复制#!/bin/bash
# /opt/scripts/backup_mysql.sh
BACKUP_DIR="/data/backup/mysql"
MYSQL_USER="root"
MYSQL_PASSWORD="你的密码"
MYSQL_HOST="127.0.0.1"
MYSQL_PORT="3306"
DATE=$(date +%F)
KEEP_DAYS=7
mkdir -p "${BACKUP_DIR}/${DATE}"
# 获取所有业务库,排除系统库
databases=$(mysql -h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER} -p${MYSQL_PASSWORD} -e "SHOW DATABASES;" | grep -Ev "^(Database|information_schema|performance_schema|mysql|sys)$")
for db in $databases; do
# 每个库单独备份
mysqldump -h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER} -p${MYSQL_PASSWORD} \
--single-transaction --set-gtid-purged=OFF --databases "${db}" \
| gzip > "${BACKUP_DIR}/${DATE}/${db}.sql.gz"
# 可选:每个库的大表单独备份
large_tables=$(mysql -h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER} -p${MYSQL_PASSWORD} -e "SELECT table_name FROM information_schema.tables WHERE table_schema='${db}' AND (data_length + index_length) > 10737418240;")
for tbl in $large_tables; do
mysqldump -h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER} -p${MYSQL_PASSWORD} \
--single-transaction --set-gtid-purged=OFF "${db}" "${tbl}" \
| gzip > "${BACKUP_DIR}/${DATE}/${db}_${tbl}.sql.gz"
done
done
# 清理超过保留天数的备份文件
find "${BACKUP_DIR}" -type d -name "20*" -mtime +${KEEP_DAYS} -exec rm -rf {} \;
echo "$(date '+%Y-%m-%d %H:%M:%S') backup completed" >> /var/log/mysql_backup.log
这个脚本的核心思路是“分库存放,按天归档,自动清理”。我把超过 10GB 的大表单独摘出来备份,是因为全库备份文件过大的时候,恢复时太痛苦,大表和小表分开管理,后续想单独恢复某张表时也更灵活。
脚本里的保留策略 KEEP_DAYS=7 要根据业务容忍度调整,核心库建议保留至少 30 天。
3.12 场景 12:定时备份与保留策略
Linux 上做定时任务用的是 crontab。结合上面的脚本,定时任务只需要一行:
cron复制0 2 * * * /bin/bash /opt/scripts/backup_mysql.sh >> /var/log/mysql_backup.log 2>&1
每天晚上 2 点执行一次备份,日志输出到单独文件。选择凌晨 2 点是因为大多数业务系统此时访问量最低,备份对线上 IO 的冲击最小。
如果你的业务是 7x24 小时在线,且访问量没有明显低谷,就需要评估备份对磁盘 IO 的冲击。mysqldump 逻辑备份的读操作在数据量大的时候也会产生不小的 IO 压力,建议观察线上 IO 情况,必要时用 ionice -c3 降低备份进程的 IO 优先级:
bash复制ionice -c3 mysqldump -uroot -p --single-transaction testdb | gzip > /data/backup/testdb.sql.gz
这里还牵扯一个保留策略的设计思路。我见过不少团队,每天全量备份,但备份文件永远只留最近 3 天,一旦第 4 天数据出问题,就只能用“全量 + binlog”去恢复,恢复链路长且容易出错。比较稳妥的做法是:每天全量备份,保留至少 7 天,每周抽一个备份挪到归档目录,保留一个月,每月再抽一个保留一年。磁盘成本是多了一点,但关键时刻它能救你命。
4. 备份只是开始,恢复才是关键
4.1 恢复数据核心三步
mysqldump 只负责导出,导入的工作交给 mysql 客户端。完整恢复流程如下:
bash复制# 第一步:确认目标库存在
mysql -uroot -p -e "CREATE DATABASE IF NOT EXISTS testdb DEFAULT CHARACTER SET utf8mb4;"
# 第二步:执行导入
mysql -uroot -p testdb < /data/backup/testdb.sql
# 第三步:验证数据
mysql -uroot -p -e "USE testdb; SELECT COUNT(*) FROM orders;"
如果备份文件是 gzip 压缩格式,用 gunzip 或 zcat 解压后送入 mysql:
bash复制zcat /data/backup/testdb_$(date +%F).sql.gz | mysql -uroot -p testdb
如果是全库备份或带 --databases 导出的文件,导入时不需要指定库名,直接用 mysql -uroot -p < all_databases.sql 即可。
4.2 导入前的检查清单
恢复操作最怕的就是“倒完了才发现导错了库”。我每次执行前都会过一遍下面的检查项:
- 确认目标环境和源环境的字符集是否一致,特别是
utf8mb4和utf8混用容易导致中文字符乱码,可以在导出时用--default-character-set=utf8mb4强制指定。 - 确认目标库表空间是否足够,
df -h看磁盘剩余用量,导入文件大小翻倍的情况很常见。 - 确认没有外键约束冲突,如果导入时报 FOREIGN KEY 错误,可以先在会话里执行
SET FOREIGN_KEY_CHECKS=0;再导入,不过要在同一个会话里执行,也就是使用管道方式把两个语句串联:bash复制或者用管道方式:mysql -uroot -p testdb -e "SET FOREIGN_KEY_CHECKS=0; SOURCE /data/backup/testdb.sql;"bash复制(echo "SET FOREIGN_KEY_CHECKS=0;"; cat /data/backup/testdb.sql) | mysql -uroot -p testdb - 确认大字段和特殊字符是否完整,备份文件里如果包含大量二进制内容,建议导出时加
--hex-blob,恢复时能避免 NULL 字节和特殊字符被终端或 sql_mode 干扰。
4.3 数据一致性验证
恢复完成后,光有 COUNT(*) 还不够。我会核对表行数、关键表的业务最大值、增量数据的边界等:
sql复制-- 对比源库和目标库的表行数
SELECT table_name, table_rows FROM information_schema.tables WHERE table_schema='testdb';
-- 核对核心业务字段
SELECT MAX(order_id), MAX(create_time) FROM testdb.orders;
这两个检查只能说明数据大体完整。如果业务上有对账表、流水表这类明细数据,建议抽样对比几个关键字段,确认导出导入过程中没有出现字符转换错误或浮点精度丢失。
5. 真实踩坑记录:这些问题我遇到过,希望你别再踩
5.1 默认参数导致的锁表事故
我第一次用 mysqldump 做线上备份,没加 --single-transaction,直接跑了默认命令。结果备份期间 MyISAM 表被 LOCK TABLES 锁住,线上订单写入全部阻塞,持续了几分钟才结束。那次之后我养成了习惯:所有 InnoDB 表的逻辑备份,一律加 --single-transaction;遇到 MyISAM 表,评估是否先切换到 InnoDB 再备份,因为 MyISAM 表在备份期间无法避免锁表。
排查这个问题时,最直观的现象就是 SHOW PROCESSLIST; 里出现大量 Waiting for table lock 状态的会话。
5.2 max_allowed_packet 导致的导入失败
备份文件导出正常,迁移到另一台机器导入时反复报错,常见报错是:
code复制ERROR 2006 (HY000) at line 1234: MySQL server has gone away
ERROR 1153 (08S01) at line 1234: Got a packet bigger than 'max_allowed_packet' bytes
原因是 mysqldump 默认开启了 --extended-insert,会把多行数据合并成一条超大 INSERT 语句,如果这条 INSERT 超过目标库的 max_allowed_packet,连接就被直接掐断。解决方式是在导入前把目标库的参数调大:
sql复制SET GLOBAL max_allowed_packet = 1073741824;
或者在连接命令里显式指定:
bash复制mysql -uroot -p --max-allowed-packet=1G testdb < /data/backup/testdb.sql
备份端也可以提前用 --max-allowed-packet=1G 参数控制导出 SQL 的单条大小,从源头规避这个问题。我的经验是:数据量大的表,这个坑几乎是必踩的,所以备份脚本和恢复方案里都应该把 max_allowed_packet 写上。
5.3 字符集乱码
导出的 SQL 文件在另一台机器导入后,中文全部变成问号。排查过程最后定位到:两边数据库字符集不一致,导出时没有用 --default-character-set=utf8mb4,而目标库默认字符集是 utf8mb3。
这里要特别说明:utf8mb3 是 MySQL 8.0 之前 utf8 的别名,它最多只能存储 3 字节的字符,像 emoji 这类 4 字节字符在旧字符集下会直接丢字或报错。解决方法是保证两端会话字符集一致,都在连接参数里指定 --default-character-set=utf8mb4,同时确认表和库的字符集也是 utf8mb4。
bash复制mysqldump -uroot -p --default-character-set=utf8mb4 --single-transaction testdb | gzip > testdb.sql.gz
5.4 GTID 冲突
MySQL 5.7 以上开启 GTID 后,从备份文件导入数据到另一个已开启 GTID 的实例,最常见的报错是:
code复制ERROR 3546 (HY000) at line 24: @@GLOBAL.GTID_PURGED cannot be changed: the added gtid values must not overlap with @@GLOBAL.GTID_EXECUTED
原因是导出文件里带上了 SET @@GLOBAL.GTID_PURGED 的语句,而目标库自身已经执行过事务,GTID 集合不为空,两边有重叠或追加顺序冲突。解决方法是对普通备份恢复场景,导出时统一加 --set-gtid-purged=OFF,完全不输出 GTID 信息。
但有一个例外:搭建从库时,恰恰需要 GTID 信息保证主从位置的对接,这时候不能用 OFF,而是保留默认的 AUTO,让 mysqldump 根据源实例的 GTID 状态自动生成 GTID_PURGED 语句。所以我在不同场景下对 GTID 参数的选择是:普通备份和跨环境迁移一律 OFF,搭从库用 AUTO。
5.5 磁盘空间估算失误
备份文件在导出和压缩过程里,磁盘占用的变化曲线很容易被忽略。mysqldump 直接输出未压缩 SQL 时,文件大小大约是数据库实际数据量的 1.2 到 1.5 倍,因为文本格式要占到额外的存储开销。如果磁盘剩余空间只比数据库稍大一点,导出到一半就可能写满磁盘,导致备份文件损坏。
所以我归档备份时,固定采用“边导出边压缩”的方案,mysqldump 输出通过管道直接进 gzip,压缩后的文件通常只有原始数据的 10% 到 20%,对磁盘压力小得多。脚本里还可以加一段磁盘检查逻辑:
bash复制disk_free=$(df /data/backup | awk 'NR==2 {print $4}')
echo "backup directory free space: ${disk_free} KB"
备份文件写完后,我还会手动校验一下 gzip 文件的完整性:
bash复制gzip -t /data/backup/testdb.sql.gz && echo "backup file OK"
这一步能提前发现文件在写入过程中是否损坏,比等要恢复的时候才发现备份文件打不开强一百倍。
最后再补充一点个人经验
从初次接触 mysqldump 到现在,我最大的感受是:这个工具不复杂,但真的需要敬畏。它不是一个“回车一下就行”的命令,而是一条完整的数据安全链路里的环节。备份、传输、存储、恢复验证,任何一环出问题,关键时刻都会演变成数据事故。我现在的做法是每两周做一次恢复演练,把最近的备份文件拿到测试环境完整恢复一遍,同时用脚本检查备份文件生成时间、大小、gzip 完整性和关键表的行数。这看起来费了一点时间,但在真实故障来临时,你会感谢自己这份“多此一举”。另外也建议你做一个备份日志文件,每次备份完成记录时间、文件大小、耗时、校验结果,出问题的时候,这两行日志能帮你快速定位是备份环节出的问题,还是恢复环节出的问题。
