├── src ├── config │ └── plugin │ │ └── webman │ │ └── redis-queue │ │ ├── app.php │ │ ├── command.php │ │ ├── process.php │ │ ├── redis.php │ │ └── log.php ├── Consumer.php ├── Install.php ├── Client.php ├── Command │ └── MakeConsumerCommand.php ├── RedisConnection.php ├── Process │ └── Consumer.php └── Redis.php ├── README.md └── composer.json /src/config/plugin/webman/redis-queue/app.php: -------------------------------------------------------------------------------- 1 | true, 4 | ]; -------------------------------------------------------------------------------- /src/config/plugin/webman/redis-queue/command.php: -------------------------------------------------------------------------------- 1 | [ 4 | 'handler' => Webman\RedisQueue\Process\Consumer::class, 5 | 'count' => 8, // 可以设置多进程同时消费 6 | 'constructor' => [ 7 | // 消费者类目录 8 | 'consumer_dir' => app_path() . '/queue/redis' 9 | ] 10 | ] 11 | ]; -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webman/redis-queue", 3 | "description": "Redis message queue plugin for webman.", 4 | "require": { 5 | "php": ">=8.1", 6 | "workerman/redis-queue": "^1.2", 7 | "workerman/webman-framework": "^2.1 || dev-master", 8 | "ext-redis": "*" 9 | }, 10 | "autoload": { 11 | "psr-4": {"Webman\\RedisQueue\\": "./src"} 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/config/plugin/webman/redis-queue/redis.php: -------------------------------------------------------------------------------- 1 | [ 4 | 'host' => 'redis://127.0.0.1:6379', 5 | 'options' => [ 6 | 'auth' => null, 7 | 'db' => 0, 8 | 'prefix' => '', 9 | 'max_attempts' => 5, 10 | 'retry_seconds' => 5, 11 | ], 12 | // Connection pool, supports only Swoole or Swow drivers. 13 | 'pool' => [ 14 | 'max_connections' => 5, 15 | 'min_connections' => 1, 16 | 'wait_timeout' => 3, 17 | 'idle_timeout' => 60, 18 | 'heartbeat_interval' => 50, 19 | ] 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /src/Consumer.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | namespace Webman\RedisQueue; 16 | 17 | 18 | /** 19 | * Interface Consumer 20 | * @package Webman\RedisQueue 21 | */ 22 | interface Consumer 23 | { 24 | public function consume($data); 25 | } -------------------------------------------------------------------------------- /src/config/plugin/webman/redis-queue/log.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | return [ 16 | 'default' => [ 17 | 'handlers' => [ 18 | [ 19 | 'class' => Monolog\Handler\RotatingFileHandler::class, 20 | 'constructor' => [ 21 | runtime_path() . '/logs/redis-queue/queue.log', 22 | 7, //$maxFiles 23 | Monolog\Logger::DEBUG, 24 | ], 25 | 'formatter' => [ 26 | 'class' => Monolog\Formatter\LineFormatter::class, 27 | 'constructor' => [null, 'Y-m-d H:i:s', true], 28 | ], 29 | ] 30 | ], 31 | ] 32 | ]; 33 | -------------------------------------------------------------------------------- /src/Install.php: -------------------------------------------------------------------------------- 1 | 'config/plugin/webman/redis-queue', 13 | ); 14 | 15 | /** 16 | * Install 17 | * @return void 18 | */ 19 | public static function install() 20 | { 21 | static::installByRelation(); 22 | if (!is_dir(app_path() . '/queue/redis')){ 23 | mkdir(app_path() . '/queue/redis', 0777, true); 24 | } 25 | } 26 | 27 | /** 28 | * Uninstall 29 | * @return void 30 | */ 31 | public static function uninstall() 32 | { 33 | self::uninstallByRelation(); 34 | } 35 | 36 | /** 37 | * installByRelation 38 | * @return void 39 | */ 40 | public static function installByRelation() 41 | { 42 | foreach (static::$pathRelation as $source => $dest) { 43 | if ($pos = strrpos($dest, '/')) { 44 | $parent_dir = base_path().'/'.substr($dest, 0, $pos); 45 | if (!is_dir($parent_dir)) { 46 | mkdir($parent_dir, 0777, true); 47 | } 48 | } 49 | //symlink(__DIR__ . "/$source", base_path()."/$dest"); 50 | copy_dir(__DIR__ . "/$source", base_path()."/$dest"); 51 | } 52 | } 53 | 54 | /** 55 | * uninstallByRelation 56 | * @return void 57 | */ 58 | public static function uninstallByRelation() 59 | { 60 | foreach (static::$pathRelation as $source => $dest) { 61 | $path = base_path()."/$dest"; 62 | if (!is_dir($path) && !is_file($path)) { 63 | continue; 64 | } 65 | /*if (is_link($path) { 66 | unlink($path); 67 | }*/ 68 | remove_dir($path); 69 | } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | namespace Webman\RedisQueue; 15 | 16 | use support\Log; 17 | use Workerman\RedisQueue\Client as RedisClient; 18 | 19 | /** 20 | * Class RedisQueue 21 | * @package support 22 | * 23 | * Strings methods 24 | * @method static void send($queue, $data, $delay=0) 25 | */ 26 | class Client 27 | { 28 | /** 29 | * @var Client[] 30 | */ 31 | protected static $_connections = null; 32 | 33 | 34 | /** 35 | * @param string $name 36 | * @return RedisClient 37 | */ 38 | public static function connection($name = 'default') { 39 | if (!isset(static::$_connections[$name])) { 40 | $config = config('redis_queue', config('plugin.webman.redis-queue.redis', [])); 41 | if (!isset($config[$name])) { 42 | throw new \RuntimeException("RedisQueue connection $name not found"); 43 | } 44 | $host = $config[$name]['host']; 45 | $options = $config[$name]['options']; 46 | $client = new RedisClient($host, $options); 47 | if (method_exists($client, 'logger')) { 48 | $client->logger(Log::channel('plugin.webman.redis-queue.default')); 49 | } 50 | static::$_connections[$name] = $client; 51 | } 52 | return static::$_connections[$name]; 53 | } 54 | 55 | /** 56 | * @param $name 57 | * @param $arguments 58 | * @return mixed 59 | */ 60 | public static function __callStatic($name, $arguments) 61 | { 62 | return static::connection('default')->{$name}(... $arguments); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Command/MakeConsumerCommand.php: -------------------------------------------------------------------------------- 1 | addArgument('name', InputArgument::REQUIRED, 'Consumer name'); 24 | } 25 | 26 | /** 27 | * @param InputInterface $input 28 | * @param OutputInterface $output 29 | * @return int 30 | */ 31 | protected function execute(InputInterface $input, OutputInterface $output): int 32 | { 33 | $name = $input->getArgument('name'); 34 | $output->writeln("Make consumer $name"); 35 | 36 | $path = ''; 37 | $namespace = 'app\\queue\\redis'; 38 | if ($pos = strrpos($name, DIRECTORY_SEPARATOR)) { 39 | $path = substr($name, 0, $pos + 1); 40 | $name = substr($name, $pos + 1); 41 | $namespace .= '\\' . str_replace(DIRECTORY_SEPARATOR, '\\', trim($path, DIRECTORY_SEPARATOR)); 42 | } 43 | $class = Util::nameToClass($name); 44 | $queue = Util::classToName($name); 45 | 46 | $file = app_path() . "/queue/redis/{$path}$class.php"; 47 | $this->createConsumer($namespace, $class, $queue, $file); 48 | 49 | return self::SUCCESS; 50 | } 51 | 52 | /** 53 | * @param $class 54 | * @param $queue 55 | * @param $file 56 | * @return void 57 | */ 58 | protected function createConsumer($namspace, $class, $queue, $file) 59 | { 60 | $path = pathinfo($file, PATHINFO_DIRNAME); 61 | if (!is_dir($path)) { 62 | mkdir($path, 0777, true); 63 | } 64 | $controller_content = << 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | namespace Webman\RedisQueue; 15 | 16 | use RedisException; 17 | use Throwable; 18 | 19 | class RedisConnection extends \Redis 20 | { 21 | /** 22 | * @var array 23 | */ 24 | protected array $config = []; 25 | 26 | /** 27 | * @param array $config 28 | * @return void 29 | * @throws RedisException 30 | */ 31 | public function connectWithConfig(array $config = []): void 32 | { 33 | if ($config) { 34 | $this->config = $config; 35 | } 36 | if (false === $this->connect($this->config['host'], $this->config['port'], $this->config['timeout'] ?? 2)) { 37 | throw new \RuntimeException("Redis connect {$this->config['host']}:{$this->config['port']} fail."); 38 | } 39 | if (!empty($this->config['auth'])) { 40 | $this->auth($this->config['auth']); 41 | } 42 | if (!empty($this->config['db'])) { 43 | $this->select($this->config['db']); 44 | } 45 | if (!empty($this->config['prefix'])) { 46 | $this->setOption(\Redis::OPT_PREFIX, $this->config['prefix']); 47 | } 48 | } 49 | 50 | /** 51 | * @param $command 52 | * @param ...$args 53 | * @return mixed 54 | * @throws Throwable 55 | */ 56 | protected function execCommand($command, ...$args) 57 | { 58 | try { 59 | return $this->{$command}(...$args); 60 | } catch (Throwable $e) { 61 | $msg = strtolower($e->getMessage()); 62 | if ($msg === 'connection lost' || strpos($msg, 'went away')) { 63 | $this->connectWithConfig(); 64 | return $this->{$command}(...$args); 65 | } 66 | throw $e; 67 | } 68 | } 69 | 70 | /** 71 | * @param $queue 72 | * @param $data 73 | * @param int $delay 74 | * @return bool 75 | * @throws Throwable 76 | */ 77 | public function send($queue, $data, int $delay = 0): bool 78 | { 79 | $queue_waiting = '{redis-queue}-waiting'; 80 | $queue_delay = '{redis-queue}-delayed'; 81 | $now = time(); 82 | $package_str = json_encode([ 83 | 'id' => time().rand(), 84 | 'time' => $now, 85 | 'delay' => $delay, 86 | 'attempts' => 0, 87 | 'queue' => $queue, 88 | 'data' => $data 89 | ]); 90 | if ($delay) { 91 | return (bool)$this->execCommand('zAdd' ,$queue_delay, $now + $delay, $package_str); 92 | } 93 | return (bool)$this->execCommand('lPush', $queue_waiting.$queue, $package_str); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Process/Consumer.php: -------------------------------------------------------------------------------- 1 | 11 | * @copyright walkor 12 | * @link http://www.workerman.net/ 13 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 14 | */ 15 | 16 | namespace Webman\RedisQueue\Process; 17 | 18 | use support\Container; 19 | use support\Context; 20 | use Webman\RedisQueue\Client; 21 | 22 | /** 23 | * Class Consumer 24 | * @package process 25 | */ 26 | class Consumer 27 | { 28 | /** 29 | * @var string 30 | */ 31 | protected $_consumerDir = ''; 32 | 33 | /** 34 | * @var array 35 | */ 36 | protected $_consumers = []; 37 | 38 | /** 39 | * StompConsumer constructor. 40 | * @param string $consumer_dir 41 | */ 42 | public function __construct($consumer_dir = '') 43 | { 44 | $this->_consumerDir = $consumer_dir; 45 | } 46 | 47 | /** 48 | * onWorkerStart. 49 | */ 50 | public function onWorkerStart() 51 | { 52 | if (!is_dir($this->_consumerDir)) { 53 | echo "Consumer directory {$this->_consumerDir} not exists\r\n"; 54 | return; 55 | } 56 | $dir_iterator = new \RecursiveDirectoryIterator($this->_consumerDir); 57 | $iterator = new \RecursiveIteratorIterator($dir_iterator); 58 | foreach ($iterator as $file) { 59 | if (is_dir($file)) { 60 | continue; 61 | } 62 | $fileinfo = new \SplFileInfo($file); 63 | $ext = $fileinfo->getExtension(); 64 | if ($ext === 'php') { 65 | $class = str_replace('/', "\\", substr(substr($file, strlen(base_path())), 0, -4)); 66 | if (is_a($class, 'Webman\RedisQueue\Consumer', true)) { 67 | $consumer = Container::get($class); 68 | $connection_name = $consumer->connection ?? 'default'; 69 | $queue = $consumer->queue; 70 | if (!$queue) { 71 | echo "Consumer {$class} queue not exists\r\n"; 72 | continue; 73 | } 74 | $this->_consumers[$queue] = $consumer; 75 | $connection = Client::connection($connection_name); 76 | $consumer_func = function ($message) use ($consumer) { 77 | try { 78 | $consumer->consume($message); 79 | } catch (\Throwable $e) { 80 | return throw $e; 81 | } finally { 82 | Context::destroy(); 83 | } 84 | }; 85 | $connection->subscribe($queue, $consumer_func); 86 | if (method_exists($connection, 'onConsumeFailure')) { 87 | $connection->onConsumeFailure(function ($exeption, $package) { 88 | $consumer = $this->_consumers[$package['queue']] ?? null; 89 | if ($consumer && method_exists($consumer, 'onConsumeFailure')) { 90 | return call_user_func([$consumer, 'onConsumeFailure'], $exeption, $package); 91 | } 92 | }); 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Redis.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright walkor 11 | * @link http://www.workerman.net/ 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | namespace Webman\RedisQueue; 15 | 16 | use RedisException; 17 | use Webman\Context; 18 | use Workerman\Coroutine\Pool; 19 | 20 | /** 21 | * Class RedisQueue 22 | * @package support 23 | * 24 | * Strings methods 25 | * @method static bool send($queue, $data, $delay=0) 26 | */ 27 | class Redis 28 | { 29 | 30 | /** 31 | * @var Pool[] 32 | */ 33 | protected static array $pools = []; 34 | 35 | /** 36 | * @param string $name 37 | * @return RedisConnection 38 | */ 39 | public static function connection($name = 'default') { 40 | $name = $name ?: 'default'; 41 | $key = "redis-queue.connections.$name"; 42 | $connection = Context::get($key); 43 | if (!$connection) { 44 | if (!isset(static::$pools[$name])) { 45 | $configs = config('redis_queue', config('plugin.webman.redis-queue.redis', [])); 46 | if (!isset($configs[$name])) { 47 | throw new \RuntimeException("RedisQueue connection $name not found"); 48 | } 49 | $config = $configs[$name]; 50 | $pool = new Pool($config['pool']['max_connections'] ?? 10, $config['pool'] ?? []); 51 | $pool->setConnectionCreator(function () use ($config) { 52 | return static::connect($config); 53 | }); 54 | $pool->setConnectionCloser(function ($connection) { 55 | $connection->close(); 56 | }); 57 | $pool->setHeartbeatChecker(function ($connection) { 58 | return $connection->ping(); 59 | }); 60 | static::$pools[$name] = $pool; 61 | } 62 | 63 | try { 64 | $connection = static::$pools[$name]->get(); 65 | Context::set($key, $connection); 66 | } finally { 67 | Context::onDestroy(function () use ($connection, $name) { 68 | try { 69 | $connection && static::$pools[$name]->put($connection); 70 | } catch (Throwable) { 71 | // ignore 72 | } 73 | }); 74 | } 75 | } 76 | return $connection; 77 | } 78 | 79 | /** 80 | * Connect to redis. 81 | * 82 | * @param $config 83 | * @return RedisConnection 84 | * @throws RedisException 85 | */ 86 | protected static function connect($config): RedisConnection 87 | { 88 | if (!extension_loaded('redis')) { 89 | throw new \RuntimeException('Please make sure the PHP Redis extension is installed and enabled.'); 90 | } 91 | $redis = new RedisConnection(); 92 | $address = $config['host']; 93 | $config = [ 94 | 'host' => str_starts_with($address, 'unix:///') ? $address : parse_url($address, PHP_URL_HOST), 95 | 'port' => parse_url($address, PHP_URL_PORT), 96 | 'db' => $config['options']['database'] ?? $config['options']['db'] ?? 0, 97 | 'auth' => $config['options']['auth'] ?? '', 98 | 'timeout' => $config['options']['timeout'] ?? 2, 99 | 'ping' => $config['options']['ping'] ?? 55, 100 | 'prefix' => $config['options']['prefix'] ?? '', 101 | ]; 102 | $redis->connectWithConfig($config); 103 | return $redis; 104 | } 105 | 106 | /** 107 | * @param $name 108 | * @param $arguments 109 | * @return mixed 110 | */ 111 | public static function __callStatic($name, $arguments) 112 | { 113 | return static::connection('default')->{$name}(... $arguments); 114 | } 115 | } 116 | --------------------------------------------------------------------------------