├── README.md ├── config.php ├── demo.php ├── demo ├── test_client.php └── test_system.php ├── func.php ├── logs └── log_20160327.log ├── parsley ├── parsley.php └── redisQueue.php └── startup.php /README.md: -------------------------------------------------------------------------------- 1 | # 一个用PHP开发的,使用redis的任务队列 2 | 3 | 注: 队列的生产、 4 | 5 | 消费(拉取方式)、 6 | 7 | 消费确认、 8 | 9 | 消费失败回队(回队策略)、 10 | 11 | 多端生产、 12 | 13 | 多段消费(避免资源竞争) 14 | 15 | 自定义队列(多队列) 16 | 17 | 多进程消费(auto队列数量?) 18 | 19 | 20 | end... 21 | 22 | 23 | 1.配置config.php 24 | 25 | 2.将/parsley/parsley.php 引入到你的项目 26 | 27 | 3.将你需要执行的方法写在func.php (保证可在文件中直接运行) 28 | 29 | 4.将startup.php 加入到crontab. ( * * * * * /usr/bin/php /你的项目路径/startup.php ) 30 | 31 | 5. 32 | //apply_async(方法名,方法参数array); 33 | include_once('parsley/parsley.php'); 34 | $c = new parsley(); 35 | $c->apply_async('test',array($i,$i)); 36 | 37 | 这是一个各消费从队列领取式的任务分配(也有接收端分配到进程处理方式) 38 | 目前项目只体现了队列使用的基本概念,后续将会增加。 39 | 多进程(接收任务进程、处理任务进程) 40 | 任务(进程)运行状态监控,提供web界面查看各个任务进程的状态,查询具体任务的分配情况,提供个进程处理的统计 41 | 控制台基本操作,如:kill进程等。 42 | 日志收集,报警 (elk static) 43 | 多服务器。 44 | 45 | 不排除使用其他语言辅助。。。 46 | 47 | 任重道远。。。 48 | 49 | end... 50 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | array( 13 | 'host' => '127.0.0.1', 14 | 'port'=>6379, 15 | 'db'=>0 16 | ), 17 | //redis 默认队列key 18 | 'queue_key'=>'celery_startup_zset', 19 | //进程数 20 | 'process'=>5, 21 | //保持脚本持续(为false时,处理完数据 脚本结束) 22 | 'keep'=>false, 23 | //消费失败重新执行次数,0不重复执行,-1一直执行 24 | 'again'=>-1, 25 | //执行log 26 | 'logs'=>'logs/log_{date}.log', 27 | //错误log 28 | 'error_logs'=>'logs/error_{date}.log', 29 | ); 30 | -------------------------------------------------------------------------------- /demo.php: -------------------------------------------------------------------------------- 1 | connect('127.0.0.1',6379,0); 13 | // $r->set('kk',1); 14 | 15 | // $r = new redisQueue(); 16 | // $r->zadd('k1', 1, 12); 17 | // $s = $r->zrange('myset2',0,10); 18 | 19 | 20 | //创建任务 21 | $c = new parsley(); 22 | for ($i=0;$i<=110000;$i++){ 23 | $c->apply_async('test',array($i,$i)); 24 | } 25 | 26 | // for ($i=0;$i<=100000;$i++){ 27 | // $c->apply_async('test.test',array('test'.$i,$i)); 28 | // } -------------------------------------------------------------------------------- /demo/test_client.php: -------------------------------------------------------------------------------- 1 | apply_async('helloWorld',array($i)); 13 | } 14 | -------------------------------------------------------------------------------- /demo/test_system.php: -------------------------------------------------------------------------------- 1 | digestion_queue_data(); -------------------------------------------------------------------------------- /func.php: -------------------------------------------------------------------------------- 1 | $queue, 33 | 'args'=>$args, 34 | 'key'=>$key, 35 | ); 36 | $data = json_encode($arr); 37 | 38 | # push redis zset 39 | $redis = new redisQueue(); 40 | $redis->zadd($conf['queue_key'] , $key, $data); 41 | return true; 42 | } 43 | 44 | /** 45 | * 消费队列数据 46 | */ 47 | public function digestion_queue_data(){ 48 | global $conf; 49 | $redis = new redisQueue(); 50 | 51 | while (1){ 52 | try { 53 | $element = $redis->zlPop($conf['queue_key']); 54 | }catch (Exception $e){ 55 | $redis = new redisQueue(); 56 | $element = $redis->zlPop($conf['queue_key']); 57 | } 58 | 59 | if(empty($element) && !$conf['keep']){ 60 | break; 61 | } 62 | 63 | $data = json_decode($element,true); 64 | if (empty($data['fun'])) { 65 | continue; 66 | } 67 | 68 | //执行 69 | $ret = $this->call_func($data['fun'], $data['args']); 70 | if ($ret==false) { 71 | //消费失败计数 72 | $incr = $redis->incr($element.'_error_no'); 73 | 74 | if($conf['again']>=0 && $conf['again']>=$incr){ 75 | /** 76 | *消费失败 数据回归队列top100位置(头部/尾部/top2) 77 | *避免放回头部,如果重复消费失败 阻塞任务 78 | */ 79 | $newSource = 0; 80 | $posElement = $redis->zRange($conf['queue_key'],100,100,true); 81 | if(!empty($posElement)){ 82 | $posElement = $redis->zRange($conf['queue_key'],-1,-1,true);//插入最后 83 | } 84 | $newSource = (!empty($posElement[0]))?reset($posElement):$data['key']; 85 | $redis->zadd($conf['queue_key'] , $newSource, $element); 86 | } 87 | } 88 | $redis->del($element.'_error_no'); 89 | 90 | $this->setLog(__URL__.$conf['logs'], date('Y-m-d H:i:s').",执行:{$element},return:{$ret}"); 91 | $element = null; 92 | $data = null; 93 | } 94 | } 95 | 96 | /** 97 | * 执行方法 98 | * @param unknown $queue 方法名 99 | * @param unknown $args 方法参数 100 | * @return mixed|boolean 101 | */ 102 | public function call_func($queue, $args=array()){ 103 | global $conf; 104 | $ex = array(); 105 | try { 106 | $ex = explode('.',$queue); 107 | if (count($ex)>1) { 108 | $c = new $ex[0]; 109 | return call_user_func_array(array($c,$ex[1]), $args); 110 | } 111 | return call_user_func_array($queue, $args); 112 | }catch(Exception $e){ 113 | $this->setLog(__URL__.$conf['error_logs'],date('Y-m-d H:i:s').",执行:{$queue}".json_encode(array($args)).",return:{$e}"); 114 | return false; 115 | } 116 | } 117 | 118 | public function setLog($file,$msg){ 119 | $file = @str_replace('{date}', date('Ymd'), $file); 120 | $myfile = @fopen($file, "a+"); 121 | @fwrite($myfile, $msg."\r\n"); 122 | @fclose($myfile); 123 | } 124 | } 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /parsley/redisQueue.php: -------------------------------------------------------------------------------- 1 | redis) { 20 | // 21 | }else { 22 | $this->redisConn(); 23 | } 24 | } 25 | 26 | private function redisConn(){ 27 | global $conf; 28 | $r = $conf['redis']; 29 | $this->redis = new Redis(); 30 | $this->redis->connect($r['host'],$r['port'],$r['db']); 31 | } 32 | 33 | /** 34 | * 获取队列头部元素,并删除 35 | * @param string $zset 36 | */ 37 | public function zlPop($zset){ 38 | return $this->zsetPopCheck($zset, self::POSITION_FIRST); 39 | } 40 | 41 | /** 42 | * 获取队列头部元素,并删除 43 | * @param string $zset 44 | */ 45 | public function zPop($zset){ 46 | return $this->zsetPop($zset, self::POSITION_FIRST); 47 | } 48 | 49 | /** 50 | * 获取队列尾部元素,并删除 51 | * @param string $zset 52 | */ 53 | public function zRevPop($zset){ 54 | return $this->zsetPop($zset, self::POSITION_LAST); 55 | } 56 | 57 | /** 58 | * redis incr 59 | * @param string $key 60 | */ 61 | public function incr($key){ 62 | try { 63 | return $this->redis->incr($key); 64 | }catch (Exception $e){ 65 | $this->redisConn(); 66 | return $this->redis->incr($key); 67 | } 68 | } 69 | 70 | /** 71 | * redis del 72 | * @param string $key 73 | */ 74 | public function del($key){ 75 | try { 76 | return $this->redis->del($key); 77 | }catch (Exception $e){ 78 | $this->redisConn(); 79 | return $this->redis->del($key); 80 | } 81 | } 82 | 83 | /** 84 | * redis zAdd 85 | * @param string $key 86 | * @param int $source 87 | * @param string $value 88 | */ 89 | public function zadd($key,$source,$value){ 90 | try { 91 | $this->redis->zadd($key,$source,$value); 92 | }catch (Exception $e){ 93 | $this->redisConn(); 94 | $this->redis->zadd($key,$source,$value); 95 | } 96 | } 97 | 98 | /** 99 | * redis zRange 100 | * @param int $position 101 | * @param int $limit 102 | * @param string $value 103 | */ 104 | public function zRange($zset, $position, $limit, $WITHSCORES=''){ 105 | try { 106 | $element = $this->redis->zRange($zset, $position, $limit); 107 | }catch (Exception $e){ 108 | $this->redisConn(); 109 | $element = $this->redis->zRange($zset, $position, $limit); 110 | } 111 | if (!isset($element[0])) { 112 | return null; 113 | } 114 | return $element; 115 | } 116 | 117 | /** 118 | * 模拟zset pop 119 | * 方法1:使用watch监控key,获取元素 (轮询大大增加了时间消耗) 120 | * @param string $zset 121 | * @param int $position 122 | * @return string|json 123 | */ 124 | private function zsetPop($zset, $position){ 125 | try { 126 | $this->redis->ping(); 127 | }catch (Exception $e){ 128 | $this->redisConn(); 129 | } 130 | 131 | $redis = $this->redis; 132 | //乐观锁监控key是否变化 133 | $redis->watch($zset); 134 | $element = $redis->zRange($zset, $position, $position); 135 | if (!isset($element[0])) { 136 | return null; 137 | } 138 | 139 | $redis->multi(); 140 | $redis->zRem($zset, $element[0]); 141 | if($redis->exec()){ 142 | return $element[0]; 143 | } 144 | //key发生变化,重新获取(轮询大大增加了时间消耗?) 145 | return $this->zsetPop($zset, $position); 146 | } 147 | 148 | /** 149 | * 模拟zset pop 避免元素竞争获取 150 | * 方法2:使用写入标记key,获取可用元素 151 | * @param string $zset 152 | * @param int $position 153 | * @return string|json 154 | */ 155 | private function zsetPopCheck($zset, $position){ 156 | //get queue top 1 157 | //php7 不支持只获取 value 的zRange? 158 | try { 159 | $element = $this->redis->zRange($zset, $position, $position); 160 | }catch (Exception $e){ 161 | $this->redisConn(); 162 | $element = $this->redis->zRange($zset, $position, $position); 163 | } 164 | 165 | if (empty($element) || !isset($element[0])) { 166 | return null; 167 | } 168 | //唯一key(可使用更严谨的生成规则,比如:redis的incr) 169 | $myCheckKey = (microtime(true)*10000).rand(1000,9999); 170 | $k = $element[0].'_check'; 171 | $checkKey = $this->redis->get($k); 172 | 173 | if (empty($checkKey) || $myCheckKey == $checkKey) { 174 | $this->redis->setex($k, 10, $myCheckKey); 175 | $this->redis->watch($k);//监控锁 176 | $this->redis->multi(); 177 | $this->redis->zRem($zset, $element[0]); 178 | if($this->redis->exec()){ 179 | return $element[0]; 180 | } 181 | //return null; 182 | } 183 | //重新获取(期待queue top1已消费,获取新的top1,或多个进程抢夺?) 184 | return $this->zsetPopCheck($zset,$position);//$position = 2 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /startup.php: -------------------------------------------------------------------------------- 1 | ($process+1)){ 20 | exit('error on start!'); 21 | } 22 | 23 | $st = new parsley(); 24 | $st->digestion_queue_data(); --------------------------------------------------------------------------------