基于redis-stream的消息队列实现

环境:

  php-version:php7.2

  redis-server:5.0.9

  php-redis扩展:5.0.0

  php-框架:thinkphp3.2

  redis-stream中文绍

1,实现的效果:

  一个stream,一到N个消费组,1到N个消费者

  

 

2,redis封装:

  

/**
     * stream 操作相关
     *Parameters [5] {
        Parameter #0 [ <required> $str_key ]
        Parameter #1 [ <required> $str_id ]
        Parameter #2 [ <required> array $arr_fields ]
        Parameter #3 [ <optional> $i_maxlen ]
        Parameter #4 [ <optional> $boo_approximate ]
     */
    public function xadd($key,$arr_fields,$str_id="*",$i_maxlen=0,$boo_approximate = null){
        $stime = microtime(true);
        for ($retry=0; $retry<2; $retry++){ //重试两次
            try {
                $res = $this->connect('xadd', $key) && $this->oRedis->xadd($key, $str_id, $arr_fields,$i_maxlen,$boo_approximate) ? true : false; 
                break;
            }catch (RedisException $e){
                $this->close(); //显式关闭,强制重连
                $retry && $this->errorlog('xadd', $e->getCode(), $e->getMessage(), false, 'redis_error.txt');
            }
        }
        $etime = microtime(true);
        //大于1秒
        if($etime - $stime >= 1){
            $runTime = $etime - $stime;
            $dateTime = date('m-d H:i:s');
            $uri = (isset( $_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''). '--';
            $error = "[{$dateTime}] > xadd time:{$runTime}; key:{$key}; uri:{$uri}";
        }
        return $res;
    }
    
    /**
     * 此命令返回流中满足给定ID范围的条目 FIFO
     * @param string $key stream key
     * @param string $start 最小ID
     * @param string $end 最大ID
     * @param int $count 返回指定条数
     */
    public function xrange($key, $start="-", $end="+", $i_count = 0){
        $stime = microtime(true);
        for ($retry=0; $retry<2; $retry++){ //重试两次
            try {
                if($i_count > 0){
                    $res = $this->connect('xrange', $key) && ($result = $this->oRedis->xrange($key, $start, $end, $i_count)) ? $result : array();
                } else {
                    $res = $this->connect('xrange', $key) && ($result = $this->oRedis->xrange($key, $start, $end)) ? $result : array();    
                }                
                break;
            }catch (RedisException $e){
                $this->close(); //显式关闭,强制重连
                $retry && $this->errorlog('xrange', $e->getCode(), $e->getMessage(), false, 'redis_error.txt');
            }
        }
        $etime = microtime(true);
        //大于1秒
        if($etime - $stime >= 1){
            $runTime = $etime - $stime;
            $dateTime = date('m-d H:i:s');
            $uri = (isset( $_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''). '--';
            $error = $this->aServer[0] . ':' .$this->aServer[1] . "[{$dateTime}] > xrange time:{$runTime}; key:{$key}; uri:{$uri}";
        }
        return $res;
    }
    
    /**
     * 此命令返回流中满足给定ID范围的条目,按条目降序返回FILO
     * @param string $key stream key
     * @param string $start 最大ID
     * @param string $end 最小ID
     * @param int $count 返回指定条数
     */
    public function xrevrange($key, $start="+", $end="-", $i_count = 0){
        $stime = microtime(true);
        for ($retry=0; $retry<2; $retry++){ //重试两次
            try {
                if($i_count > 0){
                    $res = $this->connect('xrevrange', $key) && ($result = $this->oRedis->xrevrange($key, $start, $end, $i_count)) ? $result : array();
                } else {
                    $res = $this->connect('xrevrange', $key) && ($result = $this->oRedis->xrevrange($key, $start, $end)) ? $result : array();    
                }                
                break;
            }catch (RedisException $e){
                $this->close(); //显式关闭,强制重连
                $retry && $this->errorlog('xrevrange', $e->getCode(), $e->getMessage(), false, 'redis_error.txt');
            }
        }
        $etime = microtime(true);
        //大于1秒
        if($etime - $stime >= 1){
            $runTime = $etime - $stime;
            $dateTime = date('m-d H:i:s');
            $uri = (isset( $_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''). '--';
            $error = $this->aServer[0] . ':' .$this->aServer[1] . "[{$dateTime}] > xrevrange time:{$runTime}; key:{$key}; uri:{$uri}";
        }
        return $res;
    }
    
    /**
     * 从一个或者多个流中读取数据,仅返回ID大于调用者报告的最后接收ID的条目。此命令有一个阻塞选项,用于等待可用的项目,类似于BRPOP或者BZPOPMIN
     *    Parameters [3] {
     *  Parameter #0 [ <required> array $arr_streams ] ['stream1'=>0,'stream2'=>0]
     *  Parameter #1 [ <optional> $i_count ]
     *  Parameter #2 [ <optional> $i_block ]
     */
    public function xread(array $arr_streams, $i_count = 1, $i_block = null){
        $stime = microtime(true);
        for ($retry=0; $retry<2; $retry++){ //重试两次
            try {
                $res = $this->connect() && ($result = $this->oRedis->xread($arr_streams,$i_count)) ? $result : array();    
                break;
            }catch (RedisException $e){
                $this->close(); //显式关闭,强制重连
                $retry && $this->errorlog('xread', $e->getCode(), $e->getMessage(), false, 'redis_error.txt');
            }
        }
        $etime = microtime(true);
        //大于1秒
        if($etime - $stime >= 1){
            $runTime = $etime - $stime;
            $dateTime = date('m-d H:i:s');
            $uri = (isset( $_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''). '--';
            $error = $this->aServer[0] . ':' .$this->aServer[1] . "[{$dateTime}] > xread time:{$runTime}; key:{$key}; uri:{$uri}";
        }
        return $res;
    }
    
    /**
     * 返回流中的条目数
     * @param string $key
     */
    public function xlen($key){
        return $this->connect('xlen', $key) ? $this->oRedis->xlen( $key) : 0;
    }
    
    /**
     * 返回stream group consumers中的信息流统计
     * Parameters [3] {
        Parameter #0 [ <required> $str_cmd ] GROUPS key|STREAM key|CONSUMERS key groupname
        Parameter #1 [ <optional> $str_key ]
        Parameter #2 [ <optional> $str_group ]
     */
    public function xinfo($str_cmd, $str_key, $str_group = null){        
        if(!empty($str_group)){
            for ($retry=0; $retry<2; $retry++){ //重试两次
                try {
                    $res = $this->connect() && ($result = $this->oRedis->xinfo($str_cmd,$str_key,$str_group)) ? $result : array();
                    break;
                }catch (RedisException $e){
                    $this->close(); //显式关闭,强制重连
                    $retry && $this->errorlog('xinfo', $e->getCode(), $e->getMessage(), false, 'redis_error.txt');
                }
            }
        }else{
            for ($retry=0; $retry<2; $retry++){ //重试两次
                try {
                    $res = $this->connect() && ($result = $this->oRedis->xinfo($str_cmd,$str_key)) ? $result : array();    
                    break;
                }catch (RedisException $e){
                    $this->close(); //显式关闭,强制重连
                    $retry && $this->errorlog('xinfo', $e->getCode(), $e->getMessage(), false, 'redis_error.txt');
                }
            }
        }
        
        return $res;
    }
    
    /**
     * 该命令用于管理流数据结构关联的消费者组。使用XGROUP你可以:

        创建与流关联的新消费者组。
        销毁一个消费者组。
        从消费者组中移除指定的消费者。
        将消费者组的最后交付ID设置为其他内容
        Parameters [5] {
        Parameter #0 [ <required> $str_operation ] CREATE|DESTROY|SETID|DELCONSUMER
        Parameter #1 [ <optional> $str_key ]
        Parameter #2 [ <optional> $str_arg1 ]
        Parameter #3 [ <optional> $str_arg2 ]
        Parameter #4 [ <optional> $str_arg3 ]
     */
    public function xgroup($str_operation, $str_key, $str_arg1 = null, $str_arg2 = null, $str_arg3 = null){
        switch($str_operation){
            case "CREATE":    //创建消费组 命令,streamkey,组名称,从哪开始消费
                $flag = $this->connect() ? $this->oRedis->xgroup( $str_operation,$str_key,$str_arg1,$str_arg2) : false;
                break;
            case "DESTROY":    //删除消费组 命令,streamkey,组名称
                $flag = $this->connect() ? $this->oRedis->xgroup( $str_operation,$str_key,$str_arg1) : false;
                break;
            case "SETID":    //设置传递下一个消息的ID 命令,streamkey,条目ID
                $flag =  $this->connect() ? $this->oRedis->xgroup( $str_operation,$str_key,$str_arg1) : false;
                break;
            case "DELCONSUMER":    //删除消费者 命令,streamkey,groupname,consumername
                $flag = $this->connect() ? $this->oRedis->xgroup( $str_operation,$str_key,$str_arg1,$str_arg2) : false;
                break;
            default:
                $flag = false;
                break;
        }
        return $flag;
    }
    
    /**
     * 删除流数据中的条目
     * @param string $key
     * @param array $arr_ids
     */
    public function xdel($key, array $arr_ids){
        return $this->connect('xdel', $key) ? $this->oRedis->xdel( $key, $arr_ids) : 0;
    }
    
    /**
     * XACK命令用于从流的消费者组的待处理条目列表(简称PEL)中删除一条或多条消息
     * Parameters [3] {
        Parameter #0 [ <required> $str_key ]
        Parameter #1 [ <required> $str_group ]
        Parameter #2 [ <required> array $arr_ids ]
     * 成功返回确认的消息数
     */
    public function xack($key, $str_group, array $arr_ids){
        return $this->connect('xack', $key) ? $this->oRedis->xack( $key, $str_group, $arr_ids) : 0;
    }
    
    /**
     * XREADGROUP命令是XREAD命令的特殊版本,支持消费者组
     * Parameters [5] {
        Parameter #0 [ <required> $str_group ]            groupname
        Parameter #1 [ <required> $str_consumer ]        consumername
        Parameter #2 [ <required> array $arr_streams ]    arr_streams ['stream'=>'>']
        Parameter #3 [ <optional> $i_count ]            读多少条
        Parameter #4 [ <optional> $i_block ]            是否阻塞读取
     */
    public function xreadgroup($str_group, $str_consumer, array $arr_streams, $i_count = 1, $i_block = null){
        $stime = microtime(true);
        for ($retry=0; $retry<2; $retry++){ //重试两次
            try {
                $res = $this->connect() && ($result = $this->oRedis->xreadgroup($str_group, $str_consumer, $arr_streams, $i_count)) ? $result : array();    
                break;
            }catch (RedisException $e){
                $this->close(); //显式关闭,强制重连
                $retry && $this->errorlog('xreadgroup', $e->getCode(), $e->getMessage(), false, 'redis_error.txt');
            }
        }
        $etime = microtime(true);
        //大于1秒
        if($etime - $stime >= 1){
            $runTime = $etime - $stime;
            $dateTime = date('m-d H:i:s');
            $uri = (isset( $_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''). '--';
            $error = $this->aServer[0] . ':' .$this->aServer[1] . "[{$dateTime}] > xreadgroup time:{$runTime}; key:{$key}; uri:{$uri}";
        }
        return $res;
    }
    
    /**
     * 通过消费者组从流中获取数据,而不是确认这些数据,具有创建待处理条目的效果
     * Parameters [6] {
        Parameter #0 [ <required> $str_key ]
        Parameter #1 [ <required> $str_group ]
        Parameter #2 [ <optional> $str_start ]
        Parameter #3 [ <optional> $str_end ]
        Parameter #4 [ <optional> $i_count ]
        Parameter #5 [ <optional> $str_consumer ]
        $redis->xPending('mystream', 'mygroup', '-', '+', 1, 'consumer-1')
     */
    public function xpending($key, $str_group, $str_start = '-', $str_end = '+', $i_count = 1, $str_consumer=null){
        if($str_consumer){
            return $this->connect() ? $this->oRedis->xpending( $key, $str_group,$str_start,$str_end,$i_count,$str_consumer) : [];    
        }else{
            return $this->connect() ? $this->oRedis->xpending( $key, $str_group, $str_start, $str_end, $i_count) : [];
        }        
    }
    
    /**
     * 把某些条目给认领给某个消费者
     * Parameters [6] {
        Parameter #0 [ <required> $str_key ]         streamkey
        Parameter #1 [ <required> $str_group ]        groupname
        Parameter #2 [ <required> $str_consumer ]    consumername
        Parameter #3 [ <required> $i_min_idle ]        空闲时间
        Parameter #4 [ <required> array $arr_ids ]    条目IDS
        Parameter #5 [ <optional> array $arr_opts ] [IDLE|TIME|RETRYCOUNT|FORCE|JUSTID]
     */
    public function xclaim($key, $str_group, $str_consumer, $i_min_idle, array $arr_ids, array $arr_opts = null){
        return $this->connect() ? $this->oRedis->xtrim( $key, $str_group,$str_consumer,$i_min_idle,$arr_ids) : 0;
    }
    
    /**
     * XTRIM将流裁剪为指定数量的项目
     *Parameters [3] {
        Parameter #0 [ <required> $str_key ]
        Parameter #1 [ <required> $i_maxlen ]
        Parameter #2 [ <optional> $boo_approximate ]
     */
    public function xtrim($key, $i_maxlen, $boo_approximate = null){
        return $this->connect('xtrim', $key) ? $this->oRedis->xtrim( $key, $i_maxlen) : 0;
    }

 

3,进程队列封装 关键代码

  

while ( true ) {
            //更新锁文件时间
            Lock::factory()->touchLock($filename,$locktyperun,$runid);
            
            $aValues = array();
            $objQueue->ping();
        
            //检查PEL中是否有待处理的消息,如果有,则优先取待处理消息,否则则取最新消息
            $pendinginfo = $objQueue->xpending($streamname,$groupname,'-','+',1,$consumername);
            if(!empty($pendinginfo)){
                $aValues = $objQueue->PopEvent($groupname,$consumername,[$streamname=>'0']);    
            } else {
                $aValues = $objQueue->PopEvent($groupname,$consumername,[$streamname=>'>']);    
            }
            if( $aValues ){
                foreach ($aValues as $callback => $aJobs) {
                    if( empty($aJobs) || !is_array($aJobs) ){
                        continue;    
                    }
                    //处理条目    
                    foreach ($aJobs as $id=>$value) {
                        $aCountLog['count']++;
                        if( empty($callback)){
                            $aCountLog['callbacknull']++;
                            continue;
                        }                        
                        //后台创建stream时会初始化一个0值 删除掉
                        if(empty($value)){
                            $objredis->xdel($callback,[$id]);
                            continue;
                        }
                        
                        if( !method_exists(A('Crontab/JobworkStream'),$callback) ){
                            $aCountLog[$callback]['dielist']++;
                            $aCountLog[$callback]['jobs_error']++;
                            continue;
                        }
                        
                        $starttime = microtime(true);
                        $ret = call_user_func_array(array(A('Crontab/JobworkStream'),$callback),array(json_encode($value)));
                        $endtime = microtime(true);
                        
                        $runtime = round($endtime-$starttime,4);
                        $aCountLog[$callback]['count']++;
                        $aCountLog[$callback]['runtime'] += $endtime-$starttime;
                        $aCountLog[$callback]['maxtime'] = $aCountLog[$callback]['maxtime']<$runtime ? $runtime : $aCountLog[$callback]['maxtime'];
                        if( !$ret){
                            //处理失败
                            $aCountLog[$callback]['fail']++;
                            //$objQueue->push($callback,$data,$dietype);
                            $aCountLog[$callback]['dielist']++;
                        } else {
                            //确认消息 从消费者PEL列表中拿掉待处理条目
                            $objQueue->xack($callback,$groupname,[$id]);
                        
                            //处理成功,则删掉stream里的条目
                            $objQueue->xdel($callback,[$id]);
                            
                            $aCountLog[$callback]['succ']++;
                        }
                    }
                }
            } else {
                sleep(1);
            }
            
            //检测脚本是否需要重启
            if( Lock::factory()->getLock($filename,$locktypedie,$runid)){
                if( Lock::factory()->deleteLock($filename,$locktypedie,$runid) && Lock::factory()->deleteLock($filename,$locktyperun,$runid) ){
                    die();
                }
            }
    
            
            usleep(5000);
        }

  

4,其它进程,进程锁相关,监控进程可以看我另外一篇基于redis-list实现的消息队列,目前已经投入生产应用,每天处理数据百万+的处理,还算稳定。

 

posted @ 2022-02-21 17:36  sblack  阅读(597)  评论(0编辑  收藏  举报