目的
为了后续迁移方便,方便恢复,防止意外,确保网站迁移时数据不会丢失。无论是因为技术故障、操作失误,还是遭遇黑客攻击,有备份在手,我们就能快速恢复博客,避免数据丢失带来的困扰。
备份方法
我将整个备份过程制作成了一个流程图,简明扼要地展示了每一步操作:
我的博客系统中主要是备份 mysql
和上传的 资源文件
,主要是针对这两方面进行备份
- mysql备份
- 通过mysql自带mysqldump进行备份
- 程序查询表的所有数据,模拟mysqldump备份
- 资源文件备份
- 压缩备份
mysql备份
mysql 最佳备份方式无疑是使用 mysql 自带的 mysqldump
工具。然而,当你的 MySQL 数据库和项目部署在不同的服务器上,或者使用 Docker 等容器化技术时,通过程序执行命令脚本可能无法正常使用。因此,可以通过模拟 mysqldump
备份的方式来实现 MySQL 的备份。下面我们将先分析 mysqldump
,然后通过代码实现备份。
基本备份命令
要备份一个MySQL数据库,可以使用以下命令:
mysqldump -u [username] -p [database_name] > [backup_file].sql
-u [username]
:数据库用户名-p
:提示输入数据库用户密码[database_name]
:要备份的数据库名称> [backup_file].sql
:备份文件的输出路径和文件名
示例:
mysqldump -u root -p mydatabase > mydatabase_backup.sql
恢复备份命令
从备份文件恢复数据库,可以使用以下命令:
mysql -u [username] -p [database_name] < [backup_file].sql
示例:
mysql -u root -p mydatabase < mydatabase_backup.sql
备份文件结构
通过分析 mysqldump
生成的备份文件,可以看到其中的表结构和数据。下面是对表 b_article_tag
的完整备份结构:
--
-- Table structure for table b_article_tag
--
DROP TABLE IF EXISTS b_article_tag;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE b_article_tag (
id int NOT NULL AUTO_INCREMENT COMMENT 'id',
article_id int NOT NULL COMMENT '文章id',
tag_id int NOT NULL COMMENT '标签id',
PRIMARY KEY (id) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='文章标签关联';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table b_article_tag
--
LOCK TABLES b_article_tag WRITE;
/*!40000 ALTER TABLE b_article_tag DISABLE KEYS */;
INSERT INTO b_article_tag VALUES (1,1,1),(2,2,2),(3,3,1),(4,4,2),(5,5,1),(6,6,2),(7,7,1),(8,8,2),(9,9,1),(10,10,2),(12,14,3);
/*!40000 ALTER TABLE b_article_tag ENABLE KEYS */;
UNLOCK TABLES;
让我们逐行解释这个脚本的含义。
表结构的备份
--
-- Table structure for table `b_article_tag`
--
DROP TABLE IF EXISTS `b_article_tag`;
这部分是对表 b_article_tag
的结构进行备份。首先,它会删除现有的同名表(如果存在),以确保在恢复时不会发生冲突。
/*!40101 SET @saved_cs_client = @@character_set_client */;
这是一条 MySQL 特定的命令,用于保存当前的字符集设置。
/*!50503 SET character_set_client = utf8mb4 */;
这也是一条 MySQL 特定的命令,用于将字符集设置为 utf8mb4
。
CREATE TABLE `b_article_tag` (
`id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
`article_id` int NOT NULL COMMENT '文章id',
`tag_id` int NOT NULL COMMENT '标签id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='文章标签关联';
这段代码用于创建表 b_article_tag
,并定义了表的结构:
id
列是主键,自动递增。article_id
和tag_id
列是用于关联文章和标签的外键。- 表使用 InnoDB 引擎,默认字符集为
utf8mb4
,使用utf8mb4_0900_ai_ci
校对规则,行格式为DYNAMIC
,并带有表的注释。
/*!40101 SET character_set_client = @saved_cs_client */;
这条命令用于恢复先前保存的字符集设置。
表数据的备份
--
-- Dumping data for table `b_article_tag`
--
LOCK TABLES `b_article_tag` WRITE;
这部分用于备份表 b_article_tag
的数据。首先,它会锁定表,以确保在备份期间不会有其他写操作。
/*!40000 ALTER TABLE `b_article_tag` DISABLE KEYS */;
这条命令用于临时禁用键,以加快数据插入速度。
INSERT INTO `b_article_tag` VALUES
(1,1,1),
(2,2,2),
(3,3,1),
(4,4,2),
(5,5,1),
(6,6,2),
(7,7,1),
(8,8,2),
(9,9,1),
(10,10,2),
(12,14,3);
这段代码用于插入表 b_article_tag
的所有数据。每个括号中的值表示一行数据。
/*!40000 ALTER TABLE `b_article_tag` ENABLE KEYS */;
这条命令用于重新启用先前禁用的键。
UNLOCK TABLES;
最后,这条命令用于解锁表,恢复其正常的读写操作。
特定命令解释
在 MySQL 数据库的备份脚本中,有些命令以 /*!
开头并带有一组数字,用于版本控制和配置。这些命令在备份和恢复过程中确保了数据的一致性和效率,使 MySQL 能在不同版本间保持兼容,并根据需要调整设置。
特殊命令结构
这些命令的结构通常如下:
/*!<version_number> <command> */;
<version_number>
:这是一个版本号,表示该命令将在特定版本的 MySQL 及其之后的版本中执行。<command>
:这是实际的 SQL 命令。
例如,/*!40101 SET @saved_cs_client = @@character_set_client */;
命令表示,如果 MySQL 服务器的版本是 4.1.1 或更高版本,则执行 SET @saved_cs_client = @@character_set_client
。
具体命令解释
/*!40101 SET @saved_cs_client = @@character_set_client */;
- 版本号:40101
- 含义:这条命令用于保存当前的字符集设置。它会将当前的
character_set_client
变量的值存储在用户变量@saved_cs_client
中,以便稍后恢复。 - 用途:在修改字符集之前保存当前的字符集设置,确保在备份和恢复过程中字符集的一致性。
/*!50503 SET character_set_client = utf8mb4 */;
- 版本号:50503
- 含义:这条命令将客户端字符集设置为
utf8mb4
,适用于 MySQL 5.5.3 及更高版本。 - 用途:在备份表结构时,确保使用
utf8mb4
字符集,以支持更多的字符编码,特别是表情符号等特殊字符。
/*!40101 SET character_set_client = @saved_cs_client */;
- 版本号:40101
- 含义:这条命令用于恢复先前保存的字符集设置。它会将
@saved_cs_client
的值重新赋给character_set_client
变量。 - 用途:在备份完成后恢复原来的字符集设置,确保系统环境的一致性。
/*!40000 ALTER TABLE \`b_article_tag` DISABLE KEYS */;
- 版本号:40000
- 含义:这条命令用于禁用表
b_article_tag
的键,适用于 MySQL 4.0 及更高版本。 - 用途:在批量插入数据之前禁用键,以加快插入速度。插入完成后,可以重新启用键。
/*!40000 ALTER TABLE \`b_article_tag` ENABLE KEYS */;
- 版本号:40000
- 含义:这条命令用于重新启用表
b_article_tag
的键,适用于 MySQL 4.0 及更高版本。 - 用途:在批量插入数据完成后重新启用键,以确保表的完整性和索引的有效性。
备份流程步骤
- 删除现有表(如果存在)。
- 保存并设置字符集。
- 创建表并定义其结构。
- 恢复字符集设置。
- 锁定表以防止写操作。
- 禁用键以加快数据插入速度。
- 插入所有数据。
- 重新启用键。
- 解锁表。
Java实现备份
以下是一个描述 MySQL 备份过程的横向流程图:
废话不多说,上代码
/**
* sql数据备份
* @param path 文件路径
*/
private void backupSQL(String path) {
try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(path + BLOG_SQL)))) {
//获取所有表名
List<String> tableNames = backupMapper.showTables();
for (String table : tableNames) {
// 获取表结构
Map<String, String> createTableResult = backupMapper.showCreateTable(table);
String createTableSql = createTableResult.get("Create Table");
// 写入表结构
writer.println("--");
writer.println("-- Table structure for table `" + table + "`");
writer.println("--");
writer.println("DROP TABLE IF EXISTS `" + table + "`;");
writer.println("/*!40101 SET @saved_cs_client = @@character_set_client */;");
writer.println("/*!50503 SET character_set_client = utf8mb4 */;");
writer.println(createTableSql + ";");
writer.println("/*!40101 SET character_set_client = @saved_cs_client */;");
writer.println();
// 运用URI类解析并拆解衔接地址,从头拼装
URI databaseUrl = new URI(url.replace("jdbc:", ""));
// 得到衔接地址中的库名
String databaseName = databaseUrl.getPath().substring(1);
// 获取表的所有列名
List<String> columns = backupMapper.getTableColumns(databaseName, table);
// 获取表数据
List<LinkedHashMap<String, Object>> rows = backupMapper.selectAll(table);
// 确保每一行数据包含所有列名,且列顺序与 columns 一致
for (LinkedHashMap<String, Object> row : rows) {
LinkedHashMap<String, Object> orderedRow = new LinkedHashMap<>();
for (String column : columns) {
orderedRow.put(column, row.getOrDefault(column, null));
}
row.clear();
row.putAll(orderedRow);
}
if (!rows.isEmpty()) {
writer.println("--");
writer.println("-- Dumping data for table `" + table + "`");
writer.println("--");
writer.println("LOCK TABLES `" + table + "` WRITE;");
writer.println("/*!40000 ALTER TABLE `" + table + "` DISABLE KEYS */;");
writer.print("INSERT INTO `" + table + "` VALUES ");
for (int i = 0; i < rows.size(); i++) {
LinkedHashMap<String, Object> row = rows.get(i);
StringBuilder rowSql = new StringBuilder("(");
for (Object value : row.values()) {
if (value == null) {
rowSql.append("NULL,");
} else {
if (value instanceof Long || value instanceof Integer) {
rowSql.append(value).append(",");
} else if (value instanceof LocalDateTime) {
rowSql.append("'");
rowSql.append(LocalDateTime.parse(value.toString()).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
rowSql.append("'");
rowSql.append(",");
} else {
rowSql.append("'");
rowSql.append(valueResult(value));
rowSql.append("',");
}
}
}
rowSql.deleteCharAt(rowSql.length() - 1);
rowSql.append(")");
if (i < rows.size() - 1) {
rowSql.append(",");
} else {
rowSql.append(";");
}
writer.print(rowSql.toString());
}
writer.println();
writer.println("/*!40000 ALTER TABLE `" + table + "` ENABLE KEYS */;");
writer.println("UNLOCK TABLES;");
writer.println();
}
}
writer.flush();
} catch (IOException e) {
log.error("备份SQL异常:", e);
throw new ServiceException("备份SQL出现意外");
} catch (URISyntaxException e) {
log.error("数据库衔接URL格局过错!", e);
throw new ServiceException("数据库衔接URL格局过错!");
}
}
/**
* 对字符数据处理
*
* @param value
* @return
*/
public static String valueResult(Object value) {
var str = "";
if (StringUtil.isNotBlank(value.toString()) && isValidJsonObject(value.toString())) {
JSONObject jsonObject = JSONObject.parseObject(value.toString());
str = jsonObject.toJSONString()
.replace("'", "\'")
.replace("'", "\\'")
.replace("\"", "\\\"")
.replace("\\n", "\\\\n");
return str;
}
str = value.toString().replace("'", "\\'").replace("\n", "\\n");
return str;
}
/**
* 验证是不是Json
*
* @param jsonString
* @return
*/
public static boolean isValidJsonObject(String jsonString) {
try {
JSONObject.parseObject(jsonString);
return true;
} catch (Exception e) {
return false;
}
}
注意:对转义的数据进行处理,否则后续执行入库脚本有问题。由于生成sql有误导致我研究了大半天😥
Java备份还原
/**
* 执行导出的mysqldump脚本
*
* @param scriptFilePath SQL脚本文件路径
* @param dbUrl 数据库连接URL
* @param dbUser 数据库用户名
* @param dbPassword 数据库密码
*/
public static void executeSqlScript(String scriptFilePath, String dbUrl, String dbUser, String dbPassword) {
try (Connection connection = DriverManager.getConnection(dbUrl, dbUser, dbPassword);
BufferedReader reader = new BufferedReader(new FileReader(scriptFilePath))) {
StringBuilder sql = new StringBuilder();
String line;
try (Statement statement = connection.createStatement()) {
while ((line = reader.readLine()) != null) {
// 跳过注释和空行
if (line.trim().isEmpty() || line.trim().startsWith("--") || line.trim().startsWith("/*")) {
continue;
}
sql.append(line);
// 检查是否以分号结束,即一条SQL语句结束
if (line.trim().endsWith(";")) {
log.info("执行SQL: " + sql);
statement.execute(sql.toString());
// 清空StringBuilder,准备下一条SQL语句
sql.setLength(0);
}
}
}
} catch (IOException e) {
log.error("读取SQL文件时发生错误: " + e);
throw new ServiceException("读取SQL文件时发生错误");
} catch (SQLException e) {
log.error("执行SQL时发生错误: " + e);
throw new ServiceException("执行SQL时发生错误");
}
}
注意:如果无法正常执行请检查生成sql是否有错误。
资源文件备份
当前是将文件排除备份目录进行压缩,压缩完成将文件上传到博客项目中。这个备份就比较简单粗暴,直接代码压缩。
/**
* 将指定文件夹下的所有文件和子文件夹打包到一个ZIP文件
*
* @param sourceDirPath 源文件夹路径
* @param skipDirectory 跳过文件夹路径
* @param zipFilePath 目标ZIP文件路径
* @return
*/
public static void compressDirectories(String sourceDirPath, String skipDirectory, String zipFilePath) {
long startTime = System.currentTimeMillis();
log.info("正在进行压缩操作,耐心等待");
try {
// Create a temporary file for the zip
Path zipPath = Files.createFile(Paths.get(zipFilePath));
try (ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(zipPath)))) {
// 设置压缩级别
zos.setLevel(Deflater.DEFAULT_COMPRESSION);
Path sourceDir = Paths.get(sourceDirPath);
Files.walkFileTree(sourceDir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
// Create zip entry
zos.putNextEntry(new ZipEntry(sourceDir.relativize(file).toString()));
// Write file to zip
try (BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(file))) {
byte[] buffer = new byte[16384];
int length;
while ((length = bis.read(buffer)) > 0) {
zos.write(buffer, 0, length);
}
}
zos.closeEntry();
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (dir.equals(sourceDir.resolve(skipDirectory))) {
// Skip 指定 directory
return FileVisitResult.SKIP_SUBTREE;
}
if (!sourceDir.equals(dir)) {
zos.putNextEntry(new ZipEntry(sourceDir.relativize(dir).toString() + "/"));
zos.closeEntry();
}
return FileVisitResult.CONTINUE;
}
});
}
} catch (IOException e) {
throw new ServiceException("压缩操作出错", e);
}
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
log.info("压缩操作耗时: " + duration + " 毫秒");
}
数据整合
将mysql和资源文件整合到一个压缩包中,压缩包 backup.zip
的目录结构展示:
backup.zip
├── blog.sql
└── file.zip
备份文件都在一个文件夹下面,直接对当前文件夹中的数据进行打包,代码实现如下:
/**
* 将指定文件夹下的所有文件和子文件夹打包到一个ZIP文件,并转换为MultipartFile
*
* @param sourceDirPath 源文件夹路径
* @param fileName 文件名称
* @return 目标ZIP文件的MultipartFile表示
* @throws IOException 如果打包过程中发生错误
*/
public static MultipartFile zipDirectoryToMultipartFile(String sourceDirPath, String fileName) {
long startTime = System.currentTimeMillis();
try {
// 创建一个字节输出流来保存ZIP文件数据
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(byteArrayOutputStream)) {
Path sourcePath = Paths.get(sourceDirPath);
Files.walkFileTree(sourcePath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
ZipEntry zipEntry = new ZipEntry(sourcePath.relativize(file).toString());
zos.putNextEntry(zipEntry);
Files.copy(file, zos);
zos.closeEntry();
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (!sourcePath.equals(dir)) {
ZipEntry zipEntry = new ZipEntry(sourcePath.relativize(dir).toString() + "/");
zos.putNextEntry(zipEntry);
zos.closeEntry();
}
return FileVisitResult.CONTINUE;
}
});
}
// 创建MultipartFile
FileItem fileItem = new DiskFileItem("file", "application/zip", true, fileName + ".zip", (int) byteArrayOutputStream.size(), new File(System.getProperty("java.io.tmpdir")));
try (OutputStream os = fileItem.getOutputStream()) {
byteArrayOutputStream.writeTo(os);
}
// 创建MultipartFile
MultipartFile multipartFile = new CommonsMultipartFile(fileItem);
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
log.info("压缩操作耗时: " + duration + " 毫秒");
return multipartFile;
} catch (IOException e) {
throw new ServiceException("压缩操作出错", e);
}
}
将生成的MultipartFile
上传到博客网盘备份文件中
数据还原
还原就比较简单了,将之前备份好的数据,解压还原就可以。
系统的迁移也比较简单,点击备份还原,直接将备份文件上传到新的系统就可以了。
上述实现都是基于我的个人博客项目,感兴趣的可直接获取源码查看,最后推荐大家Star一下我的博客项目
4 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
5 本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报。
6 本站资源大多存储在云盘,如发现链接失效,请联系我们我们会第一时间更新。
暂无评论内容