我決定收尾呼應(yīng)先來說一下廢墟樂隊(duì)的新專輯《正大光明》挺尾。哈哈哈哈
首先是需求,其實(shí)我們想做的就是一個定時發(fā)系統(tǒng)消息的任務(wù)魂挂。這個時間節(jié)點(diǎn)是后臺進(jìn)行設(shè)置的。然而這個定時任務(wù)是需要精確到秒的果正。所以如果用 Linux 的 crontab 就不是很合適辅肾。所以就想別的方法要尔。
然后就看到了網(wǎng)上說的 keyspace notification(密鑰空間通知)這個東西既绩。但我告訴你蚕键,這是其實(shí)是不行的笆怠。不哄你,
這里 有相關(guān)介紹办成,并且該功能是自** 2.8.0 *版本之后可用的功能状答。如果使用該功能需要到 redis 的配置中 配置 notify-keyspace-events 的參數(shù)為 “Ex” 或者 “Kx”拍摇。x 代表了過期事件 *大概就是一種你的 redis 的 key 發(fā)生變化的時候就會觸發(fā)這個功能蜂莉,并且通知你。比如你的 key 過期了蚁滋,刪除了之類的。
他會發(fā)送兩種類型:
K鍵空間通知,所有通知以 __keyspace@ __ 為前綴的
E鍵事件通知蚣旱,所有通知以 __keyevent@ __ 為前綴的
我們本來的思路是后臺每添加一個定時任務(wù)的時候猜欺,就直接在 redis 里存一個具有時效性的key (SETEX涧黄、EXPIRE)窄潭,當(dāng)該 key 失效的時候就會觸發(fā) K 或者 E 的通知月帝,收到通知后我們就做發(fā)消息處理距误。為了避免和其他的時效 key 有混淆趁俊,redis 的密鑰空間通知是提供正則匹配的。所以美滋滋的實(shí)現(xiàn)了代碼:
/**
* 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后臺消息定時發(fā)送
* 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');
// 回調(diào)函數(shù),這里寫處理邏輯
function psCallback($redis, $pattern, $chan, $msg)
{
echo "Pattern: $pattern\n";
echo "Channel: $chan\n";
echo "Payload: $msg\n\n";
}
結(jié)果饲梭,無論是用 Ex 還是用 Kx 都不是很好使,有時候可以收到通知穿扳,有時候很久都收不到通知。
那么 why? 為什么?我覺得大概的原因是:
首先 官方 最后一段關(guān)于這個 “過期活動的時間” 的說法忆首,我理解的就是,當(dāng)某個 key 過期了的時候妒潭,實(shí)際上他并沒有馬上刪除冯凹,所以并不會馬上去通知浑劳,什么情況會下觸發(fā)過期并且通知:
- 當(dāng)密鑰被命令訪問并被發(fā)現(xiàn)已過期時蒜绽。
- Redis會隨機(jī)掃帶有過期時間的 key相赁,當(dāng)掃到并發(fā)現(xiàn)已過期時間跺嗽。
所以官方講:如果沒有命令不斷地定位密鑰璃吧,并且有許多與TTL相關(guān)聯(lián)的密鑰,則在關(guān)鍵生存時間到零之間的時間和生成過期事件的時間之間(就是你設(shè)置了某個 key 失效時間【比如60秒】和該 key 實(shí)際失效并且觸發(fā)通知事件的時間 【可能70秒】)可能存在顯著的延遲。
其次 還有什么原因我就不得而知了...哈哈哈
既然不行
那只能再想別的辦法,后來參考了 1分鐘實(shí)現(xiàn)“延遲消息”功能 做一個環(huán)形隊(duì)列片仿,大概原理就是:
做一個3600(其實(shí)我做了60)的環(huán)形隊(duì)列可帽,當(dāng)你有一個任務(wù)是59秒后執(zhí)行努隙,你就可以將這個任務(wù)存到第59個格里躬存,然后圈數(shù)存為0盾剩。當(dāng)你有一個任務(wù)需要61秒后執(zhí)行,那你可以將這個任務(wù)存到第一個格里,但是圈數(shù)要存為1。
然后有一個定時器,每秒鐘都會沿著這個圈中的小格無線循環(huán)下去徘意。當(dāng)然這個定時器小人跳到某一格勤讽,他會把該格子中所有圈數(shù)為0的數(shù)據(jù)都處理掉券膀,圈數(shù)不為0的雀监,可能需要下一圈或者下下很多圈才需要處理蔚万。
這就是原理淮蜈,如果你沒太理解可以去看下原文,如果你看懂了是不是覺得,嗨泡仗,確實(shí)是這么回事截亦。這TM有點(diǎn)意思哈...然而并沒有什么卵用,代碼我還是不會寫。
這個環(huán)形隊(duì)列要怎么實(shí)現(xiàn)呢?我想了下可以用 redis 的哈希來實(shí)現(xiàn):
//添加任務(wù) hash
$value = [0=>['a任務(wù)','b任務(wù)'],1=>['c任務(wù)'],2=>['d任務(wù)']];
$value = json_encode($value);
$redis->hSet('cycle','second_1',$value);
$value = [0=>['e任務(wù)','f任務(wù)'],1=>['g任務(wù)'],2=>['h任務(wù)']];
$value = json_encode($value);
$redis->hSet('cycle','second_2',$value);
然后是小人跳格子的環(huán)形隊(duì)列:
/**
* 小人跳格子的環(huán)形隊(duì)列啊
* User: 郭貳小姐
* Date: 2017/4/16
* Time: 下午10:38
*/
require_once '/data/momoma.com/redis/RedisInstance.php';
$redis = new \RedisInstance();
// 執(zhí)行任務(wù)的函數(shù)
function task($task){
//if(!empty($task)){
echo "----開始一次執(zhí)行任務(wù)----\n";
foreach ($task as $val){
echo "正在執(zhí)行 $val\n";
}
}
while (true){
for ($i=1;$i<=60;$i++){
// 存儲當(dāng)前指針的位置
$redis->set('current',$i);
$val = $redis->hGet('cycle','second_'.$i);
$val = json_decode($val);
if($val){
// 執(zhí)行當(dāng)前要執(zhí)行的任務(wù)
task($val[0]);
array_shift($val);
// 把剩余的任務(wù)重新寫到redis
$val = json_encode($val);
$redis->hSet('cycle','second_'.$i,$val);
} else {
echo "$i 當(dāng)前時間沒有任務(wù)可執(zhí)行\(zhòng)n";
}
sleep(1);
}
}
額好吧鲫尊,我這里用的 sleep(1)革为,你可以用 swoole 來做定時器。
但是其實(shí)這個方案我們最終并沒有用崎逃,就算用 swoole 來做定時器巴柿,配合 supervisor 來實(shí)現(xiàn)進(jìn)程管理。我覺得還差點(diǎn)什么吶...
最后谐鼎,聽一首,不,是一張專輯。來自廢墟樂隊(duì)的2017新專輯钉汗,從大二開始聽周云山的廢墟樂隊(duì)肪凛。說實(shí)話,這張新專輯也是等了好久的,終于出了绎秒,滿心歡喜阅懦。美滋滋废登。
《正大光明》 - 來自廢墟樂隊(duì)