zoukankan      html  css  js  c++  java
  • 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."
    ", 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) . "
    ", 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."
    ", 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) . "
    ", 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,仅用来做定时服务和少量其他服务,能够提高这种方式的准确性和可靠性。

  • 相关阅读:
    # ConfigureAwait常见问题解答
    # Oracle 常用语句
    # C# 中的Task创建指南
    ASP.NET Core Web API 跨域(CORS) Cookie问题
    Order by 优化
    VMware 安装 CentOS 7
    ThreadLocal 内存泄漏问题深入分析
    Zookeeper 如何保证分布式系统数据一致性
    Redis 5.0 安装
    Redisson 实现分布式锁的原理分析
  • 原文地址:https://www.cnblogs.com/LO-gin/p/14446289.html
Copyright © 2011-2022 走看看