Swoole+ThinkPHP6自定义命令实现Mysql自动备份定时任务

尽管crontab+shell已经很强大很好用了,但在部署时,还是需要专门去写crontab配置,有那么一丢丢不方便,这里将备份配置放到项目里来,可以实现统一管理。

脚本直接采用swoole定时器实现,并通过swoole的蜕变守护进程达到常驻内存运行目的。之所以不在swoole的server里,通过workstart调用定时器,是因为server还需要监听端口,不够简单直接,而且在服务退出时,work进程是没法响应信号退出的,只能被master强制回收。

下面开始具体实现

一、创建thinkphp自定义命令的类文件

php think make:command TaskMysqlBackup task:mysqlbackup

CLI模式下,执行以上命令后会自动创建app/command/TaskMysqlBackup.php文件,也可以不用命令手动创建。

二、编辑TaskMysqlBackup.php类文件,实现自定义命令:

<?php
declare (strict_types = 1);

namespace app\command;

use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\Output;

class TaskMysqlBackup extends Command
{
    protected $pid_file;
    protected $log_file;
    protected $after_timer;
    protected $tick_timer;
    protected $period;
    protected $worktime;
    protected $bin_dir;
    protected $bak_dir;
    protected $keep;

    protected function configure()
    {
        // 指令配置
        $this->setName('task:mysqlbackup')
            ->addArgument('action', Argument::OPTIONAL, "start|stop|restart|status|backup", 'start')
            ->setDescription('数据库定时备份');

        $this->log_file = root_path() . 'extend' . DIRECTORY_SEPARATOR . 'backup' . DIRECTORY_SEPARATOR . 'mysqlbackup.log';
        // pid文件不要放到runtime目录,否则容易被清除掉,造成进程无法正常关闭
        $this->pid_file = root_path() . 'extend' . DIRECTORY_SEPARATOR . 'marks' . DIRECTORY_SEPARATOR . 'mysqlbackup.pid';

        // 运行周期(秒)
        $this->period = 86400;
        // 运行时间点,进程生命周期内只有一个值,不一定就是上一次/下一次的运行时间点,运行周期在时间轴上分割出一系列的点,这些点平移后,其中的某一个点与运行时间点重合
        $this->worktime = strtotime(date('Y-m-d 03:00:00'));
        // mysqldump命令所在目录
        $this->bin_dir = '/usr/bin/';
        // 备份文件存放目录
        $this->bak_dir = root_path() . 'extend' . DIRECTORY_SEPARATOR . 'backup' . DIRECTORY_SEPARATOR;
        // 备份文件保留时间(秒)
        $this->keep = 7 * 86400;
    }

    protected function execute(Input $input, Output $output)
    {
        if (!extension_loaded('swoole')) {
            $output->error('Can\'t detect Swoole extension installed.');
            return;
        }
        $action = $input->getArgument('action');
        if (in_array($action, ['start', 'stop', 'restart', 'status', 'backup'])) {
            $this->$action();
        } else {
            $output->error("Invalid argument action:{$action}, Expected start|stop|restart|status|backup.");
            return;
        }
    }

    protected function checkFile($file)
    {
        $dir = dirname($file);
        if (!is_dir($dir)) {
            @mkdir($dir, 0700, true);
        }
        @touch($file);
        if (!is_writable($file)) {
            return false;
        }
        return true;
    }

    protected function backup()
    {
        $database = config('database.connections.mysql.database');
        $username = config('database.connections.mysql.username');
        $password = config('database.connections.mysql.password');

        $host = config('database.connections.mysql.hostname');
        $port = config('database.connections.mysql.hostport');
        $charset = config('database.connections.mysql.charset');

        $time = date('Ymd_His');

        $filename = "{$this->bak_dir}{$database}_{$time}.gz";
        if (!$this->checkFile($filename)) {
            echo "[" . date('Y-m-d H:i:s') . "]\r\nCan\'t create file under: {$this->bak_dir}.\r\n\r\n";
            return;
        }
        @unlink($filename);

        $this->clear($database);

        $command = "{$this->bin_dir}mysqldump -h{$host} -P{$port} --default-character-set={$charset} -u{$username} -p{$password} {$database}";
        $command .= "|gzip>{$filename}";

        $result = shell_exec($command);
        if (!empty($result)) {
            echo "[" . date('Y-m-d H:i:s') . "]\r\nBackup failure.\r\n\r\n";
            return;
        }
    }

