最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot集成PostgreSQL表級(jí)備份與恢復(fù)的實(shí)戰(zhàn)指南

 更新時(shí)間:2026年04月01日 09:18:19   作者:Mr.4567  
本文介紹了在企業(yè)級(jí)應(yīng)用中使用SpringBoot+PostgreSQL實(shí)現(xiàn)數(shù)據(jù)備份與恢復(fù)的方法,主要包括使用pg_dump導(dǎo)出數(shù)據(jù)、pg_restore恢復(fù)數(shù)據(jù)及ProcessBuilder調(diào)用系統(tǒng)命令,并詳細(xì)解釋了ProcessBuilder、pg_dump與pg_restore的用法、參數(shù)配置及常見(jiàn)問(wèn)題解決方法

一、概述

在企業(yè)級(jí)應(yīng)用開(kāi)發(fā)中,數(shù)據(jù)備份與恢復(fù)是必不可少的核心功能。常見(jiàn)的需求包括:

  • 用戶誤操作導(dǎo)致數(shù)據(jù)丟失,需要快速恢復(fù)
  • 數(shù)據(jù)遷移到不同的數(shù)據(jù)庫(kù)環(huán)境
  • 定時(shí)備份重要業(yè)務(wù)表
  • 提供數(shù)據(jù)導(dǎo)出功能給運(yùn)維人員

本文采用 Spring Boot + PostgreSQL 原生工具 的方案:

  • 使用 pg_dump 命令導(dǎo)出表結(jié)構(gòu)和數(shù)據(jù)
  • 使用 pg_restore 命令恢復(fù)數(shù)據(jù)
  • 通過(guò) Java 的 ProcessBuilder 調(diào)用系統(tǒng)命令

二、核心知識(shí)點(diǎn)詳解

2.1 ProcessBuilder 詳解

ProcessBuilder 是 Java 中用于創(chuàng)建操作系統(tǒng)進(jìn)程的類,它提供了一種更靈活、更可控的方式來(lái)執(zhí)行外部命令。

2.1.1 基本用法

// 創(chuàng)建 ProcessBuilder 實(shí)例
ProcessBuilder pb = new ProcessBuilder("pg_dump", "-h", "localhost");

// 設(shè)置環(huán)境變量
pb.environment().put("PGPASSWORD", "password");

// 合并錯(cuò)誤流(將錯(cuò)誤輸出合并到標(biāo)準(zhǔn)輸出)
pb.redirectErrorStream(true);

// 啟動(dòng)進(jìn)程
Process process = pb.start();

// 等待進(jìn)程結(jié)束
int exitCode = process.waitFor();

2.1.2 為什么要讀取輸出流

關(guān)鍵點(diǎn): 必須讀取進(jìn)程的輸出流,否則可能導(dǎo)致進(jìn)程阻塞。

// ? 錯(cuò)誤寫法:不讀取輸出流
Process process = pb.start();
int exitCode = process.waitFor(); // 可能永遠(yuǎn)阻塞

// ? 正確寫法:讀取輸出流
try (BufferedReader reader = new BufferedReader(
        new InputStreamReader(process.getInputStream()))) {
    String line;
    while ((line = reader.readLine()) != null) {
        log.info(line);  // 必須消費(fèi)掉輸出
    }
}

原因: 操作系統(tǒng)為進(jìn)程分配了有限的緩沖區(qū),當(dāng)緩沖區(qū)滿時(shí),進(jìn)程會(huì)阻塞等待緩沖區(qū)被清空。

2.1.3 環(huán)境變量設(shè)置

Map<String, String> env = pb.environment();
env.put("PGPASSWORD", "password");  // PostgreSQL 密碼
env.put("PGDATABASE", "mydb");       // 默認(rèn)數(shù)據(jù)庫(kù)

2.2 pg_dump 命令詳解

2.2.1 命令參數(shù)說(shuō)明

