springboot项目实现mysql备份功能的坑【更换方案】

话不多说,进入正题,有一个bug,一直提示找不到文件夹,一顿排查后发现是file.mkdir()只能在app容器中创建目录,无法在宿主机中创建,于是我将目录与宿主机做了一个卷映射。但现在最新的问题来了:
springboot项目实现mysql备份功能的坑【更换方案】_第1张图片
备份语句执行失败
springboot项目实现mysql备份功能的坑【更换方案】_第2张图片

开始排查

可疑点一:由于之前是可以执行的,尝试将卷映射去掉看看
失败:难道之前只是在命令行执行,并没有部署上去执行?果然不记录就会开始怀疑(想起来了,确实没有部署过)
找到原因:其实想想为什么提示docker不存在?是因为springboot的jar包中的容器中没有这个命令,
那么其实只要将这个命令放到宿主机中运行即可
可疑点二:代码添加了一行时区,虽然可能性不大,但我决定试试(现在想想,其实也是没有办法的办法了哈哈,但凡有点想法,也不至于这样排查啊)

解决方案

借助docker in docker 的思路应该可以解决
参考链接:在docker容器中调用和执行宿主机的docker

经过验证,还是不太行

尝试一:在容器内部运行命令(前置条件,需要对docker命令进行卷映射)

  1. 进入容器内部
docker exec -it app bash

命令:

docker exec -it mysql mysqldump -u用户名 -p密码 r_blog > /mnt/docker/mysql_backup/zzz.sql

发现可行,那具体什么原因呢?

怀疑是没有指定数据库端口号造成的,那就试试吧

docker exec -i mysql mysqldump -h81.71.87.241 -P端口号 -u用户名 -p密码 r_blog > /mnt/docker/mysql_backup/abca.sql 

结果:还是不行

尝试二 : 使用Thread.sleep(10);睡眠10秒试试,等待命令执行完毕,感觉不太可行,因为exec.waitFor() 已经在等待进程了,总之试试吧

发现是由于

尝试三: 发现是由于Runtime.getRuntime().exec(command)没有成功执行导致的,那么能不能想办法打印执行的具体信息呢?当然可以!

参考文档:java执行系统命令

       //读取命令的输出信息
        InputStream inputStream = process.getInputStream();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        process.waitFor();
        if (process.exitValue() != 0) {
            log.info("命令执行失败");
            //说明命令执行失败
            //可以进入到错误处理步骤中
        }
        log.info("打印进程输出信息====================================================");
        String s = null;
        while ((s = bufferedReader.readLine()) != null){
            log.info(s);
        }
        log.info("打印进程输出信息结束=====================================================");

结果发现,命令直接执行失败,没有任何信息打印出来

更换ProcessBuilder 的方式执行命令

ProcessBuilder processBuilder = new ProcessBuilder(lcommand);
        processBuilder.redirectErrorStream(true);
        Process process = processBuilder.start();

报错Caused by: java.io.IOException: error=2, No such file or directory,可能是容器内部缺少某个命令,先在win10中运行验证一下

发现是参数传错了…

ProcessBuilder processBuilder = new ProcessBuilder(lcommand);

参数应该是List或者String []的形式传入,改变参数发现windows执行成功,重新部署尝试运行…

还是不行,但有了新的发现,打印信息:the input device is not a TTY,开始调用百度api查阅信息…
真相渐渐浮出水面…发现执行定时任务是服务器的直接调用,不需要可交互的终端
-i : 可以进入容器内部
-t : 提供一个伪客户端

参考文档: 报错解决:the input device is not a TTY

去掉参数重试…

错误信息:Couldn't find table: ">",这又是为啥嘞?继续找寻原因…,怀疑是需要-i参数,进入容器内部,怀疑错误,发现是容器无法识别">"导致,换成参数-r即可
参考文档:报错: Couldn’t find table: “>”

