MySQL
MySQL 恢复出来的数据,可能比当前业务少一批订单,也可能重新出现已经取消的订单。恢复点决定数据库回到哪一份状态;应用连接、后台任务和消息消费决定这份状态随后怎样变化。维护数据库时,这两件事需要一起安排。
接管实例时先确定实际运行对象
一套 MySQL 服务通常涉及以下对象:
应用
├─ JDBC URL、连接池、业务账号
└─ 定时任务、消息消费者、迁移工具等其他写入者
连接入口
├─ 直连地址
└─ Router / 代理 / 服务发现
MySQL 实例
├─ server_uuid、版本、读写角色
├─ 配置文件与运行中变量
├─ datadir、redo、undo、binlog
└─ 账号、证书、插件、密钥环
恢复材料
├─ 一致性备份及对应日志坐标
├─ 连续归档的 binlog
└─ 恢复所需配置、身份和加密密钥首次安装、表设计、客户端接入和复制搭建可以从 MySQL 产品主文 开始。已经运行的实例先做只读核查,避免把默认路径或交付清单当成实际配置。
在 Linux 管理机使用 MySQL 8.4 客户端,给当前运维用户配置登录入口:
mysql_config_editor set --login-path=ops-db \
--host=db.example.internal --port=3306 --user=ops_reader --password
mysql --login-path=ops-db --ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem将域名、账号和 CA 文件替换为已确认的实例信息。口令由交互输入;登录文件只是降低直接暴露风险的混淆存储,仍需限制本机账号与文件访问,不能作为密钥保险库。mysql_config_editor 文档说明其存储方式。
进入 SQL 会话后:
SELECT VERSION(), @@server_uuid, @@hostname, @@port;
SELECT CURRENT_USER(), USER();
SELECT @@global.read_only, @@global.super_read_only;
SHOW VARIABLES WHERE Variable_name IN (
'datadir','log_bin','binlog_format','binlog_expire_logs_seconds',
'gtid_mode','innodb_flush_log_at_trx_commit','sync_binlog',
'max_connections','innodb_buffer_pool_size'
);
SHOW BINARY LOG STATUS;
SHOW REPLICA STATUS\GCURRENT_USER() 表示最终匹配的授权账号,USER() 是连接声称的用户名与来源。核对 UUID 后再确认入口:经过 Router 的两次连接可能落到不同实例。复制查询需要相应监控权限;返回权限错误时申请所需权限,不要直接把业务账号换成 root。
read_only=ON 的限制存在特权例外;super_read_only 更严格,但复制线程仍可应用事务。维护期间的旧主隔离还需要关闭旧入口、停掉写入者并核查现有连接。完整语义见 服务器只读变量。
宿主机上再核对文件和服务。使用已查到的数据路径,不对整台机器做递归扫描:
systemctl show mysql -p User -p ExecStart -p FragmentPath
findmnt -T /var/lib/mysql
df -h /var/lib/mysql
df -i /var/lib/mysql服务名和路径以实际安装为准,RPM 安装可能使用 mysqld。Docker 实例改查 docker inspect <容器名> 中的镜像和 Mounts,以及 docker top <容器名> 中的进程身份;不要把包含环境凭据的完整 inspect 输出放到工单。挂载存在但落在系统盘、配置文件已改但运行变量没变,都需要在开始备份或维护前解决。
在独立实例恢复一份订单数据
下面使用 Linux Bash、Docker Engine 和 MySQL 8.4.11。两个实例都没有发布端口,也没有外部网络。宿主操作者需要 Docker 权限;官方镜像入口可用 root 初始化卷,再以 mysql 身份运行数据库。演示 root 口令只用于这两个隔离容器,真实备份使用单独授权账号。
在新的工作目录执行:
mkdir mysql-recovery-lab
cd mysql-recovery-lab
umask 077
docker volume create ops3-mysql-source
docker volume create ops3-mysql-restore
docker run -d --name ops3-mysql-source --network none --memory 1g \
-e MYSQL_ROOT_PASSWORD=LabOnly-MySQL-83 \
-v ops3-mysql-source:/var/lib/mysql mysql:8.4.11
docker run -d --name ops3-mysql-restore --network none --memory 1g \
-e MYSQL_ROOT_PASSWORD=LabOnly-MySQL-83 \
-v ops3-mysql-restore:/var/lib/mysql mysql:8.4.11卷与容器名必须是本次新建的,若已存在应换一组名称,不要覆盖旧数据。定义一个只用于实验的 SQL 入口,凭据从容器环境读取:
dbsql() {
docker exec -i "$1" sh -c \
'exec env MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql --protocol=TCP -h127.0.0.1 -uroot --batch --raw'
}
for node in ops3-mysql-source ops3-mysql-restore; do
ready=no
for attempt in {1..90}; do
if printf 'SELECT 1;\n' | dbsql "$node" >/dev/null 2>&1; then
ready=yes
break
fi
sleep 2
done
test "$ready" = yes || { docker logs --tail 80 "$node"; exit 1; }
done只有通过 TCP 完成认证并执行 SQL 才进入下一步。首次初始化会启动禁用网络的临时 mysqld;默认 Unix socket 查询甚至可能在临时实例上成功,随后又随初始化结束而断开。显式指定 TCP 可排除这段窗口。进程反复退出时检查日志、卷权限和内存,不靠不断重启等待恢复。
导出前明确包含哪些对象
在源实例创建两条订单:
dbsql ops3-mysql-source <<'SQL'
CREATE DATABASE recovery_lab;
CREATE TABLE recovery_lab.orders (
id BIGINT PRIMARY KEY,
amount DECIMAL(12,2) NOT NULL,
status VARCHAR(20) NOT NULL
) ENGINE=InnoDB;
INSERT INTO recovery_lab.orders VALUES
(101, 19.90, 'PAID'), (102, 30.00, 'NEW');
SELECT id, amount, status FROM recovery_lab.orders ORDER BY id;
SQL导出单个数据库:
set -o pipefail
docker exec ops3-mysql-source sh -c \
'exec env MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysqldump -uroot \
--single-transaction --no-tablespaces --set-gtid-purged=OFF \
--routines --events --triggers --hex-blob --databases recovery_lab' \
> recovery_lab.sql.part &&
mv recovery_lab.sql.part recovery_lab.sql
test -s recovery_lab.sql
sha256sum recovery_lab.sql > recovery_lab.sql.sha256--databases 让导出文件包含数据库创建与选择语句,因此目标导入不需要猜默认库。临时文件只在导出成功后成为正式文件;失败时停在此处,不拿旧文件继续恢复。
--single-transaction 为 InnoDB 表建立一致性读视图。备份期间禁止对相关表执行会破坏导出一致性的 DDL;MyISAM 等非事务表需要另外安排锁或停写。权限、存储过程与事件、GTID 处理按 mysqldump 8.4 参数说明逐项选择。
这里导出的只是 recovery_lab。账号授权、其他数据库和 binlog 没有包含在这份文件里,GTID_PURGED 也被显式关闭;它适合说明单库状态恢复,不能用来直接初始化完整 GTID 副本。
比较源数据与恢复数据
备份完成后,源订单发生变化:
dbsql ops3-mysql-source <<'SQL'
UPDATE recovery_lab.orders SET status='CANCELLED' WHERE id=102;
INSERT INTO recovery_lab.orders VALUES (103, 9.00, 'NEW');
SQL
sha256sum -c recovery_lab.sql.sha256
dbsql ops3-mysql-restore < recovery_lab.sql
dbsql ops3-mysql-restore <<'SQL'
SELECT id, amount, status FROM recovery_lab.orders ORDER BY id;
SELECT COUNT(*) AS rows_total, SUM(amount) AS amount_total
FROM recovery_lab.orders;
SQL目标应有 101 / PAID、102 / NEW 两行,总金额 49.90;源库已有第三行且订单 102 已取消。这正是备份时间点与当前状态的区别。导入非零退出、对象缺失或金额不符时,保留目标日志和文件,在新的空目标排除问题后重试;不要把半恢复数据库当成完整结果。
恢复数据还要经过应用身份读取。在目标中创建只读账号:
dbsql ops3-mysql-restore <<'SQL'
CREATE USER 'report_reader'@'localhost' IDENTIFIED BY 'LabOnly-Reader-83';
GRANT SELECT ON recovery_lab.* TO 'report_reader'@'localhost';
SQL
docker exec -e MYSQL_PWD=LabOnly-Reader-83 ops3-mysql-restore \
mysql -ureport_reader -e 'SELECT * FROM recovery_lab.orders ORDER BY id'
docker exec -e MYSQL_PWD=LabOnly-Reader-83 ops3-mysql-restore \
mysql -ureport_reader -e "DELETE FROM recovery_lab.orders WHERE id=101"读取应成功,删除应得到 ERROR 1142(权限拒绝)和非零退出码。不能把连接失败当成权限反例通过。真实应用还要核对视图、触发器、事件、字符集、时区以及需要的写权限;使用错误账号导入成功与业务恢复是两种检查。
实验结束用 docker stop ops3-mysql-source ops3-mysql-restore 停止资源。保留两个卷与导出文件供复查,不执行 down -v 或全局 prune。
选择恢复点,再重新接入写流量
全量备份之后的事务从哪里补
RPO 是可接受的数据损失窗口,RTO 是从中断到恢复服务的时间目标。每天做一次备份只给出了计划频率;要把 RPO 缩小到分钟级,还需要持续归档 binlog、监控归档延迟,并确认保存链条覆盖恢复目标。
全量一致性视图
└─ 备份中的 binlog 文件 + position / GTID
└─ 连续日志中的后续完整事务
├─ 最后一个希望保留的事务:应用
└─ 第一笔误操作及之后的事务:暂停,另作业务判断PITR 在隔离实例先导入基线,再回放日志。起点必须来自备份对应的一致性坐标,不能用备份结束后随手查到的当前位置代替。时间只能帮助找到候选事务;精确终点要核对事务边界、时区和 GTID,不能把行事件截在一半。MySQL 时间点恢复说明给出了基线与日志的依赖关系。
对已经取得的本地归档,DBA 可先做解码查看:
mysqlbinlog --base64-output=DECODE-ROWS --verbose \
/backup/incident/mysql-bin.000123 | lessDECODE-ROWS 输出用于检查行变化,不作为恢复回放输入。确认起止坐标后使用正常 mysqlbinlog 输出生成恢复 SQL,先检查再导入隔离目标;多文件时保持连续顺序,起点只约束首文件,终点只约束末文件。具体命令、远程归档身份及 GTID 初始化见 MySQL 的 Binlog 恢复操作。
还要计算 RTO 的主要开销:下载备份、解密、导入、建索引、回放日志、业务检查和重新接流。大库若逻辑恢复超过预算,应在相同硬件与版本条件下评估 MySQL Shell 并行导入或支持该版本的物理备份;不能从压缩包大小推算恢复一定很快。
重新接流的顺序
恢复订单 102 为 NEW 后,支付或取消消息可能已经在别处处理过。接流前必须明确这类外部状态如何补齐;必要时从业务流水重放或对账,而不是让旧数据库自行覆盖其他系统。
- 停止或隔离旧库的所有写入来源,包含 API、消费者、定时任务和 DDL 发布器;保留未完成请求及事务的处理记录。
- 确认目标 UUID、最终恢复点和所需数据;补齐账号、证书、密钥及实例配置,默认不开启会主动改变业务数据的事件或任务。
- 使用应用账号执行代表性查询;在约定的测试对象上检查事务提交与回滚,再与业务系统核对金额、状态及唯一约束。
- 更新应用连接配置并重建连接池。DNS 或代理切换后,旧长连接仍可能留在原实例,应通过连接日志及目标 UUID 确认新的读写落点。
- 分批恢复业务,限制补偿和积压消费速度;观察连接数、锁等待、错误率、复制与磁盘,再开放下一批写入。
若新目标已接收写入,切回旧库前必须处理新增事务。保留旧实例只是保留了一个旧状态,回退方案还需要写入捕获、数据合并或再恢复;没有准备这些条件时应停止扩流,在目标上修复。
运行中出现异常,从影响对象定位
连接数升高,先找连接为什么不释放
SHOW GLOBAL STATUS WHERE Variable_name IN (
'Threads_connected','Threads_running','Connections',
'Aborted_connects','Connection_errors_max_connections'
);
SELECT * FROM sys.innodb_lock_waits\G
SHOW FULL PROCESSLIST;需要被授权的监控账号;SQL 文本可能包含业务数据,输出只放受限位置。连接多但运行少时检查连接池总上限和空闲回收;运行多且等待同一阻塞事务时,先找事务所属请求与回滚影响。提高 max_connections 会增加内存和并发压力,不能消除阻塞。调整后用同样时间窗比较等待数、请求耗时和连接回收情况。
磁盘不足,区分数据增长与日志滞留
先看实际挂载的空间与 inode,再看表体量、binlog 列表、慢日志和临时文件。binlog 清理前同时核对所有副本已消费的位置和仍需保留的恢复窗口,使用 SQL 管理日志,不从文件系统直接删 binlog、redo 或表空间。增长来自长事务时,检查事务持续时间及 Undo 保留;来自业务数据时安排扩容或已确认的数据归档。
磁盘恢复后检查数据库错误日志、只读状态、备份和复制是否恢复正常,并执行一次真实事务。只看到空间释放,仍可能有客户端处在连续重试中。
副本延迟或停止,先看线程与错误
SHOW REPLICA STATUS\G 中先看 I/O 与 SQL 线程是否运行,再看最后错误、来源 UUID、已接收与已执行 GTID。延迟为零可能只代表当时没有新的事务进入;SQL 线程停止时也不能只读一个秒数下结论。
缺日志时重新建立一致性基线,SQL 冲突时查明来源和目标为什么不同。不要用跳过事务或重置 GTID 让状态表暂时好看。恢复后还需比较事务位置与业务数据;计划切换前,在停写条件下确认目标追到约定终点并隔离旧主。
恢复失败时保留原件
校验和不一致先重新获取备份;导入报权限或 DEFINER 错误时核查对象与目标账号,别全局替换 SQL 绕过安全语义;版本或字符集不支持时用兼容目标恢复再设计迁移。数据损坏的实例先保全磁盘和日志,在副本上诊断。innodb_force_recovery 属于应急导出手段,不应成为长期运行配置。
处理完成后重新生成一份独立备份并做恢复检查,让新的服务状态重新受到保护。
权威资料与规范地址
身份与实例
- mysql_config_editor:https://dev.mysql.com/doc/refman/8.4/en/mysql-config-editor.html
- super_read_only:https://dev.mysql.com/doc/refman/8.4/en/server-system-variables.html#sysvar_super_read_only
