我决定收尾呼应先来说一下废墟乐队的新专辑《正大光明》。哈哈哈哈
首先是需求,其实我们想做的就是一个定时发系统消息的任务。这个时间节点是后台进行设置的。然而这个定时任务是需要精确到秒的。所以如果用 Linux 的 crontab 就不是很合适。所以就想别的方法。
然后就看到了网上说的 keyspace notification(密钥空间通知)这个东西。但我告诉你,这是其实是不行的。不哄你,
这里 有相关介绍,并且该功能是自** 2.8.0 *版本之后可用的功能。如果使用该功能需要到 redis 的配置中 配置 notify-keyspace-events 的参数为 “Ex” 或者 “Kx”。x 代表了过期事件 *大概就是一种你的 redis 的 key 发生变化的时候就会触发这个功能,并且通知你。比如你的 key 过期了,删除了之类的。
他会发送两种类型:
K键空间通知,所有通知以 __keyspace@ __ 为前缀的
E键事件通知,所有通知以 __keyevent@ __ 为前缀的
我们本来的思路是后台每添加一个定时任务的时候,就直接在 redis 里存一个具有时效性的key (SETEX、EXPIRE),当该 key 失效的时候就会触发 K 或者 E 的通知,收到通知后我们就做发消息处理。为了避免和其他的时效 key 有混淆,redis 的密钥空间通知是提供正则匹配的。所以美滋滋的实现了代码:
/**
* Redis处理类
*/
class RedisInstance {
private $redis;
public function __construct($host = '127.0.0.1', $port = 6379) {
$this->redis = new Redis();
$this->redis->connect($host, $port);
}
public function expire($key = null, $time = 0) {
return $this->redis->expire($key, $time);
}
public function psubscribe($patterns = array(), $callback) {
$this->redis->psubscribe($patterns, $callback);
}
public function setOption() {
$this->redis->setOption(\Redis::OPT_READ_TIMEOUT,-1);
}
}
这里没有写具体的消息处理,只是收到通知并且把 key 打印出来!
<?php
/**
* 处理BI后台消息定时发送
* User: 郭贰小姐
* Date: 2017/4/13
* Time: 下午9:47
*/
require_once '/data/momoma.com/redis/RedisInstance.php';
$redis = new \RedisInstance();
// 解决Redis客户端订阅时候超时情况
$redis->setOption();
$redis->psubscribe(array('__keyspace@0*gm_cp*'), 'psCallback');
// 回调函数,这里写处理逻辑
function psCallback($redis, $pattern, $chan, $msg)
{
echo "Pattern: $pattern\n";
echo "Channel: $chan\n";
echo "Payload: $msg\n\n";
}
结果,无论是用 Ex 还是用 Kx 都不是很好使,有时候可以收到通知,有时候很久都收不到通知。
那么 why? 为什么?我觉得大概的原因是:
首先 官方 最后一段关于这个 “过期活动的时间” 的说法,我理解的就是,当某个 key 过期了的时候,实际上他并没有马上删除,所以并不会马上去通知,什么情况会下触发过期并且通知:
- 当密钥被命令访问并被发现已过期时。
- Redis会随机扫带有过期时间的 key,当扫到并发现已过期时间。
所以官方讲:如果没有命令不断地定位密钥,并且有许多与TTL相关联的密钥,则在关键生存时间到零之间的时间和生成过期事件的时间之间(就是你设置了某个 key 失效时间【比如60秒】和该 key 实际失效并且触发通知事件的时间 【可能70秒】)可能存在显着的延迟。
其次 还有什么原因我就不得而知了...哈哈哈
既然不行
那只能再想别的办法,后来参考了 1分钟实现“延迟消息”功能 做一个环形队列,大概原理就是:
做一个3600(其实我做了60)的环形队列,当你有一个任务是59秒后执行,你就可以将这个任务存到第59个格里,然后圈数存为0。当你有一个任务需要61秒后执行,那你可以将这个任务存到第一个格里,但是圈数要存为1。
然后有一个定时器,每秒钟都会沿着这个圈中的小格无线循环下去。当然这个定时器小人跳到某一格,他会把该格子中所有圈数为0的数据都处理掉,圈数不为0的,可能需要下一圈或者下下很多圈才需要处理。
这就是原理,如果你没太理解可以去看下原文,如果你看懂了是不是觉得,嗨,确实是这么回事。这TM有点意思哈...然而并没有什么卵用,代码我还是不会写。
这个环形队列要怎么实现呢?我想了下可以用 redis 的哈希来实现:
//添加任务 hash
$value = [0=>['a任务','b任务'],1=>['c任务'],2=>['d任务']];
$value = json_encode($value);
$redis->hSet('cycle','second_1',$value);
$value = [0=>['e任务','f任务'],1=>['g任务'],2=>['h任务']];
$value = json_encode($value);
$redis->hSet('cycle','second_2',$value);
然后是小人跳格子的环形队列:
/**
* 小人跳格子的环形队列啊
* User: 郭贰小姐
* Date: 2017/4/16
* Time: 下午10:38
*/
require_once '/data/momoma.com/redis/RedisInstance.php';
$redis = new \RedisInstance();
// 执行任务的函数
function task($task){
//if(!empty($task)){
echo "----开始一次执行任务----\n";
foreach ($task as $val){
echo "正在执行 $val\n";
}
}
while (true){
for ($i=1;$i<=60;$i++){
// 存储当前指针的位置
$redis->set('current',$i);
$val = $redis->hGet('cycle','second_'.$i);
$val = json_decode($val);
if($val){
// 执行当前要执行的任务
task($val[0]);
array_shift($val);
// 把剩余的任务重新写到redis
$val = json_encode($val);
$redis->hSet('cycle','second_'.$i,$val);
} else {
echo "$i 当前时间没有任务可执行\n";
}
sleep(1);
}
}
额好吧,我这里用的 sleep(1),你可以用 swoole 来做定时器。
但是其实这个方案我们最终并没有用,就算用 swoole 来做定时器,配合 supervisor 来实现进程管理。我觉得还差点什么呐...
最后,听一首,不,是一张专辑。来自废墟乐队的2017新专辑,从大二开始听周云山的废墟乐队。说实话,这张新专辑也是等了好久的,终于出了,满心欢喜。美滋滋。
《正大光明》 - 来自废墟乐队