数据库备份基础框架构建

This commit is contained in:
diantu
2023-03-08 18:19:38 +08:00
parent 4b0ec31e6f
commit 353bc4ae97
11 changed files with 742 additions and 94 deletions

View File

@@ -51,7 +51,12 @@
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View File

@@ -0,0 +1,39 @@
package com.zyplayer.doc.db.framework.db.enums;
import lombok.AllArgsConstructor;
/**
* 备份类型枚举类
*
* @author diantu
* @since 2023年3月8日
*/
@AllArgsConstructor
public enum BackupCategoryEnum {
/**
* 手动备份
*/
MANUAL("0", "手动备份"),
/**
* 自动备份
*/
AUTO("1", "自动备份");
/**
* 编码
*/
private final String code;
/**
* 信息
*/
private final String msg;
public String getCode() {
return code;
}
public String getMsg() {
return msg;
}
}

View File

@@ -0,0 +1,34 @@
package com.zyplayer.doc.db.framework.db.job;
import com.alibaba.fastjson.JSONObject;
import com.zyplayer.doc.data.repository.manage.entity.BackupLog;
import com.zyplayer.doc.db.framework.db.vo.BackupJobVO;
import com.zyplayer.doc.db.framework.utils.DatabaseBackupUtils;
import com.zyplayer.doc.db.framework.utils.QuartzManagerUtils;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 数据库备份定时任务执行
*
* @author diantu
* @since 2023年2月8日
*/
public class BackupJob implements Job {
@Autowired
private DatabaseBackupUtils databaseBackupUtils;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 解析参数
BackupJobVO jobVO = JSONObject.parseObject(context.getJobDetail().getJobDataMap().getString(QuartzManagerUtils.PARAM_KEY), BackupJobVO.class);
// TODO 保存备份记录
BackupLog backupLog = new BackupLog();
//开始备份
databaseBackupUtils.saveBackUp(jobVO, backupLog);
}
}

View File

@@ -0,0 +1,62 @@
package com.zyplayer.doc.db.framework.db.vo;
import lombok.Data;
/**
* 备份任务数据
*
* @author diantu
* @since 2023年3月3日
*/
@Data
public class BackupJobVO {
/**
* 数据源ID
*/
private Integer dbId;
/**
* 地址
*/
private String host;
/**
* 端口号
*/
private String port;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 数据库名
*/
private String databaseName;
/**
* 数据库表名
*/
private String[] tables;
/**
* 备份形式
* 0-只备份表结构
* 1-只备份表数据
* 2-备份表结构+表数据
*/
private Integer dataType;
/**
* 是否压缩
*/
private Boolean isCompress = false;
/**
* 是否上传
*/
private Boolean isUpload = false;
/**
* 是否删除
*/
private Boolean isDelete = false;
}

View File

@@ -0,0 +1,23 @@
package com.zyplayer.doc.db.framework.db.vo;
import lombok.Data;
import java.io.File;
/**
* 备份响应数据
*
* @author diantu
* @since 2023年3月3日
*/
@Data
public class BackupRespVO {
private String msg;
private File file;
public boolean isSuccess() {
return null != this.file && 0 < this.file.length();
}
}

View File

