MySQL Docker 误删恢复:binlog PITR
场景:MySQL 8.0.29 跑在 Docker 里(数据目录 bind-mount),研发误执行批量
DROP TABLE清库 本手册记录 2026-08-28 bxjl-dev 恢复实战的全部命令,按序执行即可复现 生产容器:mysql8(对外 13396) 临时恢复容器:mysql_rec(对外 13398,用后即删) 误删定位:2026-08-28 10:33:35,binlog.000006 位点 11086236
0. 前置事实确认(先想清楚能不能恢复)
| 检查项 | 判定标准 | 本次实测 |
|---|---|---|
| binlog 是否开启 | log_bin=ON |
ON |
| binlog 保留多久 | binlog_expire_logs_seconds |
30 天 |
| binlog 覆盖窗口 | 最老文件起点 ≤ 误删时刻 | 000004 起于 05-13 |
| 库的建表语句是否在窗口内 | binlog 里有 CREATE TABLE | 161 张表全有 |
| 误删后有没有人清 binlog | 无 RESET MASTER/PURGE |
无 |
以上任一不满足,就要换成"目录备份拼结构 + binlog 补数据"的组合方案。
1. 侦察:确认 binlog 状态与文件
bash
# 容器与挂载信息(确认数据目录映射)
docker inspect -f '{{.Config.Image}} {{range .Mounts}}{{.Source}}->{{.Destination}} {{end}}' mysql8
# binlog 开关、保留策略、文件列表、当前位点
docker exec mysql8 mysql -uroot -p'buEM53OR!' -e "
SHOW VARIABLES LIKE 'log_bin%';
SHOW VARIABLES LIKE 'binlog_expire%';
SHOW BINARY LOGS;
SHOW MASTER STATUS;"
# 确认目标库当前状态(表数/行数,判断损失范围)
docker exec mysql8 mysql -uroot -p'buEM53OR!' -N -e "
SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='bxjl-dev';
SELECT SUM(table_rows) FROM information_schema.tables WHERE table_schema='bxjl-dev';"
要点 :宿主机没有 mysqlbinlog,必须在容器内执行 docker exec mysql8 mysqlbinlog,输出重定向到宿主机文件。
2. 全量解码 binlog → all.sql
bash
mkdir -p /tmp/binlogs
# 按 000004 → 000005 → 000006 顺序解码拼接(文件顺序 = 时间顺序,不能乱)
docker exec mysql8 mysqlbinlog \
/var/lib/mysql/binlog.000004 \
/var/lib/mysql/binlog.000005 \
/var/lib/mysql/binlog.000006 > /tmp/binlogs/all.sql
ls -la /tmp/binlogs/all.sql # 本次约 190MB
禁用 --base64-output=NEVER------它会中途截断输出,造成"binlog 只到某天为止"的假象(本次踩过)。
3. 定位误删点
binlog 里 DDL 不带 库名. 前缀,直接 grep 会漏。用 awk 跟踪 use 上下文,把"时间戳 + 库 + 破坏性语句"打出来:
bash
# 扫描所有 DROP TABLE / TRUNCATE,附带时间戳与所属库
awk '/^#26/{ts=$1" "$2} /^use /{use=$0} /DROP TABLE|TRUNCATE/{print NR" | "ts" | "use" | "substr($0,1,100)}' \
/tmp/binlogs/all.sql > /tmp/binlogs/drops.txt
wc -l /tmp/binlogs/drops.txt
# 只看误删当天的破坏性事件(本次 = 2026-08-28,binlog 行首时间戳形如 #260828 10:33:35)
grep '#260828' /tmp/binlogs/drops.txt | head -n 60
# 按日期统计破坏性事件数量(确认误删集中在哪天)
awk -F'|' '{split($2,a," "); print a[1]}' /tmp/binlogs/drops.txt | sort | uniq -c
本次定位结果:
- 误删 = 2026-08-28 10:33:35~10:33:37 一轮按表名字母序的批量
DROP TABLE IF EXISTS - 首个误删 DROP 位于 all.sql 第 3,110,386 行附近
- 10:47:23 还有第二轮部分误删(同样要截掉)
换算 stop-position
bash
LN=3110386 # 首个误删 DROP 所在行(由上面 grep 得出)
# 先人工确认上下文,别盲信行号
sed -n "$((LN-8)),${LN}p" /tmp/binlogs/all.sql
# 取该 DROP 之前最近的 "# at NNN" 位点标记 → 即 binlog.000006 内的重放截止位点
P=$(head -n $LN /tmp/binlogs/all.sql | grep '^# at ' | tail -n 1 | awk '{print $3}')
echo "STOP_POS=$P" # 本次 = 11086236
4. 搭临时恢复实例(与生产同镜像)
bash
# 建空实例(每次重放前必须从干净库开始,避免脏数据)
docker rm -f mysql_rec 2>/dev/null
rm -rf /www/mysql/database/mysql_rec && mkdir -p /www/mysql/database/mysql_rec
IMG=$(docker inspect -f '{{.Config.Image}}' mysql8)
docker run -d --name mysql_rec -p 13398:3306 \
-e MYSQL_ROOT_PASSWORD='buEM53OR!' \
-v /www/mysql/database/mysql_rec:/var/lib/mysql $IMG
# 等实例健康(最长 5 分钟)
for i in $(seq 1 30); do
docker exec mysql_rec mysqladmin -uroot -p'buEM53OR!' ping 2>/dev/null | grep -q alive \
&& { echo "HEALTHY after $((i*10))s"; break; }
sleep 10
done
# 预建 binlog 会引用到的所有库(它们的 CREATE DATABASE 可能在 binlog 起点之前)
# 注意:bxjl-dev 带连字符,必须用反引号
docker exec -i mysql_rec mysql -uroot -p'buEM53OR!' -e "
CREATE DATABASE IF NOT EXISTS \`bxjl-dev\`;
CREATE DATABASE IF NOT EXISTS factory;
CREATE DATABASE IF NOT EXISTS rentcar_dev;"
5. 生成重放 SQL(分文件截断)
--stop-position 只对最后一个 binlog 文件生效 → 必须分两段生成再拼接:
bash
# part1:000004 + 000005 全量(--skip-gtids 去掉 ANONYMOUS GTID,否则报 ERROR 1790)
docker exec mysql8 mysqlbinlog --skip-gtids \
/var/lib/mysql/binlog.000004 /var/lib/mysql/binlog.000005 > /tmp/binlogs/part1.sql 2>/tmp/binlogs/gen1.err
echo "GEN1_RC=$?"
# part2:000006 截到误删点前一位点
docker exec mysql8 mysqlbinlog --skip-gtids --stop-position=$P \
/var/lib/mysql/binlog.000006 > /tmp/binlogs/part2.sql 2>/tmp/binlogs/gen2.err
echo "GEN2_RC=$?"
# 拼接
cat /tmp/binlogs/part1.sql /tmp/binlogs/part2.sql > /tmp/binlogs/replay.sql
wc -l /tmp/binlogs/replay.sql # 本次 311 万行
三项自检(都通过才继续)
bash
# ① part2 最后时间戳应恰好在误删点之前(本次期望 ≈ 10:33:2x)
grep -a '^#26' /tmp/binlogs/part2.sql | tail -n 2
# ② part2 中当天日期的破坏性事件数应为 0
awk '/^#26/{ts=$1" "$2} /DROP TABLE|TRUNCATE/{if (ts ~ /#260828/) c++} END{print "today-destructive-count="c+0}' /tmp/binlogs/part2.sql
# ③ 首个被误删的表不应出现在 part2(本次查 base_table,期望 0)
grep -c 'DROP TABLE IF EXISTS `base_table`' /tmp/binlogs/part2.sql
6. 后台重放(大文件必须 nohup)
SSH 通道一旦断开,前台导入会半途而废(本次踩过)。用 nohup + rc 文件轮询:
bash
rm -f /tmp/binlogs/apply2.rc /tmp/binlogs/apply2.err /tmp/binlogs/apply2.out
nohup bash -c 'docker exec -i mysql_rec mysql -uroot -p"buEM53OR!" -f \
< /tmp/binlogs/replay.sql > /tmp/binlogs/apply2.out 2>/tmp/binlogs/apply2.err; \
echo $? > /tmp/binlogs/apply2.rc' >/dev/null 2>&1 &
echo "apply launched"
# 轮询直到完成(本次约 4.5 分钟)
while [ ! -f /tmp/binlogs/apply2.rc ]; do sleep 30; done
echo "APPLY_RC=$(cat /tmp/binlogs/apply2.rc)" # 期望 0
# 错误归因:只关心 bxjl-dev 相关错误
grep -i 'ERROR' /tmp/binlogs/apply2.err | grep -v -i 'factory' | head -n 20
-f 的作用:binlog 里其他库(本次是 factory.*)的"表不存在"历史错误直接跳过------与目标库无关,不影响 bxjl-dev。
7. 恢复实例校验
bash
# 表数(期望 161 = 106 现役 + 55 历史遗留)
docker exec mysql_rec mysql -uroot -p'buEM53OR!' -N -e \
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='bxjl-dev';"
# 总行数(期望 52,499)
docker exec mysql_rec mysql -uroot -p'buEM53OR!' -N -e \
"SELECT SUM(table_rows) FROM information_schema.tables WHERE table_schema='bxjl-dev';"
# 数据新鲜度(期望覆盖到误删当天)
docker exec mysql_rec mysql -uroot -p'buEM53OR!' -N -e \
"SELECT MAX(create_time) FROM \`bxjl-dev\`.market_lead;"
# 逐表行数导出(备查,后续与生产 diff)
docker exec mysql_rec mysql -uroot -p'buEM53OR!' -N -e \
"SELECT table_name, table_rows FROM information_schema.tables WHERE table_schema='bxjl-dev' ORDER BY table_name;" \
> /tmp/binlogs/rows_rec.txt
8. 回灌生产(先留底,再导入)
bash
# ① 线上现状留底(万一要回滚,这里是恢复前状态)
docker exec mysql8 mysqldump -uroot -p'buEM53OR!' --single-transaction bxjl-dev \
> /www/mysql/database/bxjl-dev-live-before-restore-20260828.sql
ls -la /www/mysql/database/bxjl-dev-live-before-restore-20260828.sql # 本次 219KB
# ② 从恢复实例导出完整库(含存储过程/触发器/事件)
docker exec mysql_rec mysqldump -uroot -p'buEM53OR!' --single-transaction --routines --events bxjl-dev \
> /www/mysql/database/bxjl-dev-recovered-20260828.sql
ls -la /www/mysql/database/bxjl-dev-recovered-20260828.sql # 本次 15.9MB
# ③ 灌回生产 mysql8
docker exec -i mysql8 mysql -uroot -p'buEM53OR!' bxjl-dev \
< /www/mysql/database/bxjl-dev-recovered-20260828.sql 2>/tmp/binlogs/restore.err
echo "RESTORE_RC=$?"
cat /tmp/binlogs/restore.err # 期望只有 password 警告
9. 线上终验(逐表 diff)
bash
# 生产逐表行数,与恢复实例比对
docker exec mysql8 mysql -uroot -p'buEM53OR!' -N -e \
"SELECT table_name, table_rows FROM information_schema.tables WHERE table_schema='bxjl-dev' ORDER BY table_name;" \
> /tmp/binlogs/rows_live.txt
diff /tmp/binlogs/rows_rec.txt /tmp/binlogs/rows_live.txt && echo "== IDENTICAL =="
# 表数 / 总行数 / 新鲜度 三项复核(同第 7 步,换成 mysql8)
10. 清理现场
bash
docker rm -f mysql_rec mysql_pitr
rm -rf /www/mysql/database/mysql_rec /www/mysql/database/mysql_pitr /tmp/binlogs
保留产物:/www/mysql/database/ 下的 bxjl-dev-recovered-20260828.sql(恢复快照)与 bxjl-dev-live-before-restore-20260828.sql(回滚留底)。
附录 A:踩坑清单(务必先看)
| # | 坑 | 正确做法 |
|---|---|---|
| 1 | mysqlbinlog --database=库名 过滤会丢掉行级(RBR)写事件,重放完业务表全空 |
全量重放,事后单独 dump 目标库 |
| 2 | binlog 中 DDL 无 库名. 前缀,grep 'DROP TABLE `db`.' 会漏判 |
awk 跟踪 use 行上下文再匹配 |
| 3 | --base64-output=NEVER 导致 mysqlbinlog 中途截断 |
去掉该参数 |
| 4 | 重放报 ERROR 1790 @@SESSION.GTID_NEXT cannot be changed |
加 --skip-gtids |
| 5 | --stop-position 只对最后一个 binlog 文件生效 |
分文件生成 SQL 再拼接 |
| 6 | 库名含连字符,CREATE DATABASE bxjl-dev 语法错 |
反引号 bxjl-dev |
| 7 | 宿主机没有 mysqlbinlog | docker exec <容器> mysqlbinlog |
| 8 | 大文件前台导入遇 SSH 断开就半途而废 | nohup 后台 + rc 文件轮询 |
| 9 | 重放到非目标库历史语句报错导致中断 | mysql -f 跳过无关错误 |
附录 B:误删后黄金 4 条
- 不要重启 MySQL(重启无害,但没必要)
- 不要执行
RESET MASTER/PURGE BINARY LOGS/FLUSH LOGS------ 直接撕证据 - 不要把数据目录整个覆盖回去(目录备份可能是模糊备份,会引入不一致)
- 先冻结写操作 ,立刻把 binlog 文件原样拷贝一份(
cp到安全目录)