通过commons-exec实现定时备份数据库

1、mysqldump

MYSQL本身提供了数据库备份工具 mysqldump。

可以把数据库中的表结构和数据,以 SQL 语句的形式输出到标准输出:

mysqldump -u[用户名] -p[密码] [数据库] > [备份的SQL文件]

例如,备份 demo 库到 ~/mysql.sql,用户名和密码都是 root:

mysqldump -uroot -proot demo  > ~/mysql.sql

mysqldump的详细文档

2、创建Springboot应用实现备份

创建 Spring Boot 应用,并添加 commons-exec 依赖。


<dependency>
    <groupId>org.apache.commonsgroupId>
    <artifactId>commons-execartifactId>
    <version>1.3version>
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/shushan?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8&allowMultiQueries=true
    username: root
    password: root

app:
  # 备份配置
  backup:
    # 备份数据库
    db: "shushan"
    # 备份文件存储目录
    dir: "backups"
    # 备份文件最多保留时间。如,5分钟:5m、12小时:12h、1天:1d
    max-age: 3m

如上,我们配置了基本的数据源。以及自定义的 “备份配置”,其中指定了备份文件的存储目录,要备份的数据库以及备份文件滚动存储的最大保存时间。


package com.kexuexiong.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();
        }
    }
}

使用 ReentrantLock 锁来保证备份任务不会被并发执行。备份文件的名称使用 yyyy-MM-dd-HHmmss.SSS 格式,包含了年月日时分秒以及毫秒,如:2023-10-22-095300.857.sql。如果文件名称冲突,则在末尾递增编号。

使用 commons-exec 启动新进程,调用 mysqldump 执行备份,备份成功后,尝试删除备份目录下那些已经 “过期” 的备份文件,从而达到滚动存储的目的。

备份成功后,返回 SQL 备份文件的 Path 对象。

你可能感兴趣的:(java,数据库)