參數(shù)說(shuō)明示例
-h數(shù)據(jù)庫(kù)主機(jī)地址-h localhost
-p數(shù)據(jù)庫(kù)端口-p 5432
-U數(shù)據(jù)庫(kù)用戶名-U postgres
-d數(shù)據(jù)庫(kù)名稱-d mydb
-t指定要備份的表-t users
-F輸出格式(c=自定義格式)-F c
-f輸出文件路徑-f backup.dump

2.2.2 格式選擇

格式參數(shù)特點(diǎn)
自定義格式-F c壓縮、可并行恢復(fù)、可選擇性恢復(fù)
目錄格式-F d多文件輸出、支持并行
tar 格式-F t兼容性好、不支持并行
純文本默認(rèn)可讀性強(qiáng)、文件大、恢復(fù)慢

2.3 pg_restore 命令詳解

2.3.1 恢復(fù)參數(shù)說(shuō)明

參數(shù)說(shuō)明
--clean恢復(fù)前刪除已存在的數(shù)據(jù)庫(kù)對(duì)象
--if-exists與 --clean 配合,使用 IF EXISTS
--no-owner不恢復(fù)對(duì)象所有者
-t只恢復(fù)指定的表
-j并行恢復(fù)的作業(yè)數(shù)

2.4 @ConfigurationProperties 配置綁定

@Data
@Component
@ConfigurationProperties(prefix = "backup")
public class BackupProperties {

    /**
     * pg_restore 執(zhí)行文件路徑
     */
    private String pgRestorePath;

    /**
     * pg_dump 執(zhí)行文件路徑
     */
    private String pgDumpPath;

    /**
     * 數(shù)據(jù)庫(kù)地址
     */
    private String pgHost;

    /**
     * 數(shù)據(jù)庫(kù)端口
     */
    private String pgPort;

    /**
     * 數(shù)據(jù)庫(kù)用戶名
     */
    private String pgUserName;

    /**
     * 數(shù)據(jù)庫(kù)密碼
     */
    private String pgPassword;

    /**
     * 數(shù)據(jù)庫(kù)名
     */
    private String pgDbName;
}

application.yml 配置:

backup:
  pg-host: localhost
  pg-port: 5432
  pg-user-name: postgres
  pg-password: 123456
  pg-db-name: mydb
  pg-dump-path: /usr/bin/pg_dump
  pg-restore-path: /usr/bin/pg_restore

核心工具類

package com.example.backup;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.util.Map;

/**
 * PostgreSQL 表級(jí)備份恢復(fù)工具類
 * 
 * 功能說(shuō)明:
 * 1. 支持單張表的備份,生成 .dump 格式文件
 * 2. 支持從備份文件恢復(fù)表到數(shù)據(jù)庫(kù)
 * 3. 提供文件上傳恢復(fù)的 REST API 接口
 */
@Slf4j
@Component
public class BackUtils {

    @Autowired
    private BackupProperties backupProperties;

    // 臨時(shí)文件存儲(chǔ)目錄
    private static final String TEMP_FILE_DIR = System.getProperty("user.dir") + "/backups/";

    /**
     * 備份單張表
     * 
     * @param tableName 要備份的表名
     * @return 備份文件路徑,失敗返回 null
     */
    private String backupTable(String tableName) {
        // 1. 驗(yàn)證表名合法性,防止路徑遍歷和命令注入
        if (tableName == null || !tableName.matches("^[a-zA-Z0-9_.-]+$")) {
            log.error("無(wú)效的表名:{}", tableName);
            return null;
        }

        log.info("開(kāi)始備份表:{}", tableName);

        // 2. 創(chuàng)建備份目錄
        File backupDir = new File(TEMP_FILE_DIR);
        if (!backupDir.exists() && !backupDir.mkdirs()) {
            log.error("創(chuàng)建備份目錄失敗:{}", TEMP_FILE_DIR);
            return null;
        }

        // 3. 生成備份文件路徑
        String backupFileName = tableName + ".dump";
        File backupFile = new File(backupDir, backupFileName);
        String backupFilePath = backupFile.getAbsolutePath();

        // 4. 構(gòu)建 pg_dump 命令
        String[] command = {
                backupProperties.getPgDumpPath(),
                "-h", backupProperties.getPgHost(),
                "-p", backupProperties.getPgPort(),
                "-U", backupProperties.getPgUserName(),
                "-d", backupProperties.getPgDbName(),
                "-t", tableName,
                "-F", "c",              // 自定義格式
                "-f", backupFilePath
        };

        // 5. 執(zhí)行備份命令
        boolean success = executeCommand(command, backupProperties.getPgPassword());

        if (success) {
            long fileSize = backupFile.exists() ? backupFile.length() : 0;
            log.info("備份成功:{}, 文件大?。簕} 字節(jié)", backupFileName, fileSize);
            return backupFilePath;
        } else {
            log.error("備份失敗:{}, 命令:{}", tableName, String.join(" ", command));
            // 清理可能產(chǎn)生的不完整文件
            if (backupFile.exists()) {
                backupFile.delete();
            }
            return null;
        }
    }

