应用篇:2.延时任务

PHP DIY系列–一起手写一个api框架


延时任务有别于定时任务,定时任务往往是固定周期的,有明确的触发时间。而延时任务一般没有固定的开始时间,它常常是由一个事件触发的,而在这个事件触发之后的一段时间内触发另一个事件。

我们不妨来设定一个实际的场景,电商系统下单成功之后如果15分钟未支付成功,就系统自动取消订单。

我们先来实现代码,然后再来详细说明:

// 我们定义一个abstract,定义两个方法,startAfter和startAt
<?php

abstract class DelayTask
{
    const DELAY_TASK = 'delayTask';
    /**
     * 延时时间,在触发时间后多久执行
     * @param $pushDelayTime
     */
    public function startAfter(int $pushDelayTime)
    {
        RedisManager::getRedis()->zAdd(self::DELAY_TASK, time() + $pushDelayTime, serialize($this));
    }
    /**
     * 定时时间,在未来某时刻执行
     * @param $pushDelayAt
     */
    public function startAt(int $pushDelayAt)
    {
        RedisManager::getRedis()->zAdd(self::DELAY_TASK, $pushDelayAt, serialize($this));
    }
    abstract function run();
}


class TestDelayTask extends DelayTask
{
    public function __construct($id)
    {
        $this->id = $id;
    }
    public function run()
    {
        $file  = 'text.txt';//要写入文件的文件名(可以是任意文件名),如果文件不存在,将会创建一个
        $content = "写入的内容".time()."\n";
        if($f = file_put_contents($file, $content,FILE_APPEND)){// 这个函数支持版本(PHP 5)
            echo "写入成功。<br />";
        }
    }
}

RedisManager是封装的一个单例模式实现的Redis类,我们也贴出代码,然后再对上面的代码做一些说明。

<?php

class RedisManager
{
    private static $instance = null;
    private function __construct()
    {
        self::$instance = new \Redis();
        $config = require 'Redis.config.php';
        self::$instance->connect($config['host'], $config['port'], $config['timeout']);
        if (isset($config['password'])) {
            self::$instance->auth($config['password']);
        }
    }
    /**
     * 获取静态实例
     */
    public static function getRedis()
    {
        if (!self::$instance) {
            new self;
        }
        return self::$instance;
    }
    /**
     * 禁止clone
     */
    private function __clone()
    {
    }
}

有序集合

Redis 127.0.0.1:6379> ZADD saif 1 Redis
(integer) 1
Redis 127.0.0.1:6379> ZADD saif 2 mongodb
(integer) 1
Redis 127.0.0.1:6379> ZADD saif 4 mysql
(integer) 0
Redis 127.0.0.1:6379> ZRANGE saif 0 10 WITHSCORES

1) "Redis"
2) "1"
3) "mongodb"
4) "2"
5) "mysql"
6) "4"

如果你有Redis可视化工具,你会发现有序集合存储的结构是这样:

row value score
1 Redis 1
2 mongodb 2
3 mysql 4

我们再来看一下代码,我们使用时间戳作为分值,使用对象作为值,存储到Redis有序集合。

file_put_contents ( string $filename , mixed $data [, int $flags = 0 [, resource $context ]] ) : int — 将一个字符串写入文件

执行

我们先尝试写入任务:

(new TestDelayTask(4))->startAfter(900);
(new TestDelayTask(4))->startAfter(120);
(new TestDelayTask(4))->startAfter(600);
(new TestDelayTask(3))->startAt(158120384);
(new TestDelayTask(3))->startAt(158420380);
(new TestDelayTask(3))->startAt(158120900);

执行代码:

class DelayTaskTask
{
    const QueneName = 'delayTask';
    private $currentTime;
    private $once = 5;
    public function run()
    {
        $this->currentTime = time();
        error_reporting(error_reporting() & ~E_WARNING);
        while (true) {
            // 每次取出5条
            $list = RedisManager::getRedis()->zRange(self::QueneName, 0, $this->once, true);
            if (!empty($list)) {
                foreach ($list as $val=>$score) {
                    if ($score < $this->currentTime) {
                        unserialize($val)->run();
                        RedisManager::getRedis()->zDelete(self::QueneName, $val);
                    } else {
                        break 2;
                    }
                }
            } else {
                break;
            }
        }
    }
}
(new DelayTaskTask())->run();

我们可以设置一个定时任务,每分钟执行一次上述代码。

我们执行之后,会发现一旦过了我们设定的时间,text.txt就不断有文字写入了。

代码比较杂,我有一个demo代码,大家可以查看。

DelayTask-基于Redis的延时任务

这样,我们就利用Redis有序集合,完成了一个很基础的延时任务。

问题

  • 加入run方法代码执行时间过长,一分钟执行一次有什么问题吗?
  • 每分钟执行一次,间隔有点长,能不能优化呢?

应用篇:2.延时任务
https://blog.puresai.com/2020/02/22/phpframe14/
作者
puresai
许可协议