基于redis-stream的消息队列实现
环境:
php-version:php7.2
redis-server:5.0.9
php-redis扩展:5.0.0
php-框架:thinkphp3.2
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实现的消息队列,目前已经投入生产应用,每天处理数据百万+的处理,还算稳定。
PHP中常见的问题点,知识点,及盲点。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异