执行后果然可行,不过新的问题出现:
mysqldump: Can't create/write to file '/mnt/docker/mysql_backup/2022-02-21-13-13-56.sql' (OS errno 2 - No such file or directory)
难道是权限问题?,我emo了…
将命令在服务器终端执行,发现报同样错误,然后确定是 -r参数导致的,这时候就要去看mysqldump文档了
首先查看下mysqldump的版本是否最新:
在这里插入图片描述

经过不断尝试,发现问题出在ProcessBuilder方法上面,它会将 数据库后的>字符识别成表,那么如何解决呢?

不断尝试后,决定去寻求大佬帮助!!!
错误总结:
目的背景:将springboot项目部署进docker容器中,备份docker中mysql容器中blog数据库
java中相关代码命令:

        ProcessBuilder processBuilder = new ProcessBuilder("docker","exec","mysql","mysqldump","-h81.71.87.241","-P3306","-uroot","-proot","blog",">","/mnt/docker/backup/aaa.sql");

错误码:

mysqldump: Couldn't find table: ">"

请教各位大佬如何解决这个问题。

找到一个解决思路:
https://blog.csdn.net/weixin_43625121/article/details/109203934
https://blog.51cto.com/u_14299052/2986120

估计是glib的坑,后续再解决吧

最终办法:改写成shell脚本,ProcessBuilder去调用它

总结

将命令存放在shell脚本,然后映射进springboot容器中,让springboot项目去调用它

  1. docker compose 目录映射
services:
  nginx:
   image: nginx
   container_name: nginx
   ports:
    - 80:80    
    - 443:443
   links:
    - app
   depends_on:
    - app
   volumes:
    - /mnt/docker/nginx/:/etc/nginx/
    - /mnt/raxcl/blog_admin:/raxcl/blog_admin
    - /mnt/raxcl/blog_view:/raxcl/blog_view
   network_mode: "bridge"
  app:
    image: app
    container_name: app
    volumes:
    # 映射shell脚本和mysql备份存放路径
     - /mnt/docker/mysql_backup:/mnt/docker/mysql_backup 
     # 映射docker命令
     - /var/run/docker.sock:/var/run/docker.sock
     - /usr/bin/docker:/usr/bin/docker
    expose:
      - "8090"
    network_mode: "bridge"

  1. 编写shell脚本:参考链接
#!/bin/bash
backName=$1
echo backName is $backName
docker exec mysql mysqldump -h81.71.87.241 -P3306 -uroot -proot r_blog > /mnt/docker/mysql_backup/sql_file/${backName}
  1. 验证脚本正确性:参考链接
    检查是否有语法错误-n:
bash -n script_name.sh

使用下面的命令来执行并调试 Shell 脚本-x:

bash -x script_name.sh
  1. 设置脚本权限:chmod 777 mysqlDump.sh
  2. java调用shell脚本
package cn.raxcl.task;

import cn.raxcl.exception.NotFoundException;
import com.alibaba.fastjson.JSON;
import com.qiniu.http.Response;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.*;

/** 
 * mysql相关任务
 * @author c-long.chan
 * @date 2022/2/17 12:37
 */

@Component
@Slf4j
public class MysqlBackupScheduleTask {

    @Value("${mysql-backup.host}")
    private String host;
    @Value("${mysql-backup.port}")
    private Integer port;
    @Value("${spring.datasource.username}")
    private String username;
    @Value("${spring.datasource.password}")
    private String password;
    @Value("${mysql-backup.qi-niu-yun.path}")
    private String filePath;
    @Value("${mysql-backup.qi-niu-yun.accessKey}")
    private String accessKey;
    @Value("${mysql-backup.qi-niu-yun.secretKey}")
    private String secretKey;
    @Value("${mysql-backup.qi-niu-yun.bucket}")
    private String bucket;