    protected function clear($filePrefix)
    {
        foreach (glob($this->bak_dir . '*.gz') as $file) {
            if (preg_match('/^' . $filePrefix . '_(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})\.gz$/', basename($file), $matches)) {
                $time = mktime((int) $matches[4], (int) $matches[5], (int) $matches[6], (int) $matches[2], (int) $matches[3], (int) $matches[1]);
                if ($time !== false && $time <= time() - $this->keep) {
                    @unlink($file);
                }
            }
        }
    }

    protected function start()
    {
        if ($this->isRunning()) {
            $this->output->error('Process is already running.');
            return;
        }
        $this->output->writeln('Starting process...');

        if (!$this->checkFile($this->pid_file)) {
            $this->output->error("Can\'t write file: {$this->pid_file}.");
            return;
        }
        if (!$this->checkFile($this->log_file)) {
            $this->output->error("Can\'t write file: {$this->log_file}.");
            return;
        }

        $timespan = ($this->worktime - time()) % $this->period;
        if ($timespan <= 0) {
            $timespan += $this->period;
        }

        $this->after_timer = \swoole_timer::after($timespan * 1000, function () {
            $this->tick_timer = \swoole_timer::tick($this->period * 1000, function () {
                ob_start();
                $this->backup();
                file_put_contents($this->log_file, ob_get_clean(), FILE_APPEND);
            });
            ob_start();
            $this->backup();
            file_put_contents($this->log_file, ob_get_clean(), FILE_APPEND);
        });

        \swoole_process::daemon(true, false);

        file_put_contents($this->pid_file, getmypid());

        \swoole_process::signal(SIGTERM, function () {
            if ($this->after_timer) {
                \swoole_timer::clear($this->after_timer);
            }
            if ($this->tick_timer) {
                \swoole_timer::clear($this->tick_timer);
            }
            @unlink($this->pid_file);
        });

        $nextTime = date('Y-m-d H:i:s', time() + $timespan);
        $this->output->writeln("Starting success, Task will run in the {$nextTime} for the first time.");

        \swoole_event::wait();
    }

    protected function stop()
    {
        if (!$this->isRunning()) {
            $this->output->error('No process running.');
            return;
        }
        $this->output->writeln('Stopping process...');
        $pid = (int) file_get_contents($this->pid_file);
        \swoole_process::kill($pid, SIGTERM);
        $end = time() + 15;
        while (time() < $end && \swoole_process::kill($pid, 0)) {
            usleep(100000);
        }
        if ($this->isRunning()) {
            $this->output->error('Unable to stop the process.');
            return;
        }
        $this->output->writeln('Stopping success.');
    }

    protected function restart()
    {
        if ($this->isRunning()) {
            $this->stop();
        }
        $this->start();
    }

    protected function status()
    {
        $this->output->writeln($this->isRunning() ? 'Process is running.' : 'Process stopped.');
    }

    protected function isRunning()
    {
        if (!is_readable($this->pid_file)) {
            return false;
        }
        $pid = (int) file_get_contents($this->pid_file);
        return $pid > 0 && \swoole_process::kill($pid, 0);
    }
}

三、配置命令使生效

config/console.php修改配置如下:

<?php
return [
    'commands' => [
        'task:mysqlbackup' => 'app\command\TaskMysqlBackup',
    ],
];

四、手动运行命令测试备份逻辑

php think task:mysqlbackup backup

五、开启自动备份

php think task:mysqlbackup start

此时,进程已常驻后台,会定时进行备份

虽然前面有提到蜕变守护进程,但并不是多了一个守护进程,只是swoole变为后台进程的说法,要想真正守护此进程,得另外实现一个进程来监控此进程,当然更简单的方法是采用第三方守护工具,比如Python的supervisor。

命令帮助:

php think task:mysqlbackup -h

Usage:
  task:mysqlbackup [<action>]

Arguments:
  action                start|stop|restart|status|backup [default: "start"]

Options:
  -h, --help            Display this help message
  -V, --version         Display this console version
  -q, --quiet           Do not output any message
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值