定时备份网站数据,再也不怕被删库了~

定时备份网站数据,再也不怕被删库了~

当前文章收录状态:
未收录

目的

为了后续迁移方便,方便恢复,防止意外,确保网站迁移时数据不会丢失。无论是因为技术故障、操作失误,还是遭遇黑客攻击,有备份在手,我们就能快速恢复博客,避免数据丢失带来的困扰。

备份方法

我将整个备份过程制作成了一个流程图,简明扼要地展示了每一步操作:

图片[1]-定时备份网站数据,再也不怕被删库了~-明恒博客

我的博客系统中主要是备份 mysql和上传的 资源文件,主要是针对这两方面进行备份

  1. mysql备份
    • 通过mysql自带mysqldump进行备份
    • 程序查询表的所有数据,模拟mysqldump备份
  2. 资源文件备份
    • 压缩备份

mysql备份

mysql 最佳备份方式无疑是使用 mysql 自带的 mysqldump 工具。然而,当你的 MySQL 数据库和项目部署在不同的服务器上,或者使用 Docker 等容器化技术时,通过程序执行命令脚本可能无法正常使用。因此,可以通过模拟 mysqldump 备份的方式来实现 MySQL 的备份。下面我们将先分析 mysqldump,然后通过代码实现备份。

基本备份命令

要备份一个MySQL数据库,可以使用以下命令:

mysqldump -u [username] -p [database_name] > [backup_file].sql
mysqldump -u [username] -p [database_name] > [backup_file].sql
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
mysqldump -u root -p mydatabase > mydatabase_backup.sql
mysqldump -u root -p mydatabase > mydatabase_backup.sql

恢复备份命令

从备份文件恢复数据库,可以使用以下命令:

mysql -u [username] -p [database_name] < [backup_file].sql
mysql -u [username] -p [database_name] < [backup_file].sql
mysql -u [username] -p [database_name] < [backup_file].sql

示例:

mysql -u root -p mydatabase < mydatabase_backup.sql
mysql -u root -p mydatabase < mydatabase_backup.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;
/*!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 及更高版本。
  • 用途:在批量插入数据完成后重新启用键,以确保表的完整性和索引的有效性。

备份流程步骤

  1. 删除现有表(如果存在)。
  2. 保存并设置字符集。
  3. 创建表并定义其结构。
  4. 恢复字符集设置。
  5. 锁定表以防止写操作。
  6. 禁用键以加快数据插入速度。
  7. 插入所有数据。
  8. 重新启用键。
  9. 解锁表。

Java实现备份

以下是一个描述 MySQL 备份过程的横向流程图:

图片[2]-定时备份网站数据,再也不怕被删库了~-明恒博客

废话不多说,上代码

/**
* 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 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 + " 毫秒");
}
/** * 将指定文件夹下的所有文件和子文件夹打包到一个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
backup.zip
├── blog.sql
└── file.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("<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("<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]-定时备份网站数据,再也不怕被删库了~-明恒博客
图片[4]-定时备份网站数据,再也不怕被删库了~-明恒博客

数据还原

还原就比较简单了,将之前备份好的数据,解压还原就可以。

系统的迁移也比较简单,点击备份还原,直接将备份文件上传到新的系统就可以了。

图片[5]-定时备份网站数据,再也不怕被删库了~-明恒博客

上述实现都是基于我的个人博客项目,感兴趣的可直接获取源码查看,最后推荐大家Star一下我的博客项目

© 版权声明
THE END
我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=270198dipw4ko
点赞7赞赏 分享
Nothing can't be figured out. The past just can't be reached again.
没有什么过不去,只是再也回不去
评论 抢沙发

请登录后发表评论

    暂无评论内容