难度系数:⭐⭐⭐⭐⭐
实用指数:💯💯💯💯💯
📖 开篇:一次删库跑路的惨案
凌晨2点,实习生小李正在加班修复bug:
-- 想删除测试数据
DELETE FROM user WHERE id = 12345;
但是,他少打了一个条件...
-- 实际执行的SQL 😱
DELETE FROM user; -- WHERE条件没了!
3秒后...
Query OK, 10000000 rows affected
小李愣了3秒,然后:
"卧槽!!!我删了1000万用户!😱😱😱"
老板凌晨被叫醒:
"什么?!数据全没了?有备份吗?😡"
运维:
"有备份,但是... 上次备份是1个月前的... 😭"
老板:
"那这1个月的数据都丢了?!我们要赔多少钱!!"
这就是为什么数据备份如此重要! 🎯
🎯 数据备份的重要性
1. 人为误操作 👨💻
-- 常见的删库操作
DROP TABLE user;
TRUNCATE TABLE order;
DELETE FROM xxx; -- 忘记WHERE
UPDATE xxx; -- 忘记WHERE
2. 硬件故障 💽
- 磁盘损坏
- 服务器宕机
- 机房断电
- 自然灾害(地震、火灾)
3. 软件Bug 🐛
- 程序逻辑错误导致数据损坏
- 数据库升级失败
- 主从同步异常
4. 安全攻击 🔒
- 勒索病毒
- 黑客入侵
- SQL注入
- 数据篡改
5. 成本计算 💰
某电商平台数据丢失损失:
- 用户数据:1000万用户 × 10元 = 1亿元
- 订单数据:100万订单 × 50元 = 5000万元
- 业务中断:每小时损失100万元
- 声誉损失:无法估量
备份成本:
- 存储:1TB × 1元/月 = 1元/月
- 人力:1天/月 × 1000元 = 1000元/月
- 总成本:约1000元/月
ROI:1亿 ÷ 1000 = 100000倍!
🎨 备份策略选择
┌──────────────────────────────────────────────────────────┐
│ 备份方案对比 │
└──────────────────────────────────────────────────────────┘
类型1: 全量备份(Full Backup)
- 备份所有数据
- 恢复速度:快 ⚡
- 备份速度:慢 🐌
- 存储成本:高 💰💰💰
类型2: 增量备份(Incremental Backup)
- 只备份变化的数据
- 恢复速度:慢(需要合并)
- 备份速度:快 ⚡
- 存储成本:低 💰
类型3: 差异备份(Differential Backup)
- 备份与上次全量备份的差异
- 恢复速度:中等
- 备份速度:中等
- 存储成本:中等 💰💰
推荐策略:
- 每周全量备份
- 每天差异备份
- 每小时binlog备份
💻 MySQL备份方案
1. mysqldump备份(逻辑备份)
全量备份
#!/bin/bash
# backup.sh
# 配置
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backup/mysql"
DB_NAME="my_database"
DB_USER="root"
DB_PASS="password"
# 创建备份目录
mkdir -p $BACKUP_DIR
# 执行备份
mysqldump -u$DB_USER -p$DB_PASS \
--single-transaction \ # 事务一致性
--master-data=2 \ # 记录binlog位置
--flush-logs \ # 刷新binlog
--all-databases \ # 所有数据库
--routines \ # 存储过程
--triggers \ # 触发器
--events \ # 事件
| gzip > $BACKUP_DIR/full_backup_$DATE.sql.gz
# 检查备份结果
if [ $? -eq 0 ]; then
echo "✅ 备份成功:$BACKUP_DIR/full_backup_$DATE.sql.gz"
# 计算文件大小
SIZE=$(du -h $BACKUP_DIR/full_backup_$DATE.sql.gz | cut -f1)
echo "备份大小:$SIZE"
# 删除7天前的备份
find $BACKUP_DIR -name "full_backup_*.sql.gz" -mtime +7 -delete
echo "✅ 清理7天前的备份"
else
echo "❌ 备份失败"
exit 1
fi
单表备份
# 备份单个表
mysqldump -u root -p database_name table_name > table_backup.sql
# 只备份表结构
mysqldump -u root -p --no-data database_name > schema_only.sql
# 只备份数据(不含表结构)
mysqldump -u root -p --no-create-info database_name > data_only.sql
恢复数据
# 恢复整个数据库
gunzip < full_backup_20241027.sql.gz | mysql -u root -p
# 恢复单个数据库
mysql -u root -p database_name < database_backup.sql
# 恢复单个表
mysql -u root -p database_name < table_backup.sql
2. XtraBackup备份(物理备份,推荐⭐⭐⭐⭐⭐)
全量备份
#!/bin/bash
# xtrabackup_full.sh
BACKUP_DIR="/backup/xtrabackup"
DATE=$(date +%Y%m%d_%H%M%S)
FULL_BACKUP_DIR="$BACKUP_DIR/full_$DATE"
# 创建全量备份
xtrabackup --backup \
--target-dir=$FULL_BACKUP_DIR \
--user=root \
--password=password
if [ $? -eq 0 ]; then
echo "✅ 全量备份成功:$FULL_BACKUP_DIR"
else
echo "❌ 全量备份失败"
exit 1
fi
增量备份
#!/bin/bash
# xtrabackup_incremental.sh
BACKUP_DIR="/backup/xtrabackup"
FULL_BACKUP_DIR="$BACKUP_DIR/full_20241027" # 上次全量备份
DATE=$(date +%Y%m%d_%H%M%S)
INCR_BACKUP_DIR="$BACKUP_DIR/incr_$DATE"
# 创建增量备份
xtrabackup --backup \
--target-dir=$INCR_BACKUP_DIR \
--incremental-basedir=$FULL_BACKUP_DIR \ # 基于哪个备份
--user=root \
--password=password
echo "✅ 增量备份成功:$INCR_BACKUP_DIR"
恢复数据
#!/bin/bash
# xtrabackup_restore.sh
BACKUP_DIR="/backup/xtrabackup"
FULL_BACKUP_DIR="$BACKUP_DIR/full_20241027"
INCR_BACKUP_DIR="$BACKUP_DIR/incr_20241028"
MYSQL_DATA_DIR="/var/lib/mysql"
# 1. 停止MySQL
systemctl stop mysqld
# 2. 清空数据目录
rm -rf $MYSQL_DATA_DIR/*
# 3. 准备全量备份
xtrabackup --prepare --apply-log-only \
--target-dir=$FULL_BACKUP_DIR
# 4. 应用增量备份
xtrabackup --prepare --apply-log-only \
--target-dir=$FULL_BACKUP_DIR \
--incremental-dir=$INCR_BACKUP_DIR
# 5. 最终准备
xtrabackup --prepare --target-dir=$FULL_BACKUP_DIR
# 6. 恢复数据
xtrabackup --copy-back --target-dir=$FULL_BACKUP_DIR
# 7. 修改权限
chown -R mysql:mysql $MYSQL_DATA_DIR
# 8. 启动MySQL
systemctl start mysqld
echo "✅ 恢复完成"
3. Binlog备份(持续备份)
开启binlog
# my.cnf
[mysqld]
log-bin=mysql-bin
binlog_format=ROW
expire_logs_days=7
max_binlog_size=100M
备份binlog
#!/bin/bash
# binlog_backup.sh
BINLOG_DIR="/var/lib/mysql"
BACKUP_DIR="/backup/binlog"
DATE=$(date +%Y%m%d)
# 刷新binlog(生成新文件)
mysqladmin -u root -p flush-logs
# 复制binlog文件
for file in $(ls $BINLOG_DIR/mysql-bin.* | grep -v ".index"); do
if [ ! -f "$BACKUP_DIR/$(basename $file)" ]; then
cp $file $BACKUP_DIR/
echo "✅ 备份binlog:$(basename $file)"
fi
done
# 删除30天前的binlog备份
find $BACKUP_DIR -name "mysql-bin.*" -mtime +30 -delete
恢复binlog
# 查看binlog内容
mysqlbinlog mysql-bin.000001
# 恢复指定时间段的binlog
mysqlbinlog --start-datetime="2024-10-27 10:00:00" \
--stop-datetime="2024-10-27 12:00:00" \
mysql-bin.000001 | mysql -u root -p
# 恢复指定位置的binlog
mysqlbinlog --start-position=1000 \
--stop-position=2000 \
mysql-bin.000001 | mysql -u root -p
📦 应用数据备份
1. 文件备份(rsync)
#!/bin/bash
# file_backup.sh
SOURCE_DIR="/var/www/html"
BACKUP_DIR="/backup/files"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="files_$DATE"
# 使用rsync同步文件
rsync -avz --delete \
--exclude="*.log" \
--exclude="temp/" \
--exclude=".git/" \
$SOURCE_DIR/ \
$BACKUP_DIR/$BACKUP_NAME/
if [ $? -eq 0 ]; then
echo "✅ 文件备份成功"
# 创建tar.gz压缩包
cd $BACKUP_DIR
tar -czf $BACKUP_NAME.tar.gz $BACKUP_NAME
rm -rf $BACKUP_NAME
echo "✅ 压缩完成:$BACKUP_NAME.tar.gz"
else
echo "❌ 文件备份失败"
exit 1
fi
2. Java应用备份(Spring Boot)
@Service
@Slf4j
public class DataBackupService {
@Value("${backup.dir}")
private String backupDir;
@Autowired
private DataSource dataSource;
/**
* 定时备份
*/
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点
public void autoBackup() {
try {
String backupFile = backup();
log.info("✅ 自动备份成功:{}", backupFile);
// 上传到云存储
uploadToOss(backupFile);
// 清理本地旧备份
cleanOldBackups();
} catch (Exception e) {
log.error("❌ 备份失败", e);
// 发送告警
alertService.sendAlert("数据备份失败:" + e.getMessage());
}
}
/**
* 执行备份
*/
public String backup() throws IOException, InterruptedException {
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
String fileName = "backup_" + date + ".sql.gz";
String filePath = backupDir + "/" + fileName;
// 构建mysqldump命令
String[] cmd = {
"/bin/sh",
"-c",
String.format(
"mysqldump -h%s -u%s -p%s --single-transaction --all-databases | gzip > %s",
getHost(),
getUsername(),
getPassword(),
filePath
)
};
// 执行命令
Process process = Runtime.getRuntime().exec(cmd);
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("备份失败,exitCode=" + exitCode);
}
log.info("✅ 备份完成:{}", filePath);
return filePath;
}
/**
* 上传到OSS
*/
private void uploadToOss(String localFile) {
String ossKey = "backup/" + new File(localFile).getName();
ossClient.putObject("my-bucket", ossKey, new File(localFile));
log.info("✅ 上传到OSS:{}", ossKey);
}
/**
* 清理本地旧备份(保留7天)
*/
private void cleanOldBackups() {
File backupFolder = new File(backupDir);
File[] files = backupFolder.listFiles((dir, name) -> name.startsWith("backup_"));
if (files != null) {
long sevenDaysAgo = System.currentTimeMillis() - 7 * 24 * 3600 * 1000L;
for (File file : files) {
if (file.lastModified() < sevenDaysAgo) {
file.delete();
log.info("✅ 删除旧备份:{}", file.getName());
}
}
}
}
/**
* 恢复数据
*/
public void restore(String backupFile) throws IOException, InterruptedException {
String[] cmd = {
"/bin/sh",
"-c",
String.format(
"gunzip < %s | mysql -h%s -u%s -p%s",
backupFile,
getHost(),
getUsername(),
getPassword()
)
};
Process process = Runtime.getRuntime().exec(cmd);
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("恢复失败,exitCode=" + exitCode);
}
log.info("✅ 恢复完成");
}
// 从数据源获取连接信息
private String getHost() {
// 解析JDBC URL
return "localhost";
}
private String getUsername() {
return "root";
}
private String getPassword() {
return "password";
}
}
☁️ 云备份方案
1. 备份到阿里云OSS
@Service
public class OssBackupService {
@Autowired
private OSS ossClient;
/**
* 上传备份到OSS
*/
public void uploadBackup(String localFile) {
String date = LocalDate.now().toString();
String ossKey = String.format("backup/%s/%s", date, new File(localFile).getName());
// 上传文件
PutObjectRequest request = new PutObjectRequest(
"my-bucket",
ossKey,
new File(localFile)
);
// 设置存储类型为归档存储(最便宜)
request.setStorageClass(StorageClass.Archive);
ossClient.putObject(request);
log.info("✅ 上传到OSS:{}", ossKey);
}
/**
* 从OSS下载备份
*/
public String downloadBackup(String ossKey) {
String localFile = "/tmp/" + new File(ossKey).getName();
// 归档存储需要先解冻
if (isArchive(ossKey)) {
restoreArchive(ossKey);
}
// 下载文件
ossClient.getObject(
new GetObjectRequest("my-bucket", ossKey),
new File(localFile)
);
log.info("✅ 从OSS下载:{}", ossKey);
return localFile;
}
/**
* 解冻归档文件
*/
private void restoreArchive(String ossKey) {
RestoreObjectRequest request = new RestoreObjectRequest("my-bucket", ossKey);
ossClient.restoreObject(request);
// 等待解冻完成(通常需要1小时)
log.info("⏳ 归档文件解冻中,请稍后...");
}
}
2. 备份到AWS S3
@Service
public class S3BackupService {
@Autowired
private AmazonS3 s3Client;
/**
* 上传到S3
*/
public void uploadToS3(String localFile) {
String key = "backup/" + LocalDate.now() + "/" + new File(localFile).getName();
// 上传文件
s3Client.putObject(
new PutObjectRequest("my-backup-bucket", key, new File(localFile))
.withStorageClass(StorageClass.StandardInfrequentAccess) // 低频访问存储
);
// 设置生命周期(30天后转为Glacier,90天后删除)
setLifecyclePolicy("my-backup-bucket");
log.info("✅ 上传到S3:{}", key);
}
/**
* 设置生命周期策略
*/
private void setLifecyclePolicy(String bucketName) {
BucketLifecycleConfiguration.Rule rule = new BucketLifecycleConfiguration.Rule()
.withId("backup-lifecycle")
.withPrefix("backup/")
.withStatus(BucketLifecycleConfiguration.ENABLED)
.withTransitions(Arrays.asList(
// 30天后转为Glacier
new BucketLifecycleConfiguration.Transition()
.withDays(30)
.withStorageClass(StorageClass.Glacier),
// 90天后删除
new BucketLifecycleConfiguration.Transition()
.withDays(90)
.withStorageClass(StorageClass.DeepArchive)
))
.withExpirationInDays(365); // 1年后彻底删除
BucketLifecycleConfiguration configuration = new BucketLifecycleConfiguration()
.withRules(Arrays.asList(rule));
s3Client.setBucketLifecycleConfiguration(bucketName, configuration);
}
}
🎯 备份验证
1. 自动化验证
@Service
public class BackupVerificationService {
/**
* 验证备份完整性
*/
public boolean verifyBackup(String backupFile) {
try {
// 1. 检查文件是否存在
File file = new File(backupFile);
if (!file.exists()) {
log.error("❌ 备份文件不存在");
return false;
}
// 2. 检查文件大小(不能为空)
if (file.length() == 0) {
log.error("❌ 备份文件为空");
return false;
}
// 3. 校验MD5
String md5 = calculateMD5(file);
log.info("备份文件MD5:{}", md5);
// 4. 尝试解压
if (backupFile.endsWith(".gz")) {
testGzipFile(file);
}
// 5. 尝试恢复到测试库
restoreToTestDatabase(backupFile);
log.info("✅ 备份验证通过");
return true;
} catch (Exception e) {
log.error("❌ 备份验证失败", e);
return false;
}
}
/**
* 计算MD5
*/
private String calculateMD5(File file) throws IOException {
try (InputStream is = new FileInputStream(file)) {
return DigestUtils.md5Hex(is);
}
}
/**
* 测试gzip文件
*/
private void testGzipFile(File file) throws IOException {
try (GZIPInputStream gis = new GZIPInputStream(new FileInputStream(file))) {
byte[] buffer = new byte[1024];
while (gis.read(buffer) != -1) {
// 读取测试
}
}
}
/**
* 恢复到测试数据库
*/
private void restoreToTestDatabase(String backupFile) throws Exception {
// 1. 创建测试数据库
String testDb = "backup_test_" + System.currentTimeMillis();
createDatabase(testDb);
try {
// 2. 恢复数据
restoreDatabase(testDb, backupFile);
// 3. 检查表数量
int tableCount = getTableCount(testDb);
if (tableCount == 0) {
throw new RuntimeException("恢复后表数量为0");
}
log.info("✅ 测试库恢复成功,表数量:{}", tableCount);
} finally {
// 4. 删除测试数据库
dropDatabase(testDb);
}
}
}
2. 定期演练
@Service
public class DisasterRecoveryDrillService {
/**
* 容灾演练
*/
@Scheduled(cron = "0 0 3 1 * ?") // 每月1号凌晨3点
public void drill() {
log.info("🔥 开始容灾演练...");
try {
// 1. 选择最近的备份
String backupFile = findLatestBackup();
// 2. 恢复到测试环境
restoreToTestEnv(backupFile);
// 3. 执行测试用例
runTestCases();
// 4. 生成演练报告
DrillReport report = generateReport();
// 5. 发送通知
notifyTeam(report);
log.info("✅ 容灾演练完成");
} catch (Exception e) {
log.error("❌ 容灾演练失败", e);
alertService.sendAlert("容灾演练失败:" + e.getMessage());
}
}
}
📊 备份监控
@Service
public class BackupMonitorService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 记录备份状态
*/
public void recordBackupStatus(String backupType, boolean success) {
String key = "backup:status:" + backupType;
BackupStatus status = new BackupStatus();
status.setBackupType(backupType);
status.setSuccess(success);
status.setTimestamp(LocalDateTime.now());
redisTemplate.opsForValue().set(key, JSON.toJSONString(status));
}
/**
* 检查备份健康状态
*/
@Scheduled(cron = "0 0 9 * * ?") // 每天早上9点
public void checkBackupHealth() {
Map<String, String> report = new HashMap<>();
// 1. 检查全量备份(每周一次)
BackupStatus fullBackup = getBackupStatus("full");
if (fullBackup == null || isOlderThan(fullBackup, 7)) {
report.put("全量备份", "❌ 超过7天未备份");
} else {
report.put("全量备份", "✅ 正常");
}
// 2. 检查增量备份(每天一次)
BackupStatus incrBackup = getBackupStatus("incremental");
if (incrBackup == null || isOlderThan(incrBackup, 1)) {
report.put("增量备份", "❌ 超过1天未备份");
} else {
report.put("增量备份", "✅ 正常");
}
// 3. 检查binlog备份(每小时一次)
BackupStatus binlogBackup = getBackupStatus("binlog");
if (binlogBackup == null || isOlderThan(binlogBackup, 0.042)) { // 1小时
report.put("Binlog备份", "❌ 超过1小时未备份");
} else {
report.put("Binlog备份", "✅ 正常");
}
// 发送报告
sendReport(report);
}
}
📝 总结
备份策略(3-2-1原则)
3份副本:原始数据 + 2份备份
2种介质:本地硬盘 + 云存储
1份异地:不同地域/机房
完整备份方案
┌──────────────────────────────────────────────────────────┐
│ 推荐备份方案 │
└──────────────────────────────────────────────────────────┘
每周日 00:00:
- 全量备份(xtrabackup)
- 上传到OSS(归档存储)
每天 02:00:
- 增量备份(xtrabackup)
- 保留7天
每小时:
- binlog备份
- 保留3天
实时:
- 主从同步
每月1号:
- 容灾演练
- 验证备份
保留策略:
- 全量备份:保留4周
- 增量备份:保留7天
- Binlog:保留3天
- 年度备份:永久保留
关键要点 🎯
- 定期备份 - 自动化脚本
- 多地备份 - 本地 + 云端
- 验证备份 - 定期测试恢复
- 监控告警 - 备份失败及时通知
- 演练恢复 - 每月容灾演练
数据无价,备份先行! 🎉🎉🎉