    /**
     * 恢復(fù)表到目標(biāo)數(shù)據(jù)庫(kù)
     * 
     * @param file 上傳的備份文件(格式:表名_backup.dump)
     * @return API 響應(yīng)結(jié)果
     */
    public ApiResponse restoreTable(MultipartFile file) {
        // 1. 文件非空校驗(yàn)
        if (file == null) {
            log.warn("上傳文件為空");
            return ApiResponse.error("上傳文件為空");
        }

        String fileName = file.getOriginalFilename();
        if (fileName == null || fileName.isEmpty()) {
            log.warn("文件名為空");
            return ApiResponse.error("文件名為空");
        }

        // 2. 文件名安全性校驗(yàn)(防止路徑遍歷攻擊)
        if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
            log.warn("非法的文件名:{}", fileName);
            return ApiResponse.error("非法的文件名");
        }

        // 3. 文件格式校驗(yàn)
        if (!fileName.endsWith("_backup.dump")) {
            log.warn("文件格式不正確,期望以 _backup.dump 結(jié)尾:{}", fileName);
            return ApiResponse.error("文件格式不正確,期望以 _backup.dump 結(jié)尾");
        }

        // 4. 提取表名并校驗(yàn)
        String tableName = fileName.replaceAll("_backup\\.dump$", "");
        if (!isValidTableName(tableName)) {
            log.error("無(wú)效的表名:{}", tableName);
            return ApiResponse.error("無(wú)效的表名");
        }

        log.info("開(kāi)始恢復(fù)表:{} 到數(shù)據(jù)庫(kù):{}", tableName, backupProperties.getPgDbName());

