目的
为了后续迁移方便,方便恢复,防止意外,确保网站迁移时数据不会丢失。无论是因为技术故障、操作失误,还是遭遇黑客攻击,有备份在手,我们就能快速恢复博客,避免数据丢失带来的困扰。
备份方法
我将整个备份过程制作成了一个流程图,简明扼要地展示了每一步操作:
![图片[1]-定时备份网站数据,再也不怕被删库了~-明恒博客](https://www.zym88.cn/wp-content/uploads/2024/08/20240815084647176-2024081500464773-1024x159.png)
我的博客系统中主要是备份 mysql
和上传的 资源文件
,主要是针对这两方面进行备份
- mysql备份
- 通过mysql自带mysqldump进行备份
- 程序查询表的所有数据,模拟mysqldump备份
- 资源文件备份
- 压缩备份
mysql备份
mysql 最佳备份方式无疑是使用 mysql 自带的 mysqldump
工具。然而,当你的 MySQL 数据库和项目部署在不同的服务器上,或者使用 Docker 等容器化技术时,通过程序执行命令脚本可能无法正常使用。因此,可以通过模拟 mysqldump
备份的方式来实现 MySQL 的备份。下面我们将先分析 mysqldump
,然后通过代码实现备份。
基本备份命令
要备份一个MySQL数据库,可以使用以下命令:
mysqldump -u [username] -p [database_name] > [backup_file].sqlmysqldump -u [username] -p [database_name] > [backup_file].sqlmysqldump -u [username] -p [database_name] > [backup_file].sql
-u [username]
:数据库用户名-p
:提示输入数据库用户密码[database_name]
:要备份的数据库名称> [backup_file].sql
:备份文件的输出路径和文件名
示例:
mysqldump -u root -p mydatabase > mydatabase_backup.sqlmysqldump -u root -p mydatabase > mydatabase_backup.sqlmysqldump -u root -p mydatabase > mydatabase_backup.sql
恢复备份命令
从备份文件恢复数据库,可以使用以下命令:
mysql -u [username] -p [database_name] < [backup_file].sqlmysql -u [username] -p [database_name] < [backup_file].sqlmysql -u [username] -p [database_name] < [backup_file].sql
示例:
mysql -u root -p mydatabase < mydatabase_backup.sqlmysql -u root -p mydatabase < mydatabase_backup.sqlmysql -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; /*!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; /*!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`;-- -- Table structure for table `b_article_tag` -- DROP TABLE IF EXISTS `b_article_tag`;-- -- 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 */;/*!40101 SET @saved_cs_client = @@character_set_client */;/*!40101 SET @saved_cs_client = @@character_set_client */;
这是一条 MySQL 特定的命令,用于保存当前的字符集设置。
/*!50503 SET character_set_client = utf8mb4 */;/*!50503 SET character_set_client = utf8mb4 */;/*!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='文章标签关联';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='文章标签关联';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 */;/*!40101 SET character_set_client = @saved_cs_client */;/*!40101 SET character_set_client = @saved_cs_client */;
这条命令用于恢复先前保存的字符集设置。
表数据的备份
---- Dumping data for table `b_article_tag`--LOCK TABLES `b_article_tag` WRITE;-- -- Dumping data for table `b_article_tag` -- LOCK TABLES `b_article_tag` WRITE;-- -- Dumping data for table `b_article_tag` -- LOCK TABLES `b_article_tag` WRITE;
这部分用于备份表 b_article_tag
的数据。首先,它会锁定表,以确保在备份期间不会有其他写操作。
/*!40000 ALTER TABLE `b_article_tag` DISABLE KEYS */;/*!40000 ALTER TABLE `b_article_tag` DISABLE KEYS */;/*!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);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);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 */;/*!40000 ALTER TABLE `b_article_tag` ENABLE KEYS */;/*!40000 ALTER TABLE `b_article_tag` ENABLE KEYS */;
这条命令用于重新启用先前禁用的键。
UNLOCK TABLES;UNLOCK TABLES;UNLOCK TABLES;
最后,这条命令用于解锁表,恢复其正常的读写操作。
特定命令解释
在 MySQL 数据库的备份脚本中,有些命令以 /*!
开头并带有一组数字,用于版本控制和配置。这些命令在备份和恢复过程中确保了数据的一致性和效率,使 MySQL 能在不同版本间保持兼容,并根据需要调整设置。
特殊命令结构
这些命令的结构通常如下:
/*!<version_number> <command> */;/*!<version_number> <command> */;/*!<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 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 SET character_set_client = utf8mb4 */;/*!50503 SET character_set_client = utf8mb4 */;
- 版本号:50503
- 含义:这条命令将客户端字符集设置为
utf8mb4
,适用于 MySQL 5.5.3 及更高版本。 - 用途:在备份表结构时,确保使用
utf8mb4
字符集,以支持更多的字符编码,特别是表情符号等特殊字符。
/*!40101 SET character_set_client = @saved_cs_client */;/*!40101 SET character_set_client = @saved_cs_client */;/*!40101 SET character_set_client = @saved_cs_client */;
- 版本号:40101
- 含义:这条命令用于恢复先前保存的字符集设置。它会将
@saved_cs_client
的值重新赋给character_set_client
变量。 - 用途:在备份完成后恢复原来的字符集设置,确保系统环境的一致性。
/*!40000 ALTER TABLE \`b_article_tag` DISABLE KEYS */;/*!40000 ALTER TABLE \`b_article_tag` DISABLE KEYS */;/*!40000 ALTER TABLE \`b_article_tag` DISABLE KEYS */;
- 版本号:40000
- 含义:这条命令用于禁用表
b_article_tag
的键,适用于 MySQL 4.0 及更高版本。 - 用途:在批量插入数据之前禁用键,以加快插入速度。插入完成后,可以重新启用键。
/*!40000 ALTER TABLE \`b_article_tag` ENABLE KEYS */;/*!40000 ALTER TABLE \`b_article_tag` ENABLE KEYS */;/*!40000 ALTER TABLE \`b_article_tag` ENABLE KEYS */;
- 版本号:40000
- 含义:这条命令用于重新启用表
b_article_tag
的键,适用于 MySQL 4.0 及更高版本。 - 用途:在批量插入数据完成后重新启用键,以确保表的完整性和索引的有效性。
备份流程步骤
- 删除现有表(如果存在)。
- 保存并设置字符集。
- 创建表并定义其结构。
- 恢复字符集设置。
- 锁定表以防止写操作。
- 禁用键以加快数据插入速度。
- 插入所有数据。
- 重新启用键。
- 解锁表。
Java实现备份
以下是一个描述 MySQL 备份过程的横向流程图:
![图片[2]-定时备份网站数据,再也不怕被删库了~-明恒博客](https://www.zym88.cn/wp-content/uploads/2024/08/20240815085145915-2024081500514545-1024x336.png)
废话不多说,上代码
/*** 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数据备份 * @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数据备份 * @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时发生错误");}}/** * 执行导出的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时发生错误"); } }/** * 执行导出的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 zipPath 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>() {@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {// Create zip entryzos.putNextEntry(new ZipEntry(sourceDir.relativize(file).toString()));// Write file to ziptry (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;}@Overridepublic FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {if (dir.equals(sourceDir.resolve(skipDirectory))) {// Skip 指定 directoryreturn 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 + " 毫秒");}/** * 将指定文件夹下的所有文件和子文件夹打包到一个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 + " 毫秒"); }/** * 将指定文件夹下的所有文件和子文件夹打包到一个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.zipbackup.zip ├── blog.sql └── file.zipbackup.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>() {@Overridepublic 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;}@Overridepublic 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;}});}// 创建MultipartFileFileItem fileItem = new DiskFileItem("file", "application/zip", true, fileName + ".zip", (int) byteArrayOutputStream.size(), new File(System.getProperty("<a href="https://www.zym88.cn/tag/java" title="更多关于 java 的文章" target="_blank">java</a>.io.tmpdir")));try (OutputStream os = fileItem.getOutputStream()) {byteArrayOutputStream.writeTo(os);}// 创建MultipartFileMultipartFile multipartFile = new CommonsMultipartFile(fileItem);long endTime = System.currentTimeMillis();long duration = endTime - startTime;log.info("压缩操作耗时: " + duration + " 毫秒");return multipartFile;} catch (IOException e) {throw new ServiceException("压缩操作出错", e);}}/** * 将指定文件夹下的所有文件和子文件夹打包到一个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("<a href="https://www.zym88.cn/tag/java" title="更多关于 java 的文章" target="_blank">java</a>.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); } }/** * 将指定文件夹下的所有文件和子文件夹打包到一个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
上传到博客网盘备份文件中
![图片[3]-定时备份网站数据,再也不怕被删库了~-明恒博客](https://www.zym88.cn/wp-content/uploads/2024/08/20240815085443787-2024081500544351-1024x508.png)
![图片[4]-定时备份网站数据,再也不怕被删库了~-明恒博客](https://www.zym88.cn/wp-content/uploads/2024/08/20240815085450251-2024081500545059.png)
数据还原
还原就比较简单了,将之前备份好的数据,解压还原就可以。
系统的迁移也比较简单,点击备份还原,直接将备份文件上传到新的系统就可以了。
![图片[5]-定时备份网站数据,再也不怕被删库了~-明恒博客](https://www.zym88.cn/wp-content/uploads/2024/08/1723563473016.jpg)
上述实现都是基于我的个人博客项目,感兴趣的可直接获取源码查看,最后推荐大家Star一下我的博客项目
4 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
5 本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报。
6 本站资源大多存储在云盘,如发现链接失效,请联系我们我们会第一时间更新。
暂无评论内容