💾 数据备份和恢复:数据的保险箱

23 阅读8分钟

难度系数:⭐⭐⭐⭐⭐
实用指数:💯💯💯💯💯


📖 开篇:一次删库跑路的惨案

凌晨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天
  - 年度备份:永久保留

关键要点 🎯

  1. 定期备份 - 自动化脚本
  2. 多地备份 - 本地 + 云端
  3. 验证备份 - 定期测试恢复
  4. 监控告警 - 备份失败及时通知
  5. 演练恢复 - 每月容灾演练

数据无价,备份先行! 🎉🎉🎉