PHP DIY 系列------應用篇:2. 延時任務

13sai發表於2020-02-25

延時任務有別於定時任務,定時任務往往是固定週期的,有明確的觸發時間。而延時任務一般沒有固定的開始時間,它常常是由一個事件觸發的,而在這個事件觸發之後的一段時間內觸發另一個事件。

我們不妨來設定一個實際的場景,電商系統下單成功之後如果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方法程式碼執行時間過長,一分鐘執行一次有什麼問題嗎?
  • 每分鐘執行一次,間隔有點長,能不能優化呢?
本作品採用《CC 協議》,轉載必須註明作者和本文連結

分享開發知識,歡迎交流。qq957042781

相關文章