51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

在 Spring Boot 中实现定时备份 MySQL 数据库

应用系统中最重要的东西就是 "数据",定期备份数据的重要性就不言而喻了。本文将会带你了解如何在 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(&quot;${spring.datasource.username}&quot;)
private String username;

// 密码
@Value(&quot;${spring.datasource.password}&quot;)
private String password; 

// 备份数据库
@Value(&quot;${app.backup.db}&quot;)
private String db;

// 备份目录
@Value(&quot;${app.backup.dir}&quot;)
private String dir;

// 最大备份文件数量
@Value(&quot;${app.backup.max-age}&quot;)
private Duration maxAge;

// 锁,防止并发备份
private Lock lock = new ReentrantLock();

// 日期格式化
private DateTimeFormatter formatter = DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd-HHmmss.SSS&quot;);

/**
 * 备份文件
 * @return
 * @throws Exception 
 */
public Path backup() throws Exception {
    
    if (!this.lock.tryLock()) {
        throw new Exception(&quot;备份任务进行中!&quot;);
    }
    
    try {
        
        LocalDateTime now = LocalDateTime.now();
        
        Path dir = Paths.get(this.dir);

        // 备份的SQL文件
        Path sqlFile = dir.resolve(Path.of(now.format(formatter) + &quot;.sql&quot;));
        
        if (Files.exists(sqlFile)) {
            // 文件已经存在,则添加后缀
            for (int i = 1; i &gt;= 1; i ++) {
                sqlFile = dir.resolve(Path.of(now.format(formatter) + &quot;-&quot; + i + &quot;.sql&quot;));
                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(&quot;mysqldump&quot;);
            commandLine.addArgument(&quot;-u&quot; + this.username); 	// 用户名
            commandLine.addArgument(&quot;-p&quot; + this.password); 	// 密码
            commandLine.addArgument(this.db); 				// 数据库

            log.info(&quot;备份 SQL 数据&quot;);

            // 同步执行,阻塞直到子进程执行完毕。
            int exitCode = defaultExecutor.execute(commandLine);

            if (defaultExecutor.isFailure(exitCode)) {
                throw new Exception(&quot;备份任务执行异常:exitCode=&quot; + exitCode);
            }
        }

        
        if (this.maxAge.isPositive() &amp;&amp; !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(&quot;删除过期文件:{}&quot;, 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(&quot;备份文件:{}&quot;, 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

查看该备份文件:

mysqldump 生成的 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(&quot;备份成功:{}&quot;, file.toAbsolutePath().toString());
        
    } catch (Exception e) {
        log.error(&quot;备份任务执行异常: {}&quot;, e.getMessage());
    }
}

}

通过 @Scheduled 注解指定执行周期。在这里为了简单,设置的是 1 分钟执行一次。如果你需要在具体的时间执行备份任务,可以使 cron 表达式,如:@Scheduled(cron = "0 0 2 1/1 * ? ") 表示每天凌晨 2 点执行备份任务。

总结 {#总结}

通过 mysqldump 进行备份的 限制 就是 应用和数据库必须在同一个机器上,适用于资源有限、刚起步的小项目。

赞(3)
未经允许不得转载:工具盒子 » 在 Spring Boot 中实现定时备份 MySQL 数据库