PHP + Redis 實現定時任務觸發
定時任務,是很常見的業務場景了。比如說游戲服的定時開服,定時發消息,定時發郵件等等。
定時任務的觸發方式有很多,有的人借助 linux 系統的 crontab 服務,但是 crontab需要每分鐘去輪詢,所以 crontab 會有一分鐘誤差。也有的人選擇自己寫一個定時器去處理定時任務。
這里我們介紹一種通過訂閱 redis 鍵過期的消息回調來觸發定時任務的方式。
具體原理
利用 redis 鍵事件的消息訂閱,當 redis 鍵過期時,會觸發一次回調事件,利用該次回調的觸發,帶上相應參數,便可完成一次定時任務的喚起。
一、調整 redis 配置
1、修改redis配置文件開啟鍵值事件的通知:vim redis.conf
原來的:notify-keyspace-events ""
更改后:notify-keyspace-events "Ex"
保存redis.conf并重啟redis服務。
2、執行redis-cli進入redis查看配置是否生效:config get notify-keyspace-events
3、如果結果不是 xE 那么還需要再redis-cli中執行配置修改:config set notify-keyspace-events Ex
二、編寫相應的腳本(以下為測試用的腳本)
1、LibRedis.php
<?php class LibRedis { private $redis; public function __construct($host = '127.0.0.1', $port = '6379',$password = '',$db = '15') { $this->redis = new Redis(); $this->redis->connect($host, $port); $this->redis->auth($password); $this->redis->select($db); } public function setex($key, $time, $val) { return $this->redis->setex($key, $time, $val); }public function psubscribe($patterns = array(), $callback) { $this->redis->psubscribe($patterns, $callback); } public function setOption() { $this->redis->setOption(\Redis::OPT_READ_TIMEOUT, -1); } }
2、添加定時任務的腳本:set_timer_task.php
<?php require_once 'LibRedis.php'; $redis = new \LibRedis('127.0.0.1','6379','','0'); $time = 100; //設置redis鍵100s后過期,即定時100s后觸發定時任務。 $ctl = 'timerTaskManage'; //定時器調用的控制器 $fun = 'callbackFun'; //定時器調用的控制器中的方法 $param = 'id=1'; //透傳的參數(需要透傳的參數盡量少,并且簡單,因為這些數據要拼接成一個redis鍵) $key = "timerTask:{$ctl}:{$fun}:{$param}"; //最終在監聽腳本中調用: $ctl->$fun($param); $re = $redis->setex($key,$time,1); var_dump($re);
3、常駐進程腳本,監聽redis鍵值過期事件,從而觸發定時器:get_timer_task.php
<?php ini_set('default_socket_timeout', -1); //不超時 require_once 'LibRedis.php'; $redis_db = '0'; $redis = new \LibRedis('127.0.0.1','6379','',$redis_db); // 解決Redis客戶端訂閱時候超時情況 $redis->setOption(); //當key過期的時候就看到通知,訂閱的key __keyevent@<db>__:expired 這個格式是固定的,db代表的是數據庫的編號,由于訂閱開啟之后這個庫的所有key過期時間都會被推送過來,所以最好單獨使用一個數據庫來進行隔離 $redis->psubscribe(array('__keyevent@'.$redis_db.'__:expired'), 'keyCallback'); /*回調函數,這里寫處理邏輯,格式固定 *@param $redis 固定格式參數,一般不會用到,但必須帶上。 *@param $pattern 固定格式參數,一般不會用到,但必須帶上。 *@param $channel 固定格式參數,一般不會用到,但必須帶上。 *@param $msg 真正業務中用到的參數,也就是在設置redis時的鍵。set_timer_task.php 腳本中的$key變量 */ function keyCallback($redis, $pattern, $channel, $msg) { try{ //可能有其他非定時器的鍵值對過期了,它們也會回調過來,此處將這部分鍵的事件過濾掉 if($arr[0] != 'timerTask'){ return true; } //多記錄日志,方便后面查驗結果和問題 file_put_contents('/data/logs/timer_task_'.date('Ymd').'.log', 'N1:'.date('Y-m-d H:i:s:').$msg."\n", FILE_APPEND); $controller = $arr[1]; //控制器:timerTaskManage $function = $arr[2]; //方法:callbackFun $param = $arr[3]; //透傳參數id=1 $ctl = new $controller(); $re = $ctl->$function($param); //該類的方法執行具體任務邏輯, file_put_contents('/data/logs/timer_task' . date('Ymd') . '.log', date('Y-m-d H:i:s:') . var_export(array($msg, $re), true) . "\n", FILE_APPEND); if(!$re){ //TODO:任務執行出問題,此處為報警邏輯 } //不論結果執行如何,都有個返回,如果執行出問題了,就在錯誤處理邏輯中處理。 return true; }catch(Exception $e){ //TODO:報警邏輯,錯誤處理邏輯 return true; } }
這里有一點比較重要的問題需要注意,由于腳本 get_timer_task.php 是常駐進程的腳本,那么在該腳本中去實例話一個類,若是要注意做好該類何相應方法的處理,否則一旦出現什么問題都會在該進程中延續下去直到進程掛掉。
比如如果在任務方法中有數據庫連接,那么這個連接超時的時候,下一次任務執行時會報錯。并且報錯會一直存在。
最好是做一種優化,使用 CGI 的方式來調用定時任務的邏輯。這樣,將任務的執行邏輯放到CGI的接口中,每一次任務的執行都跟一次CGI請求一樣,不能論該次任務執行遇到什么錯誤或異常,都不會影響常駐腳本,不會有數據庫連接超時的問題產生,也不會影響接下來其他任務的執行。
比如以下優化方式:
<?php ini_set('default_socket_timeout', -1); //不超時 require_once 'LibRedis.php'; $redis_db = '0'; $redis = new \LibRedis('127.0.0.1','6379','',$redis_db); // 解決Redis客戶端訂閱時候超時情況 $redis->setOption(); //當key過期的時候就看到通知,訂閱的key __keyevent@<db>__:expired 這個格式是固定的,db代表的是數據庫的編號,由于訂閱開啟之后這個庫的所有key過期時間都會被推送過來,所以最好單獨使用一個數據庫來進行隔離 $redis->psubscribe(array('__keyevent@'.$redis_db.'__:expired'), 'keyCallback'); /*回調函數,這里寫處理邏輯,格式固定 *@param $redis 固定格式參數,一般不會用到,但必須帶上。 *@param $pattern 固定格式參數,一般不會用到,但必須帶上。 *@param $channel 固定格式參數,一般不會用到,但必須帶上。 *@param $msg 真正業務中用到的參數,也就是在設置redis時的鍵。set_timer_task.php 腳本中的$key變量 */ function keyCallback($redis, $pattern, $channel, $msg) { try{ //可能有其他非定時器的鍵值對過期了,它們也會回調過來,此處將這部分鍵的事件過濾掉 if($arr[0] != 'timerTask'){ return true; } //多記錄日志,方便后面查驗結果和問題 file_put_contents('/data/logs/timer_task_'.date('Ymd').'.log', 'N1:'.date('Y-m-d H:i:s:').$msg."\n", FILE_APPEND); $data['controller'] = $arr[1]; $data['function'] = $arr[2]; $data['param'] = $arr[3]; $api_url = "http://www.test.com/apiTimerTask.php"; //定時任務執行接口 $re = make_request($api_url,$data); $str = json_encode($re); file_put_contents('/data/logs/timer_task' . date('Ymd') . '.log', date('Y-m-d H:i:s:') . var_export(array($msg, $re), true) . "\n", FILE_APPEND); if(!$re){ //TODO:任務執行出問題,此處為報警邏輯 } //不論結果執行如何,都有個返回,如果執行出問題了,就在錯誤處理邏輯中處理。 return true; }catch(Exception $e){ //TODO:報警邏輯,錯誤處理邏輯 return true; } } function make_request($url,$data) { //TODO:執行curl請求 }
三、總結
在業務的使用過程中,還需要注意以下問題。
用來做定時器的redis最好是單獨的一臺比較低配的redis服務,該redis服務出來用來做定時器盡量不要再做其他用處,或者不要放其他數據,更不要有太多定時過期的鍵。
由于redis過期策略的問題,如果該redis服務中存在太多需要過期的鍵值對,那么定時器的鍵可能并不能準時過期,導致事件不能準時觸發。具體細節可以去詳細了解下 redis鍵的過期策略。
所以,單獨搞一臺小配置的redis,僅用來做定時服務和少量其他服務,能夠提高這種方式的準確性和可靠性。

浙公網安備 33010602011771號