应用系统中最重要的东西就是 "数据",定期备份数据的重要性就不言而喻了。本文将会带你了解如何在 Spring Boot 应用中实现定期备份 MySQL 数据库。
mysqldump {#mysqldump}
MYSQL本身提供了一个工具 mysqldump
,通过它可以完成数据库的备份。
简单来说就是一个命令,可以把数据库中的表结构和数据,以 SQL 语句的形式输出到标准输出:
mysqldump -u[用户名] -p[密码] [数据库] > [备份的SQL文件]
注意,命令中的 >
符号在linux下是重定向符,在这里的意思就是把标准输出重定向到文件。
例如,备份 demo
库到 ~/mysql.sql
,用户名和密码都是 root
:
mysqldump -uroot -proot demo > ~/mysql.sql
mysqldump 的详细文档:https://dev.mysql.com/doc/refman/en/mysqldump.html
创建应用 {#创建应用}
创建任意 Spring Boot 应用,并添加 commons-exec
依赖。
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-exec -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>1.3</version>
</dependency>
由于我们的备份是通过新启动一个子进程调用 mysqldump
来完成,所以建议使用 apache 的 commons-exec
库。它的使用比较简单,且设计合理,包含了子进程超时控制,异步执行等等功能。
应用配置 {#应用配置}
spring:
# 基本的数据源配置
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8&allowMultiQueries=true
username: root
password: root
app:
# 备份配置
backup:
# 备份数据库
db: "demo"
# 备份文件存储目录
dir: "backups"
# 备份文件最多保留时间。如,5分钟:5m、12小时:12h、1天:1d
max-age: 3m
如上,我们配置了基本的数据源。以及自定义的 "备份配置",其中指定了备份文件的存储目录,要备份的数据库以及备份文件滚动存储的最大保存时间。
数据备份 {#数据备份}
BackupService {#backupservice}
创建 BackupService
服务类,用于备份服务。如下:
package cn.springdoc.demo.service;
import java.io.OutputStream;
import java.io.BufferedOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.PumpStreamHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
*
* 数据库备份服务
*
*/
@Component
public class BackupService {
static final Logger log = LoggerFactory.getLogger(BackupService.class);
// 用户名
@Value("${spring.datasource.username}")
private String username;
// 密码
@Value("${spring.datasource.password}")
private String password;
// 备份数据库
@Value("${app.backup.db}")
private String db;
// 备份目录
@Value("${app.backup.dir}")
private String dir;
// 最大备份文件数量
@Value("${app.backup.max-age}")
private Duration maxAge;
// 锁,防止并发备份
private Lock lock = new ReentrantLock();
// 日期格式化
private DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd-HHmmss.SSS");
/**
* 备份文件
* @return
* @throws Exception
*/
public Path backup() throws Exception {
if (!this.lock.tryLock()) {
throw new Exception("备份任务进行中!");
}
try {
LocalDateTime now = LocalDateTime.now();
Path dir = Paths.get(this.dir);
// 备份的SQL文件
Path sqlFile = dir.resolve(Path.of(now.format(formatter) + ".sql"));
if (Files.exists(sqlFile)) {
// 文件已经存在,则添加后缀
for (int i = 1; i >= 1; i ++) {
sqlFile = dir.resolve(Path.of(now.format(formatter) + "-" + i + ".sql"));
if (!Files.exists(sqlFile)) {
break;
}
}
}
// 初始化目录
if (!Files.isDirectory(sqlFile.getParent())) {
Files.createDirectories(sqlFile.getParent());
}
// 创建备份文件文件
Files.createFile(sqlFile);
// 标准流输出的内容就是 SQL 的备份内容
try (OutputStream stdOut = new BufferedOutputStream(
Files.newOutputStream(sqlFile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) {
// 监视狗。执行超时时间,1小时
ExecuteWatchdog watchdog = new ExecuteWatchdog(TimeUnit.HOURS.toMillis(1));
// 子进程执行器
DefaultExecutor defaultExecutor = new DefaultExecutor();
// defaultExecutor.setWorkingDirectory(null); // 工作目录
defaultExecutor.setWatchdog(watchdog);
defaultExecutor.setStreamHandler(new PumpStreamHandler(stdOut, System.err));
// 进程执行命令
CommandLine commandLine = new CommandLine("mysqldump");
commandLine.addArgument("-u" + this.username); // 用户名
commandLine.addArgument("-p" + this.password); // 密码
commandLine.addArgument(this.db); // 数据库
log.info("备份 SQL 数据");
// 同步执行,阻塞直到子进程执行完毕。
int exitCode = defaultExecutor.execute(commandLine);
if (defaultExecutor.isFailure(exitCode)) {
throw new Exception("备份任务执行异常:exitCode=" + exitCode);
}
}
if (this.maxAge.isPositive() && !this.maxAge.isZero()) {
for (Path file : Files.list(dir).toList()) {
// 获取文件的创建时间
LocalDateTime createTime = LocalDateTime.ofInstant(Files.readAttributes(file, BasicFileAttributes.class).creationTime().toInstant(), ZoneId.systemDefault());
if (createTime.plus(this.maxAge).isBefore(now)) {
log.info("删除过期文件:{}", file.toAbsolutePath().toString());
// 删除文件
Files.delete(file);
}
}
}
return sqlFile;
} finally {
this.lock.unlock();
}
}
}
如上,我们在 Service 类中通过 @Value
注入了 application.yaml
文件中的配置信息。使用 ReentrantLock
锁来保证备份任务不会被并发执行。备份文件的名称使用 yyyy-MM-dd-HHmmss.SSS
格式,包含了年月日时分秒以及毫秒,如:2023-10-22-095300.857.sql
。如果文件名称冲突,则在末尾递增编号。
使用 commons-exec
启动新进程,调用 mysqldump
执行备份,备份成功后,尝试删除备份目录下那些已经 "过期" 的备份文件,从而达到滚动存储的目的。
备份成功后,返回 SQL 备份文件的 Path
对象。
测试 {#测试}
新建测试类,测试 BackupService
的备份方法:
package cn.springdoc.demo.test;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import cn.springdoc.demo.service.BackupService;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class DemoApplicationTests {
static final Logger log = LoggerFactory.getLogger(DemoApplicationTests.class);
@Autowired
BackupService backupService;
@Test
public void test() throws Exception {
Path path = this.backupService.backup();
log.info("备份文件:{}", path.toAbsolutePath().toString());
}
}
运行测试,输出日志如下:
2023-10-22T09:56:59.913+08:00 INFO 15352 --- [ main] c.springdoc.demo.service.BackupService : 备份 SQL 数据
2023-10-22T09:57:00.062+08:00 INFO 15352 --- [ main] c.s.demo.test.DemoApplicationTests : 备份文件:D:\eclipse\eclipse-jee-2023-09-R-win32-x86_64\app\springdoc-demo\backups\2023-10-22-095659.912.sql
查看该备份文件:
备份成功,只需要把该 SQL 文件通过客户端导入,即可恢复数据。
定时备份 {#定时备份}
配合 spring-task
就可以实现定时备份。
启用定时任务 {#启用定时任务}
在启动类上添加注解 @EnableScheduling
以启用定时任务。
package cn.springdoc.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
定时备份实现 {#定时备份实现}
创建 BackupTask
任务类,定时执行备份任务。
package cn.springdoc.demo.task;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import cn.springdoc.demo.service.BackupService;
@Component
public class BackupTask {
static final Logger log = LoggerFactory.getLogger(BackupTask.class);
@Autowired
private BackupService backupService;
// 1 分钟执行一次
@Scheduled(fixedRate = 1, timeUnit = TimeUnit.MINUTES)
public void backup () {
try {
Path file = this.backupService.backup();
log.info("备份成功:{}", file.toAbsolutePath().toString());
} catch (Exception e) {
log.error("备份任务执行异常: {}", e.getMessage());
}
}
}
通过 @Scheduled
注解指定执行周期。在这里为了简单,设置的是 1 分钟执行一次。如果你需要在具体的时间执行备份任务,可以使 cron
表达式,如:@Scheduled(cron = "0 0 2 1/1 * ? ")
表示每天凌晨 2 点执行备份任务。
总结 {#总结}
通过 mysqldump
进行备份的 限制 就是 应用和数据库必须在同一个机器上,适用于资源有限、刚起步的小项目。