        File tempFile = null;
        try {
            // 5. 創(chuàng)建臨時(shí)目錄
            File dir = new File(TEMP_FILE_DIR);
            if (!dir.exists()) {
                dir.mkdirs();
            }

            // 6. 將上傳文件保存到臨時(shí)文件
            tempFile = new File(dir, fileName);
            file.transferTo(tempFile);

            // 7. 構(gòu)建 pg_restore 恢復(fù)命令
            String[] command = {
                    backupProperties.getPgRestorePath(),
                    "-h", backupProperties.getPgHost(),
                    "-p", backupProperties.getPgPort(),
                    "-U", backupProperties.getPgUserName(),
                    "-d", backupProperties.getPgDbName(),
                    "--no-owner",           // 不恢復(fù)對(duì)象所有者
                    "--clean",              // 恢復(fù)前刪除已存在的對(duì)象
                    "--if-exists",          // 使用 IF EXISTS
                    tempFile.getAbsolutePath()
            };

            // 8. 執(zhí)行恢復(fù)命令
            boolean restoreSuccess = executeCommand(command, backupProperties.getPgPassword());

            if (restoreSuccess) {
                log.info("恢復(fù)成功:表 {} 已恢復(fù)到 {}", tableName, backupProperties.getPgDbName());
                return ApiResponse.success("恢復(fù)成功");
            } else {
                log.error("恢復(fù)失敗:表 {}", tableName);
                return ApiResponse.error("恢復(fù)失敗");
            }

        } catch (IOException e) {
            log.error("備份文件寫入臨時(shí)路徑失敗", e);
            return ApiResponse.error("備份文件寫入臨時(shí)路徑失敗");
        } finally {
            // 9. 清理臨時(shí)文件
            deleteTempFile(tempFile);
        }
    }

    /**
     * 驗(yàn)證表名是否合法
     * 
     * @param tableName 表名
     * @return 是否合法
     */
    private boolean isValidTableName(String tableName) {
        if (tableName == null || tableName.isEmpty()) {
            return false;
        }
        // 表名只能包含字母、數(shù)字、下劃線
        return tableName.matches("^[a-zA-Z0-9_]+$");
    }

    /**
     * 執(zhí)行系統(tǒng)命令
     * 
     * @param command  命令數(shù)組
     * @param password 數(shù)據(jù)庫(kù)密碼(通過(guò)環(huán)境變量傳遞)
     * @return 是否執(zhí)行成功
     */
    private boolean executeCommand(String[] command, String password) {
        ProcessBuilder processBuilder = new ProcessBuilder(command);

        // 設(shè)置密碼環(huán)境變量(pg_dump/pg_restore 通過(guò)此變量讀取密碼)
        Map<String, String> env = processBuilder.environment();
        env.put("PGPASSWORD", password);

        // 合并錯(cuò)誤流到標(biāo)準(zhǔn)輸出流,方便統(tǒng)一處理
        processBuilder.redirectErrorStream(true);

        try {
            log.info("執(zhí)行命令: {}", String.join(" ", command));

            Process process = processBuilder.start();

            // 讀取輸出流(必須讀取,否則進(jìn)程可能阻塞)
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    log.info("命令輸出: {}", line);
                }
            }

            // 等待命令執(zhí)行完成
            int exitCode = process.waitFor();
            log.info("命令執(zhí)行完成,退出碼: {}", exitCode);

            return exitCode == 0;

        } catch (Exception e) {
            log.error("命令執(zhí)行異常: {}", e.getMessage(), e);
            return false;
        }
    }

    /**
     * 刪除臨時(shí)文件
     * 
     * @param tempFile 臨時(shí)文件
     */
    private void deleteTempFile(File tempFile) {
        if (tempFile != null && tempFile.exists()) {
            try {
                Files.delete(tempFile.toPath());
                log.info("臨時(shí)文件已刪除: {}", tempFile.getAbsolutePath());
            } catch (IOException e) {
                log.warn("刪除臨時(shí)文件失敗: {}", e.getMessage());
            }
        }
    }
}

三、常見(jiàn)問(wèn)題

3.1 找不到命令

Cannot run program "pg_dump": error=2, No such file or directory

解決:配置絕對(duì)路徑

backup:
  pg-dump-path: /usr/local/bin/pg_dump  # Linux
  # Windows: C:\\Program Files\\PostgreSQL\\14\\bin\\pg_dump.exe

3.2 版本不兼容

unrecognized configuration parameter "idle_in_transaction_session_timeout"

解決:添加兼容參數(shù)

"--no-owner", "--no-privileges", "--no-sync"

以上就是SpringBoot集成PostgreSQL表級(jí)備份與恢復(fù)的實(shí)戰(zhàn)指南的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot PostgreSQL表級(jí)備份與恢復(fù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論

兴文县| 德令哈市| 遂溪县| 绵竹市| 松溪县| 龙口市| 新乡县| 石首市| 长岛县| 禹州市| 梁山县| 滨海县| 涞源县| 柯坪县| 合江县| 开封县| 桦甸市| 黑河市| 专栏| 蓝田县| 乌鲁木齐市| 萝北县| 固原市| 益阳市| 永济市| 怀来县| 长宁县| 临颍县| 苏州市| 茂名市| 昌吉市| 栖霞市| 安宁市| 微山县| 道孚县| 宜兴市| 镇远县| 南陵县| 丽江市| 辽宁省| 林州市|