如何快速高效备份数据库
测试前备份数据库是保障数据安全、支持快速回滚的基础操作。本文从备份场景 、思路与流程图 、实战代码三个维度,讲解在表数据多、数据量大的情况下,如何快速高效地完成备份。
一、为什么测试前要备份?
| 场景 | 风险 | 备份价值 |
|---|---|---|
| 造数测试 | 批量插入/更新可能写错 | 一键回滚到测试前状态 |
| 接口压测 | 并发写入可能污染数据 | 恢复干净环境复测 |
| 规则验证 | 修改配置后需对比 | 保留原始数据做 diff |
| 环境复用 | 多人共用测试库 | 各自备份互不干扰 |
二、备份方式对比
| 方式 | 速度 | 适用场景 | 恢复难度 |
|---|---|---|---|
| mysqldump 全库 | 慢(大库) | 小库、完整迁移 | 简单 |
| mysqldump 单表 | 较快 | 只改少数表 | 简单 |
| mysqldump + 压缩 | 中等 | 大库、省空间 | 需解压 |
| 物理文件拷贝 | 最快 | 可停库、同机 | 需停库 |
| SELECT INTO OUTFILE | 中等 | 单表导出 CSV | 需建表再导入 |
三、备份思路与流程图
3.1 备份决策流程
小, <1GB
大, >1GB
是
否
需要备份
数据量?
mysqldump 全库
只改部分表?
mysqldump 指定表
mysqldump 全库 + gzip
输出 .sql
输出 .sql.gz
保存到备份目录
记录备份信息
3.2 备份执行流程
全库
单库
指定表
是
否
开始
读取连接配置
创建备份目录
备份范围?
mysqldump --all-databases
mysqldump db_name
mysqldump db table1 table2
是否压缩?
管道 gzip
直接输出
写入 .sql.gz
写入 .sql
校验文件大小
记录备份路径/时间
结束
3.3 恢复决策流程
.sql
.sql.gz
需要恢复
定位备份文件
文件格式?
mysql < backup.sql
gunzip -c backup.sql.gz | mysql
验证数据
结束
3.4 测试前备份完整流程
是
否
测试计划
确定涉及的表
执行备份脚本
备份完成
执行测试
测试通过?
保留备份一段时间
执行恢复
数据回滚
四、实战一:mysqldump 命令行备份
4.1 全库备份(适合小库)
bash
# 全库备份,输出到 SQL 文件
mysqldump -h 主机 -P 3306 -u 用户 -p 数据库名 > backup_$(date +%Y%m%d_%H%M%S).sql
# 示例
mysqldump -h rm-xxx.mysql.rds.aliyuncs.com -P 3306 -u kpos -p kkpos_shopping > backup_20250316.sql
4.2 指定表备份(大库推荐)
只备份测试会涉及的表,显著减少时间和文件大小。
bash
# 备份指定表
mysqldump -h 主机 -P 3306 -u 用户 -p 数据库名 表1 表2 表3 > backup_tables.sql
# 示例:只备份商品、价格、库存相关表
mysqldump -h rm-xxx.mysql.rds.aliyuncs.com -P 3306 -u kpos -p kkpos_shopping \
kkpos_product kkpos_product_price kkpos_stock > backup_product_$(date +%Y%m%d).sql
4.3 全库 + 压缩(大库)
bash
# 备份并压缩,节省空间
mysqldump -h 主机 -P 3306 -u 用户 -p 数据库名 | gzip > backup_$(date +%Y%m%d).sql.gz
4.4 常用参数说明
| 参数 | 含义 | 建议 |
|---|---|---|
--single-transaction |
一致性快照,不锁表 | InnoDB 必加 |
--quick |
逐行读,不缓存在内存 | 大表必加 |
--skip-lock-tables |
不锁表 | 测试库可用 |
--routines |
备份存储过程/函数 | 按需 |
--triggers |
备份触发器 | 默认包含 |
五、实战二:Python 自动化备份脚本
5.1 配置与思路
将连接信息、备份目录、表列表配置化,一键执行备份。
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
数据库备份脚本 - 测试前快速备份
支持:全库、单库、指定表;可选压缩
"""
import subprocess
import os
from datetime import datetime
# 数据库连接配置(请替换为实际值)
DB_CONFIG = {
"host": "rm-xxx.mysql.rds.aliyuncs.com",
"port": 3306,
"user": "kpos",
"password": "your_password",
"database": "kkpos_shopping",
}
# 备份目录
BACKUP_DIR = os.path.expanduser("~/Desktop/DB_Backup")
# 备份模式: "all" | "database" | "tables"
BACKUP_MODE = "tables"
# 指定表备份时的表列表(仅当 BACKUP_MODE="tables" 时生效)
TABLES = [
"kkpos_product",
"kkpos_product_price",
"kkpos_stock",
"kkpos_order",
]
# 是否压缩
USE_GZIP = True
def ensure_backup_dir():
"""确保备份目录存在"""
if not os.path.exists(BACKUP_DIR):
os.makedirs(BACKUP_DIR)
print(f"创建备份目录: {BACKUP_DIR}")
def build_mysqldump_cmd():
"""构建 mysqldump 命令"""
base = [
"mysqldump",
"-h", DB_CONFIG["host"],
"-P", str(DB_CONFIG["port"]),
"-u", DB_CONFIG["user"],
f"-p{DB_CONFIG['password']}",
"--single-transaction",
"--quick",
"--skip-lock-tables",
"--default-character-set=utf8mb4",
]
if BACKUP_MODE == "all":
base.append("--all-databases")
elif BACKUP_MODE == "database":
base.append(DB_CONFIG["database"])
elif BACKUP_MODE == "tables":
base.append(DB_CONFIG["database"])
base.extend(TABLES)
return base
def run_backup():
"""执行备份"""
ensure_backup_dir()
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
if BACKUP_MODE == "tables":
suffix = "_".join(TABLES[:2]) + "_" if len(TABLES) <= 2 else "tables_"
else:
suffix = ""
filename = f"backup_{DB_CONFIG['database']}_{suffix}{timestamp}.sql"
if USE_GZIP:
filename += ".gz"
filepath = os.path.join(BACKUP_DIR, filename)
cmd = build_mysqldump_cmd()
print("=" * 60)
print("数据库备份")
print("=" * 60)
print(f"模式: {BACKUP_MODE}")
if BACKUP_MODE == "tables":
print(f"表: {TABLES}")
print(f"输出: {filepath}")
print("=" * 60)
try:
with open(filepath, "wb") as f:
if USE_GZIP:
import gzip
proc_dump = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
proc_gzip = subprocess.Popen(
["gzip", "-c"],
stdin=proc_dump.stdout,
stdout=f,
stderr=subprocess.PIPE,
)
proc_dump.stdout.close()
proc_gzip.communicate()
if proc_dump.wait() != 0:
raise RuntimeError(f"mysqldump 失败: {proc_dump.stderr.read().decode()}")
else:
result = subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE, check=True)
size_mb = os.path.getsize(filepath) / (1024 * 1024)
print(f"备份完成: {filepath}")
print(f"文件大小: {size_mb:.2f} MB")
return filepath
except subprocess.CalledProcessError as e:
print(f"备份失败: {e.stderr.decode()}")
raise
except Exception as e:
print(f"备份失败: {e}")
raise
if __name__ == "__main__":
run_backup()
5.2 使用步骤
- 修改
DB_CONFIG为实际连接信息。 - 设置
BACKUP_MODE:all/database/tables。 - 若为
tables,填写TABLES列表。 - 执行:
python backup_db.py。
六、实战三:大表分批备份(流式导出)
当单表千万级时,可分批 SELECT 导出,降低内存压力。
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
大表分批备份 - 按主键范围导出,避免 OOM
"""
import pymysql
import csv
import os
from datetime import datetime
DB_CONFIG = {
"host": "localhost",
"port": 3306,
"user": "root",
"password": "xxx",
"database": "test_db",
"charset": "utf8mb4",
"cursorclass": pymysql.cursors.DictCursor,
}
TABLE = "large_table"
PK_COLUMN = "id"
BATCH_SIZE = 50000
BACKUP_DIR = os.path.expanduser("~/Desktop/DB_Backup")
def get_min_max_id(conn):
"""获取主键范围"""
with conn.cursor() as cur:
cur.execute(f"SELECT MIN({PK_COLUMN}) as min_id, MAX({PK_COLUMN}) as max_id FROM {TABLE}")
row = cur.fetchone()
return row["min_id"], row["max_id"]
def backup_table_in_batches():
"""分批导出大表到 CSV"""
os.makedirs(BACKUP_DIR, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filepath = os.path.join(BACKUP_DIR, f"{TABLE}_{timestamp}.csv")
conn = pymysql.connect(**DB_CONFIG)
min_id, max_id = get_min_max_id(conn)
print(f"表 {TABLE}: id 范围 {min_id} ~ {max_id}")
with open(filepath, "w", encoding="utf-8-sig", newline="") as f:
writer = None
current = min_id
total_rows = 0
while current is not None and current <= max_id:
with conn.cursor() as cur:
cur.execute(
f"SELECT * FROM {TABLE} WHERE {PK_COLUMN} >= %s ORDER BY {PK_COLUMN} LIMIT %s",
(current, BATCH_SIZE),
)
rows = cur.fetchall()
if not rows:
break
if writer is None:
writer = csv.DictWriter(f, fieldnames=rows[0].keys())
writer.writeheader()
for row in rows:
writer.writerow(row)
total_rows += len(rows)
current = rows[-1][PK_COLUMN] + 1
print(f"已导出 {total_rows} 行...")
conn.close()
print(f"完成: {filepath}, 共 {total_rows} 行")
return filepath
if __name__ == "__main__":
backup_table_in_batches()
七、实战四:恢复操作
7.1 恢复 .sql 文件
bash
# 恢复未压缩的 SQL
mysql -h 主机 -P 3306 -u 用户 -p 数据库名 < backup.sql
# 恢复前先清空库(谨慎!)
mysql -h 主机 -u 用户 -p -e "DROP DATABASE IF EXISTS kkpos_shopping; CREATE DATABASE kkpos_shopping;"
mysql -h 主机 -u 用户 -p kkpos_shopping < backup.sql
7.2 恢复 .sql.gz 文件
bash
gunzip -c backup.sql.gz | mysql -h 主机 -P 3306 -u 用户 -p 数据库名
7.3 Python 恢复脚本
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""从备份文件恢复数据库"""
import subprocess
import sys
DB_CONFIG = {
"host": "localhost",
"port": 3306,
"user": "root",
"password": "xxx",
"database": "kkpos_shopping",
}
BACKUP_FILE = sys.argv[1] if len(sys.argv) > 1 else "backup.sql"
def restore():
if BACKUP_FILE.endswith(".gz"):
cmd = f"gunzip -c {BACKUP_FILE} | mysql -h {DB_CONFIG['host']} -P {DB_CONFIG['port']} -u {DB_CONFIG['user']} -p{DB_CONFIG['password']} {DB_CONFIG['database']}"
subprocess.run(cmd, shell=True)
else:
cmd = [
"mysql", "-h", DB_CONFIG["host"], "-P", str(DB_CONFIG["port"]),
"-u", DB_CONFIG["user"], f"-p{DB_CONFIG['password']}",
DB_CONFIG["database"],
]
with open(BACKUP_FILE, "r") as f:
subprocess.run(cmd, stdin=f, check=True)
print("恢复完成")
if __name__ == "__main__":
restore()
八、大表备份优化建议
| 策略 | 说明 |
|---|---|
| 只备份涉及的表 | 避免全库,显著缩短时间 |
| --single-transaction | InnoDB 一致性快照,不锁表 |
| --quick | 大表不缓存在内存 |
| gzip 压缩 | 减少磁盘占用,网络传输更快 |
| 分批导出 | 千万级表用脚本分批 SELECT |
| 避开业务高峰 | 备份有 IO 压力,尽量低峰执行 |
九、备份信息记录
建议在备份后记录:时间、文件路径、大小、涉及表,便于后续恢复。
python
# 备份完成后追加到日志
log_line = f"{datetime.now().isoformat()} | {filepath} | {size_mb:.2f}MB | {','.join(TABLES)}\n"
with open(os.path.join(BACKUP_DIR, "backup_log.txt"), "a") as f:
f.write(log_line)
十、总结
- 测试前必备份:造数、压测、规则验证前先备份涉及表。
- 大库用指定表:只备份会改动的表,节省时间和空间。
- 加压缩:大库用 gzip,减少存储和传输成本。
- 加 --single-transaction:InnoDB 不锁表,保证一致性。
- 记录备份信息:便于快速找到并恢复对应备份。
掌握以上思路与脚本,可以快速、安全地完成测试前数据库备份与恢复。