应用系统中最重要的东西就是 "数据",定期备份数据的重要性就不言而喻了。本文将会带你了解如何在 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
进行备份的 限制 就是 应用和数据库必须在同一个机器上,适用于资源有限、刚起步的小项目。