系列文章---应用篇:2.延时任务


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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 我们定义一个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类,我们也贴出代码,然后再对上面的代码做一些说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?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()
{
}
}

有序集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 — 将一个字符串写入文件

执行

1
2
3
4
5
6
7
8
我们先尝试写入任务:

(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);

执行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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方法代码执行时间过长,一分钟执行一次有什么问题吗?
  • 每分钟执行一次,间隔有点长,能不能优化呢?