@@ -1,7 +1,14 @@
package com.zyplayer.doc.db.framework.utils;
import cn.hutool.core.util.IdUtil;
import com.alibaba.excel.util.DateUtils;
import com.zyplayer.doc.data.repository.manage.entity.BackupLog;
import com.zyplayer.doc.data.utils.CleanInputCache;
import com.zyplayer.doc.db.framework.db.vo.BackupJobVO;
import com.zyplayer.doc.db.framework.db.vo.BackupRespVO;
import org.springframework.scheduling.annotation.Async;
import java.io.*;
import java.time.LocalDate;
import java.util.Date;
/**
* 数据库备份恢复工具类
@@ -12,113 +19,211 @@ import java.time.LocalDate;
public class DatabaseBackupUtils {
/**
* MySQL数据库导出
*
* @param hostIP MySQL数据库所在服务器地址IP
* @param userName 进入数据库所需要的用户名
* @param password 进入数据库所需要的密码
* @param savePath 数据库导出文件保存路径
* @param fileName 数据库导出文件文件名
* @param databaseName 要导出的数据库名
* @return 返回true表示导出成功否则返回false。
* 项目路径
*/
public static boolean exportDatabaseForMysql(String mysqldumpPath, String hostIP, String userName, String password, String savePath, String fileName, String databaseName) throws InterruptedException {
File saveFile = new File(savePath);
if (!saveFile.exists()) {// 如果目录不存在
saveFile.mkdirs();// 创建文件夹
}
if(!savePath.endsWith(File.separator)){
savePath = savePath + File.separator;
}
public static final String PROJECT_PATH = System.getProperty("user.dir");
PrintWriter printWriter = null;
BufferedReader bufferedReader = null;
try {
/**
* 当前系统类型
*/
public static final String OS_NAME = System.getProperty("os.name");
printWriter = new PrintWriter(new OutputStreamWriter(new FileOutputStream(savePath + fileName), "utf8"));
Process process = Runtime.getRuntime().exec(" "+mysqldumpPath+"/mysqldump -h" + hostIP + " -u" + userName + " -p" + password + " " + databaseName);
InputStreamReader inputStreamReader = new InputStreamReader(process.getInputStream(), "utf8");
bufferedReader = new BufferedReader(inputStreamReader);
String line;
while((line = bufferedReader.readLine())!= null){
printWriter.println(line);
}
printWriter.flush();
if(process.waitFor() == 0){//0 表示线程正常终止。
return true;
}
}catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bufferedReader != null) {
bufferedReader.close();
}
if (printWriter != null) {
printWriter.close();
}
} catch (IOException e) {
e.printStackTrace();
/**
* 编码格式
*/
public static final String CHAR_SET = OS_NAME.startsWith("Win") ? "GBK" : "UTF-8";
/**
* 异步执行备份任务,更新备份记录
*
* @param jobVO 备份参数
* @param backupLog 备份记录
*/
@Async
public void saveBackUp(BackupJobVO jobVO, BackupLog backupLog) {
// 执行备份
BackupRespVO respVO = doBackup(jobVO);
backupLog.setStatus(respVO.isSuccess() ? (jobVO.getIsUpload() ? 1 : 2) : -1);
backupLog.setMsg(respVO.getMsg());
// 备份成功
if (respVO.isSuccess()) {
// 文件相对路径
backupLog.setFilePath(respVO.getFile().getPath().replace(PROJECT_PATH + File.separator, ""));
backupLog.setFileSize(respVO.getFile().length());
}
// 备份失败
if (!respVO.isSuccess()) {
if (null != respVO.getFile()) {
respVO.getFile().delete();
}
}
return false;
// 计算耗时
backupLog.setEndTime(new Date());
backupLog.setSpendTime(backupLog.getEndTime().getTime() - backupLog.getStartTime().getTime());
//@TODO 存入备份记录信息
// 备份文件上传至文件服务器
if (!jobVO.getIsUpload()) {
return;
}
//@TODO 备份文件上传至文件服务器
}
public static void backup(String savePath) {
File file = new File(savePath);
if (!file.exists()) {
file.mkdirs();
}
String fileName = savePath + "/" + LocalDate.now() + ".sql";
/** 默认使用linux*/
//String cmdPrefix = "/bin/sh -c ";
String c1 = "/bin/sh";
String c2 = "-c";
String os_name = System.getProperty("os.name");
// 判断是否是windows系统
if (os_name.toLowerCase().startsWith("win")){
//cmdPrefix = "cmd /c ";
c1 = "cmd";
c2 = "/c";
}
//参考示例:# /usr/local/mysql/bin/mysqldump -uroot -p123456 -P3306 shuju > shuju.sql
String cmd = "mysqldump" // mysqldump的绝对路径配置环境变量直接写mysqldump即可
+ " -h" + "127.0.0.1" // 数据库端口号
+ " -P" + "3306" // 数据库端口号
+ " -u" + "root" // 数据库用户名
+ " -p" + "root" // 数据库密码
+ " " + "zyplayer_doc_manage" // 数据库名
+ " > " + fileName; // 最终写入的文件路径
/**
* 执行备份命令(mysql)
*/
public static BackupRespVO doBackup(BackupJobVO jobVO) {
// 返回对象
BackupRespVO respVO = new BackupRespVO();
try {
System.out.println("第一个参数 " + c1);
System.out.println("第二个参数 " + c2);
System.out.println("具体命令 " + cmd);
// 当前年月日yyyyMMdd
String date = DateUtils.format(new Date(),"yyyyMMdd");
// 文件目录
String path = PROJECT_PATH + File.separator + "static" + File.separator + date + File.separator;
// 文件名
String fileName = IdUtil.fastSimpleUUID() + ".sql" + (jobVO.getIsCompress() ? ".gz" : "");
// 创建文件
File file = new File(path, fileName);
// 路径不存在,则新建
if (!file.getParentFile().exists()) {
boolean flag = file.getParentFile().mkdirs();
if (!flag) {
respVO.setMsg("文件夹创建失败");
return respVO;
}
}
respVO.setFile(file);
// shell 命令脚本
String[] commands = createBackupCommandForMysql(jobVO, path, fileName);
//log.error("数据库备份START" + LocalDateTime.now());
/**
* exec重载方法有一个参数的window下执行正常linux下无法完成备份。
* 使用多参数重载方法都可以正常备份
*/
Process process = Runtime.getRuntime().exec(new String[]{c1, c2, cmd});
process.waitFor();
//log.error("数据库备份END" + LocalDateTime.now());
ProcessBuilder processBuilder = new ProcessBuilder();
Process process = processBuilder.command(commands).start();
//Process process = Runtime.getRuntime().exec(commands);
// 创建一个线程类来不停地来读出Process调用脚本的输出数据防止缓冲区被缓冲数据塞满而线程阻塞
new CleanInputCache(process.getInputStream(), "info", CHAR_SET, null).start();
// 错误信息
StringBuilder msg = new StringBuilder("" + jobVO.getDatabaseName() + "】备份失败,原因:");
new CleanInputCache(process.getErrorStream(), "error", CHAR_SET, msg).start();
// 备份成功
if (process.waitFor() == 0) {
respVO.setMsg("备份成功");
}
// 备份失败
else {
respVO.setMsg(msg.toString());
}
} catch (Exception e) {
e.printStackTrace();
//log.error("数据库备份失败:{}", e.getMessage());
respVO.setMsg("" + jobVO.getDatabaseName() + "】备份失败,原因:" + e.getMessage());
}
return respVO;
}
/**
* 创建命令头(只支持Windows平台和Linux平台)
*/
public static String[] createBaseCommand() {
// shell 命令
String[] commands = new String[3];
if (OS_NAME.startsWith("Win")) {
commands[0] = "cmd.exe";
commands[1] = "/c";
} else {
commands[0] = "/bin/sh";
commands[1] = "-c";
}
return commands;
}
public static void main(String[] args){
try {
if (exportDatabaseForMysql("C:/Program Files/MySQL/MySQL Server 5.7/bin","127.0.0.1", "root", "root", "D:/backupDatabase", "2023-2-8.sql", "zyplayer_doc_manage")) {
System.out.println("数据库成功备份!!!");
} else {
System.out.println("数据库备份失败!!!");
/**
* 拼接备份命令(mysql)
*
* @param jobVO 备份参数
* @param path 备份文件目录
* @param fileName 备份文件名
*/
public static String[] createBackupCommandForMysql(BackupJobVO jobVO, String path, String fileName) {
String[] commands = createBaseCommand();
// 拼接命令
StringBuilder mysqldump = new StringBuilder();
//需配置mysql安装路径bin目录环境变量
mysqldump.append("mysqldump");
mysqldump.append(" --opt");
// 用户,密码
mysqldump.append(" --user=").append(jobVO.getUsername());
mysqldump.append(" --password=\"").append(jobVO.getPassword()).append("\"");
// ip端口
mysqldump.append(" --host=").append(jobVO.getHost());
mysqldump.append(" --port=").append(jobVO.getPort());
// 使用的连接协议包括tcp, socket, pipe, memory
mysqldump.append(" --protocol=tcp");
// 设置默认字符集默认值为utf8
mysqldump.append(" --default-character-set=utf8");
// 在导出数据之前提交一个BEGIN SQL语句BEGIN 不会阻塞任何应用程序且能保证导出时数据库的一致性状态
mysqldump.append(" --single-transaction=TRUE");
// 导出存储过程以及自定义函数
mysqldump.append(" --routines");
// 导出事件
mysqldump.append(" --events");
// 只备份表结构
if (null != jobVO.getDataType()) {
if (0 == jobVO.getDataType()) {
mysqldump.append(" --no-data");
}
// 只备份表数据
else if (1 == jobVO.getDataType()) {
mysqldump.append(" --no-create-info");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
//backup("D:/backupDatabase");
// 数据库名
mysqldump.append(" ").append(jobVO.getDatabaseName());
// 数据表名
if (null != jobVO.getTables() && 0 < jobVO.getTables().length) {
for (String item : jobVO.getTables()) {
mysqldump.append(" ").append(item);
}
}
// 保存文件路径
if (jobVO.getIsCompress()) {
// gzip压缩
mysqldump.append(" | gzip");
}
mysqldump.append(" > ").append(path).append(fileName);
commands[2] = mysqldump.toString();
return commands;
}
/**
* 拼接备份命令(oracle)
* @param jobVO 备份参数
* @param path 备份文件目录
* @param fileName 备份文件名
*/
public static String[] createBackupCommandForOracle(BackupJobVO jobVO, String path, String fileName) {
String[] commands = createBaseCommand();
// 拼接命令
StringBuilder oracledump = new StringBuilder();
//需配置mysql安装路径bin目录环境变量
oracledump.append("expdp ");
// 用户,密码
oracledump.append(jobVO.getUsername());
oracledump.append("/").append(jobVO.getPassword());
//数据库名
oracledump.append("@").append(jobVO.getDatabaseName());
//@TODO 待完善
return commands;
}
}

View File

@@ -0,0 +1,142 @@
package com.zyplayer.doc.db.framework.utils;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* quartz工具类
*
* @author diantu
* @since 2023年3月8日
*/
@Slf4j
@Component
public class QuartzManagerUtils {
/**
* 参数传递key
*/
public static final String PARAM_KEY = "params";
/**
* 执行任务类名
*/
public static final String CLASS_NAME = "com.zyplayer.doc.db.framework.db.job.BackupJob";
/**
* 程序调度器
*/
@Autowired
private Scheduler scheduler;
/**
* 添加定时任务
*/
public void add(Integer id, String cronExpression, String param, Boolean status) {
try {
// 构建job信息
JobDetail jobDetail = JobBuilder.newJob(getClass(CLASS_NAME).getClass()).withIdentity(getKey(id)).usingJobData(PARAM_KEY, param).build();
// 表达式调度构建器(即任务执行的时间)
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
// 按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getKey(id)).withSchedule(scheduleBuilder).build();
// 创建定时任务
scheduler.scheduleJob(jobDetail, trigger);
// 停止
if (!status) {
stop(id);
}
} catch (Exception e) {
log.error("添加定时任务失败:{}", e.getMessage());
}
}
/**
* 编辑定时任务
*/
public void update(Integer id, String cronExpression, String param, Boolean status) {
try {
// 判断是否存在,存在先删除
if (scheduler.checkExists(JobKey.jobKey(getKey(id)))) {
scheduler.deleteJob(JobKey.jobKey(getKey(id)));
}
// 再创建
add(id, cronExpression, param, status);
} catch (Exception e) {
log.error("修改定时任务失败:{}", e.getMessage());
}
}
/**
* 暂停任务
*/
public void stop(Integer id) {
try {
scheduler.pauseJob(JobKey.jobKey(getKey(id)));
} catch (SchedulerException e) {
// 暂停定时任务失败
log.error("暂停定时任务失败:{}", e.getMessage());
}
}
/**
* 恢复任务
*/
public void start(Integer id) {
try {
scheduler.resumeJob(JobKey.jobKey(getKey(id)));
} catch (SchedulerException e) {
// 暂停定时任务失败
log.error("启动定时任务失败:{}", e.getMessage());
}
}
/**
* 立即执行一次
*/
public void run(Integer id) {
try {
scheduler.triggerJob(JobKey.jobKey(getKey(id)));
} catch (SchedulerException e) {
// 暂停定时任务失败
log.error("执行定时任务失败:{}", e.getMessage());
}
}
/**
* 删除定时任务
*/
public void delete(Integer id) {
try {
// 停止触发器
scheduler.pauseTrigger(TriggerKey.triggerKey(getKey(id)));
// 移除触发器
scheduler.unscheduleJob(TriggerKey.triggerKey(getKey(id)));
// 删除任务
scheduler.deleteJob(JobKey.jobKey(getKey(id)));
} catch (Exception e) {
log.error("删除定时任务失败:{}", e.getMessage());
}
}
/**
* 根据类名获取类
*/
private Job getClass(String className) throws Exception {
Class<?> class1 = Class.forName(className);
return (Job) class1.newInstance();
}
/**
* 拼接key
*/
public String getKey(Integer id) {
return "dbBackUp-" + id;
}
}