swoole上的定时任务

525 阅读3分钟

定时任务可以采用linux的crontab、workerman中的timer定时器、swoole中的timer定时器;结合laravel8+swoole扩展来做一个定时任务

安装

首先下载laravel8

composer global require laravel/installer

安装swoole扩展

composer require swooletw/laravel-swoole

安装完成后记得启动服务

启动服务器的命令为:

laravel-swoole这个扩展的热更新是有问题的,并且没有消息队列。需要手动启动消息队列命令。需要解决这些问题可以购买【陀螺匠】系统,此系统中重写了热更新,增加了消息队列的执行。启动swoole命令就可以直接运行

php artison swoole:http restart

注册事件

首先需要在app\Providers\EventServiceProvider.php文件中增加如下事件

protected $listen = [
 		//swoole事件
		'swoole.start'           => [],//swoole 启动
		'swoole.task'            => [],//swoole 任务
		'swoole.shutDown'        => [],//swoole 停止
		'swoole.workerStart'     => [],//socket 启动
		'swoole.workerStop'      => [],//socket 停止
		'swoole.workerError'     => [],//socket 发生错误
];

需要在当前事件中增加crontab事件

protected $listen = [
		'crontab'=>[
      
    ]
];

而在swoole的启动事件中增加执行crontab的事件,新建一个app\Listeners\swoole\SwooleWorkStart.php的文件,文件内容如下

<?php

namespace App\Listeners\swoole;

class SwooleWorkStart 
{

   public function handle($event): void
 	 {
      	//执行计划任务
        event('crontab');
   }
}

把事件类注入到事件中,然后重启swoole,到此crontab的定时任务事件已经注册完毕

protected $listen = [
 		//swoole事件
		'swoole.workerStart'     => [\App\Listeners\swoole\SwooleWorkStart::class],//socket 启动
];

这样执行可能有很多疑问

问题1:swoole启动了多个进程,那么这里的事件回执行多次吗?

答:是进程,事件也会执行多次;

问题2:执行多次事件,那crontab里面的事件放的定时任务也会执行多次?

答:可以在执行定时任务的时候根据进程号来区分当前需要执行在哪个进程上,保持只有在一个进程上执行。

执行计划任务类

因为所有的定时任务都需要执行Timer::tick或者其他的定时任务,所以稍作封装下,更利于使用

<?php

namespace crmeb\utils;


use Swoole\Http\Server;
use Illuminate\Support\Facades\Log;

/**
 * Class Timer
 * @package crmeb\utils
 */
class Timer
{
    /**
     * @var Server
     */
    protected $server;

    /**
     * @var int
     */
    protected $workerId = 0;

    /**
     * @var array
     */
    protected $timer = [];

    /**
     * @var bool
     */
    protected bool $debug = false;

    /**
     * Timer constructor.
     */
    public function __construct()
    {
        //这里获取到底层已经被实例化过得swoole服务,不然是获取不到当前的进程号
        $this->server = app('swoole.server');
        $this->debug  = env('APP_DEBUG', false);
    }

    /**
     * 设置在几号进程上执行
     * @param int $workerId
     * @return $this
     */
    public function setWorkerId(int $workerId)
    {
        //这里已经要注意,设置的进程数字不能大于中进程数,不然是无法执行的
        $workerNum = config('swoole_http.server.options.worker_num');
        if ($workerId > $workerNum) {
            $workerId = $workerNum;
        }
        $this->workerId = $workerId;
        return $this;
    }

    /**
     * 执行
     * @param callable $callable
     * @param array $params
     */
    public function runInSandbox(callable $callable, array $params = [])
    {
        try {
            $callable(...$params);
        } catch (\Throwable $e) {
            $this->debug && Log::error($e->getMessage(), ['file' => $e->getFile(), 'line' => $e->getLine()]);
        }
    }

    /**
     * 添加定时器
     * @param int $ms
     * @param callable $callable
     * @return int
     */
    public function tick(int $ms, callable $callable)
    {
        if ($this->workerId === $this->server->getWorkerId()) {
            $objectId               = spl_object_id($callable);
            //把当前的定时器放入timer属性中利于管理,可以在程序中启动一个定时器,
            //然后再某个业务之后关闭它,当然这种情况可能性很少
            $this->timer[$objectId] = \Swoole\Timer::tick($ms, function () use ($callable) {
                $this->runInSandbox($callable);
            });
        }
    }

    /**
     * 执行一次的定时器
     * @param int $ms
     * @param callable $callable
     * @param array $params
     */
    public function after(int $ms, callable $callable, array $params = [])
    {
        if ($this->workerId === $this->server->getWorkerId()) {
            \Swoole\Timer::after($ms, function ($callable, $params) {
                $this->runInSandbox($callable, $params);
            }, $callable, $params);
        }
    }

}

增加定时任务事件

上面的crontab事件内属于定时任务事件,可以在crontab事件中增加任意的定时任务用来执行,这里创建个app\Listeners\crontab\TestTimer.php文件先把文件放入事件中

protected $listen = [
		'crontab'=>[
      \App\Listeners\crontab\TestTimer::class
    ]
];

然后我们创建TestTimer文件,并继承crmeb\utils\Timer类,大致如下handle是执行事件逻辑的方法

<?php

namespace App\Listeners\crontab;


use crmeb\utils\Timer;

class TestTimer extends Timer
{
  	public function handle()
  	{
      
  	}
}
  

这时我们就可以在handle中写入自己的逻辑

例如1分钟执行一次的定时任务,下面的代码会在0号进程上运行,并且每1分钟执行一次,定时器的执行事件单位为毫秒

public function handle()
{
    $this->tick(1000 * 60, function () {
      echo '我一分钟执行一次';
    });
}

例如:在2号进程上运行,1秒执行一次

public function handle()
{
    $this->setWorkerId(2)->tick(1000 * 60, function () {
      echo '我一分钟执行一次';
    });
}

例如:查询一个列表,列表中的数据非常多,这个时候怎么执行才好呢

可以分页进行执行,再利用一次性定时器进行执行任务

public function handle()
{
    $this->setWorkerId(2)->tick(1000 * 60, function () {
        $sum = 5000;
        
      	$limit = 100;

        for ($i = 1; $i <= $sumPage; $i++) {
          //1秒后执行
          $this->after(1000, function($page, $limit) {
            
          }, [$i, $limit]);
        
        }
        
    });
}

执行定时器内的时间和执行到程序的时间有误差

程序执行会从上到下时间,确实会遇到到了执行时间可是业务还没执行到发送消息的逻辑。等执行到了,会发现当前的时间已经过了。这个时候只要在创建定时器的时候把时间变量提前定义出来,就可以避免这个问题