    /**
     * 提取mysql数据并同步到七牛云
     */
    public void syncMysqlToCloud() throws IOException, InterruptedException {
        //1. 提取mysql数据
        log.info("开始备份数据库");
        String backName = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()) + ".sql";
        dataBaseDump(backName);
        //2. 备份数据至七牛云
        String localFilePath = filePath + File.separator + backName;
        boolean upload = upload(localFilePath, backName);
        if (!upload){
            throw new NotFoundException("数据上传失败,请联系管理员");
        }
    }

    /**
     * 提取mysql数据操作
     * @param backName sql备份名
     */
    private void dataBaseDump(String backName) throws IOException, InterruptedException {
        //非Linux下判断目录是否存在
        log.warn("如果项目部署在docker等容器中,请将目录与宿主机进行映射!!!");
        if(!isOsLinux()){
            Path path = Paths.get(filePath);
            //判断目录是否存在
            //File存在创建文件夹的缺陷,改用Files
            log.info("判断目录是否存在:{}",path);
            if (Files.notExists(path)){
                log.info("目录不存在,创建它~");
                Path directories = Files.createDirectories(path);
                log.info("创建目录成功:{}",directories);
            }else{
                log.info("目录已存在");
            }
        }
        File datafile = new File(filePath + File.separator + backName);
        if (datafile.exists()){
            log.error("文件名已存在,请更换:{}",backName);
            throw new NotFoundException("文件名已存在,请更换");
        }
        //拼接cmd命令
        String command = isOsLinux() ?
                "sh /mnt/docker/mysql_backup/shell/mysqlDump.sh " +backName :
                "cmd /c mysqldump -h" + host + " -P" + port + " -u" + username + " -p" + password + " r_blog > " + datafile;
        log.info("备份命令为:{}",command);
        List<String> commandList = new ArrayList<>();
        Collections.addAll(commandList,command.split(" "));
        log.info("commandList为:{}",commandList);
        log.info("开始执行备份操作");
        //执行备份命令
        ProcessBuilder processBuilder = new ProcessBuilder(commandList);
        processBuilder.redirectErrorStream(true);
        Process process = processBuilder.start();
        log.info("执行完成:{}",processBuilder);
        //读取命令的输出信息
        InputStream inputStream = process.getInputStream();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        process.waitFor();
        if (process.exitValue() != 0) {
            log.error("命令执行失败");
            throw new NotFoundException("命令执行失败");
        }
        log.info("打印进程输出信息====================================================");
        String info;
        while ((info = bufferedReader.readLine()) != null){
            log.info(info);
        }
        log.info("打印进程输出信息结束=====================================================");
        if (process.waitFor() == 0){
            log.info("数据库备份成功,备份路径为:{}",datafile);
        }
    }

    /**
     * 判断项目运行环境是否为Linux
     * @return boolean
     */
    private boolean isOsLinux() {
        Properties properties = System.getProperties();
        String os = properties.getProperty("os.name");
        return os != null && os.toLowerCase().contains("linux");
    }

    /**
     * 将sql备份上传至七牛云
     * @param localFilePath 文件路径(包含文件名)
     * @param fileName 文件名
     * @return boolean
     */
    public boolean upload(String localFilePath,String fileName){
        //1. 构建一个带指定Region对象的配置类(指定七牛云的机房区域)
        Configuration configuration = new Configuration(Region.huanan());
        //其他参数参考类注释
        UploadManager uploadManager = new UploadManager(configuration);
        //准备上传
        try {
            Auth auth = Auth.create(accessKey,secretKey);
            String upToken = auth.uploadToken(bucket);
            //上传文件
            Response response = uploadManager.put(localFilePath, fileName, upToken);
            //解析上传成功的结果
            DefaultPutRet defaultPutRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);
            log.info("上传文件到七牛云成功:{}",defaultPutRet);
            return true;
        } catch (IOException e) {
            log.info("上传文件至七牛云失败:{}",e.toString());
            return false;
        }
    }
}

你可能感兴趣的:(个人博客搭建,spring,boot,mysql,docker)