目录
[2.1 ProcessBuilder 详解](#2.1 ProcessBuilder 详解)
[2.1.1 基本用法](#2.1.1 基本用法)
[2.1.2 为什么要读取输出流](#2.1.2 为什么要读取输出流)
[2.1.3 环境变量设置](#2.1.3 环境变量设置)
[2.2 pg_dump 命令详解](#2.2 pg_dump 命令详解)
[2.2.1 命令参数说明](#2.2.1 命令参数说明)
[2.2.2 格式选择](#2.2.2 格式选择)
[2.3 pg_restore 命令详解](#2.3 pg_restore 命令详解)
[2.3.1 恢复参数说明](#2.3.1 恢复参数说明)
[2.4 @ConfigurationProperties 配置绑定](#2.4 @ConfigurationProperties 配置绑定)
[3.1 找不到命令](#3.1 找不到命令)
[3.2 版本不兼容](#3.2 版本不兼容)
一、概述
在企业级应用开发中,数据备份与恢复是必不可少的核心功能。常见的需求包括:
-
用户误操作导致数据丢失,需要快速恢复
-
数据迁移到不同的数据库环境
-
定时备份重要业务表
-
提供数据导出功能给运维人员
本文采用 Spring Boot + PostgreSQL 原生工具 的方案:
-
使用
pg_dump命令导出表结构和数据 -
使用
pg_restore命令恢复数据 -
通过 Java 的
ProcessBuilder调用系统命令
二、核心知识点详解
2.1 ProcessBuilder 详解
ProcessBuilder 是 Java 中用于创建操作系统进程的类,它提供了一种更灵活、更可控的方式来执行外部命令。
2.1.1 基本用法
java
// 创建 ProcessBuilder 实例
ProcessBuilder pb = new ProcessBuilder("pg_dump", "-h", "localhost");
// 设置环境变量
pb.environment().put("PGPASSWORD", "password");
// 合并错误流(将错误输出合并到标准输出)
pb.redirectErrorStream(true);
// 启动进程
Process process = pb.start();
// 等待进程结束
int exitCode = process.waitFor();
2.1.2 为什么要读取输出流
关键点: 必须读取进程的输出流,否则可能导致进程阻塞。
java
// ❌ 错误写法:不读取输出流
Process process = pb.start();
int exitCode = process.waitFor(); // 可能永远阻塞
// ✅ 正确写法:读取输出流
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
log.info(line); // 必须消费掉输出
}
}
原因: 操作系统为进程分配了有限的缓冲区,当缓冲区满时,进程会阻塞等待缓冲区被清空。
2.1.3 环境变量设置
java
Map<String, String> env = pb.environment();
env.put("PGPASSWORD", "password"); // PostgreSQL 密码
env.put("PGDATABASE", "mydb"); // 默认数据库
2.2 pg_dump 命令详解
2.2.1 命令参数说明
| 参数 | 说明 | 示例 |
|---|---|---|
-h |
数据库主机地址 | -h localhost |
-p |
数据库端口 | -p 5432 |
-U |
数据库用户名 | -U postgres |
-d |
数据库名称 | -d mydb |
-t |
指定要备份的表 | -t users |
-F |
输出格式(c=自定义格式) | -F c |
-f |
输出文件路径 | -f backup.dump |
2.2.2 格式选择
| 格式 | 参数 | 特点 |
|---|---|---|
| 自定义格式 | -F c |
压缩、可并行恢复、可选择性恢复 |
| 目录格式 | -F d |
多文件输出、支持并行 |
| tar 格式 | -F t |
兼容性好、不支持并行 |
| 纯文本 | 默认 | 可读性强、文件大、恢复慢 |
2.3 pg_restore 命令详解
2.3.1 恢复参数说明
| 参数 | 说明 |
|---|---|
--clean |
恢复前删除已存在的数据库对象 |
--if-exists |
与 --clean 配合,使用 IF EXISTS |
--no-owner |
不恢复对象所有者 |
-t |
只恢复指定的表 |
-j |
并行恢复的作业数 |
2.4 @ConfigurationProperties 配置绑定
java
@Data
@Component
@ConfigurationProperties(prefix = "backup")
public class BackupProperties {
/**
* pg_restore 执行文件路径
*/
private String pgRestorePath;
/**
* pg_dump 执行文件路径
*/
private String pgDumpPath;
/**
* 数据库地址
*/
private String pgHost;
/**
* 数据库端口
*/
private String pgPort;
/**
* 数据库用户名
*/
private String pgUserName;
/**
* 数据库密码
*/
private String pgPassword;
/**
* 数据库名
*/
private String pgDbName;
}
application.yml 配置:
bash
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
核心工具类
java
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 表级备份恢复工具类
*
* 功能说明:
* 1. 支持单张表的备份,生成 .dump 格式文件
* 2. 支持从备份文件恢复表到数据库
* 3. 提供文件上传恢复的 REST API 接口
*/
@Slf4j
@Component
public class BackUtils {
@Autowired
private BackupProperties backupProperties;
// 临时文件存储目录
private static final String TEMP_FILE_DIR = System.getProperty("user.dir") + "/backups/";
/**
* 备份单张表
*
* @param tableName 要备份的表名
* @return 备份文件路径,失败返回 null
*/
private String backupTable(String tableName) {
// 1. 验证表名合法性,防止路径遍历和命令注入
if (tableName == null || !tableName.matches("^[a-zA-Z0-9_.-]+$")) {
log.error("无效的表名:{}", tableName);
return null;
}
log.info("开始备份表:{}", tableName);
// 2. 创建备份目录
File backupDir = new File(TEMP_FILE_DIR);
if (!backupDir.exists() && !backupDir.mkdirs()) {
log.error("创建备份目录失败:{}", TEMP_FILE_DIR);
return null;
}
// 3. 生成备份文件路径
String backupFileName = tableName + ".dump";
File backupFile = new File(backupDir, backupFileName);
String backupFilePath = backupFile.getAbsolutePath();
// 4. 构建 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. 执行备份命令
boolean success = executeCommand(command, backupProperties.getPgPassword());
if (success) {
long fileSize = backupFile.exists() ? backupFile.length() : 0;
log.info("备份成功:{}, 文件大小:{} 字节", backupFileName, fileSize);
return backupFilePath;
} else {
log.error("备份失败:{}, 命令:{}", tableName, String.join(" ", command));
// 清理可能产生的不完整文件
if (backupFile.exists()) {
backupFile.delete();
}
return null;
}
}
/**
* 恢复表到目标数据库
*
* @param file 上传的备份文件(格式:表名_backup.dump)
* @return API 响应结果
*/
public ApiResponse restoreTable(MultipartFile file) {
// 1. 文件非空校验
if (file == null) {
log.warn("上传文件为空");
return ApiResponse.error("上传文件为空");
}
String fileName = file.getOriginalFilename();
if (fileName == null || fileName.isEmpty()) {
log.warn("文件名为空");
return ApiResponse.error("文件名为空");
}
// 2. 文件名安全性校验(防止路径遍历攻击)
if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
log.warn("非法的文件名:{}", fileName);
return ApiResponse.error("非法的文件名");
}
// 3. 文件格式校验
if (!fileName.endsWith("_backup.dump")) {
log.warn("文件格式不正确,期望以 _backup.dump 结尾:{}", fileName);
return ApiResponse.error("文件格式不正确,期望以 _backup.dump 结尾");
}
// 4. 提取表名并校验
String tableName = fileName.replaceAll("_backup\\.dump$", "");
if (!isValidTableName(tableName)) {
log.error("无效的表名:{}", tableName);
return ApiResponse.error("无效的表名");
}
log.info("开始恢复表:{} 到数据库:{}", tableName, backupProperties.getPgDbName());
File tempFile = null;
try {
// 5. 创建临时目录
File dir = new File(TEMP_FILE_DIR);
if (!dir.exists()) {
dir.mkdirs();
}
// 6. 将上传文件保存到临时文件
tempFile = new File(dir, fileName);
file.transferTo(tempFile);
// 7. 构建 pg_restore 恢复命令
String[] command = {
backupProperties.getPgRestorePath(),
"-h", backupProperties.getPgHost(),
"-p", backupProperties.getPgPort(),
"-U", backupProperties.getPgUserName(),
"-d", backupProperties.getPgDbName(),
"--no-owner", // 不恢复对象所有者
"--clean", // 恢复前删除已存在的对象
"--if-exists", // 使用 IF EXISTS
tempFile.getAbsolutePath()
};
// 8. 执行恢复命令
boolean restoreSuccess = executeCommand(command, backupProperties.getPgPassword());
if (restoreSuccess) {
log.info("恢复成功:表 {} 已恢复到 {}", tableName, backupProperties.getPgDbName());
return ApiResponse.success("恢复成功");
} else {
log.error("恢复失败:表 {}", tableName);
return ApiResponse.error("恢复失败");
}
} catch (IOException e) {
log.error("备份文件写入临时路径失败", e);
return ApiResponse.error("备份文件写入临时路径失败");
} finally {
// 9. 清理临时文件
deleteTempFile(tempFile);
}
}
/**
* 验证表名是否合法
*
* @param tableName 表名
* @return 是否合法
*/
private boolean isValidTableName(String tableName) {
if (tableName == null || tableName.isEmpty()) {
return false;
}
// 表名只能包含字母、数字、下划线
return tableName.matches("^[a-zA-Z0-9_]+$");
}
/**
* 执行系统命令
*
* @param command 命令数组
* @param password 数据库密码(通过环境变量传递)
* @return 是否执行成功
*/
private boolean executeCommand(String[] command, String password) {
ProcessBuilder processBuilder = new ProcessBuilder(command);
// 设置密码环境变量(pg_dump/pg_restore 通过此变量读取密码)
Map<String, String> env = processBuilder.environment();
env.put("PGPASSWORD", password);
// 合并错误流到标准输出流,方便统一处理
processBuilder.redirectErrorStream(true);
try {
log.info("执行命令: {}", String.join(" ", command));
Process process = processBuilder.start();
// 读取输出流(必须读取,否则进程可能阻塞)
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
log.info("命令输出: {}", line);
}
}
// 等待命令执行完成
int exitCode = process.waitFor();
log.info("命令执行完成,退出码: {}", exitCode);
return exitCode == 0;
} catch (Exception e) {
log.error("命令执行异常: {}", e.getMessage(), e);
return false;
}
}
/**
* 删除临时文件
*
* @param tempFile 临时文件
*/
private void deleteTempFile(File tempFile) {
if (tempFile != null && tempFile.exists()) {
try {
Files.delete(tempFile.toPath());
log.info("临时文件已删除: {}", tempFile.getAbsolutePath());
} catch (IOException e) {
log.warn("删除临时文件失败: {}", e.getMessage());
}
}
}
}
三、常见问题
3.1 找不到命令
bash
Cannot run program "pg_dump": error=2, No such file or directory
解决:配置绝对路径
bash
backup:
pg-dump-path: /usr/local/bin/pg_dump # Linux
# Windows: C:\\Program Files\\PostgreSQL\\14\\bin\\pg_dump.exe
3.2 版本不兼容
bash
unrecognized configuration parameter "idle_in_transaction_session_timeout"
解决:添加兼容参数
bash
"--no-owner", "--no-privileges", "--no-sync"