<?php
namespace services\common;
use common\components\Service;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Exchange\AMQPExchangeType;
use PhpAmqpLib\Message\AMQPMessage;
use PhpAmqpLib\Wire\AMQPTable;
use Exception;
use Closure;
use common\enums\RabbitMqEnum;
use Yii;
use yii\redis\Connection;
/**
* RabbitMq服务类
* Date: 2021/8/20
* Time: 14:56
*/
class RabbitMqService extends Service
{
public $host;
public $port = 5672;
public $userName;
public $passWord;
public $setting = [
'vhost' => '/',
'insist' => false,
'login_method' => 'AMQPLAIN',
'login_response' => null,
'locale' => 'en_US',
'connection_timeout' => 3.0,
'read_write_timeout' => 3.0,
'context' => null,
'keepalive' => false,
'heartbeat' => 0
];
// Send a message with the string "quit" to cancel the consumer.
public $cancelTag = 'quit';
public $connection = null;
/**
* 获取一个rabbitMQ连接
* @return AMQPStreamConnection
* @throws Exception
*/
public function connect()
{
$rabbitMqConfig = \Yii::$app->getComponents()['rabbitMq'];
unset($rabbitMqConfig['class']);
if ($rabbitMqConfig) {
$this->host = $rabbitMqConfig['host'];
$this->port = $rabbitMqConfig['port'];
$this->userName = $rabbitMqConfig['userName'];
$this->passWord = $rabbitMqConfig['passWord'];
$this->setting = $rabbitMqConfig['setting'];
}
$config = [
'host' => $this->host,
'port' => $this->port,
'user' => $this->userName,
'password' => $this->passWord,
'vhost' => $this->setting['vhost'],
'insist' => $this->setting['insist'],
'login_method' => $this->setting['login_method'],
'login_response' => $this->setting['login_response'],
'locale' => $this->setting['locale'],
'connection_timeout' => $this->setting['connection_timeout'],
'read_write_timeout' => $this->setting['read_write_timeout'],
'context' => $this->setting['context'],
'keepalive' => $this->setting['keepalive'],
'heartbeat'=>$this->setting['heartbeat']
];
$connection = new AMQPStreamConnection(...array_values($config));
if (!$connection->isConnected()) throw new Exception('Connect failed!');
$this->connection = $connection;
return $connection;
}
/**
* 生产一个消息并发送到指定(direct)交换机
* @param string $exchange
* @param string $queue_name 队列名称
* @param array $data
* @param string $handler_class
* @param string $method
* @param Closure $error_callback
* @param Closure $success_callback
* @param string $exchange_type 交换机类型:默认直连交换机
* @throws Exception
* @return int
*/
public function push(string $exchange, string $queue_name, array $data, string $handler_class = '', string $method = '', Closure $error_callback = null, Closure $success_callback = null,$exchange_type = AMQPExchangeType::DIRECT)
{
$message['data'] = $data;
$message['handler_class'] = $handler_class;
$message['method'] = $method;
/** @var AMQPStreamConnection $conn */
$conn = $this->connect();
$channel = $conn->channel();
$channel->exchange_declare($exchange, $exchange_type, false, false, false);
$channel->queue_declare($queue_name, false, true, false, false);
//绑定队列到交换机
$channel->queue_bind($queue_name, $exchange, $queue_name);
$message_id = $this->createMessageId();
$body = new AMQPMessage(json_encode($message,JSON_UNESCAPED_UNICODE), array('delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT, 'message_id' => $message_id, 'application_headers' => new AMQPTable(['retry' => 0])));
//消息发送状态回调
$channel->set_ack_handler(function (AMQPMessage $message) use ($success_callback) {
!empty($success_callback) && $success_callback($message);
});
$channel->set_nack_handler(function (AMQPMessage $message) use ($error_callback) {
!empty($error_callback) && $error_callback($message);
});
//开启消息发送状态监听
$channel->confirm_select();
$res = $this->setMessage($message_id, json_encode($message));
if (!$res) {
throw new Exception('设置消息失败');
}
$channel->basic_publish($body, $exchange, $queue_name);
$channel->wait_for_pending_acks();
$channel->close();
$conn->close();
return $message_id;
}
/**
* 延时队列生产
* @notice:此延时队列不能保证时间的精准,当业务处理出现阻塞,则在队列里已达到过期时间的消息并不会被发送到对应的队列。
* @param int $sec 延时秒数
* @param array $data 传递的数据
* @param string $handler_class 处理类
* @param string $method 处理方法
* @param array $params 处理类参数
* @throws Exception
* @return void
*/
public function delay(int $sec, array $data, string $handler_class = '', string $method = '', array $params = [])
{
$message['data'] = $data;
$message['handler_class'] = $handler_class;
$message['method'] = $method;
$message['params'] = $params;
$micro_sec = $sec * 1000;
/** @var AMQPStreamConnection $conn */
$conn = $this->connect();
$channel = $conn->channel();
$channel->exchange_declare(RabbitMqEnum::EXCHANGE_DELAY, 'direct', false, false, false);
$channel->exchange_declare(RabbitMqEnum::EXCHANGE_DELAY_CACHE, 'direct', false, false, false);
//死信交换机和路由
$tale = new AMQPTable();
$tale->set('x-dead-letter-exchange', RabbitMqEnum::EXCHANGE_DELAY);
$tale->set('x-dead-letter-routing-key', RabbitMqEnum::DELAY_HANDLE_QUEUE);
$tale->set('x-message-ttl', $micro_sec);
$queue_name = 'delay_cache_queue_' . $sec . 's';
//延时缓存队列声明及绑定
$channel->queue_declare($queue_name, false, true, false, true, false, $tale);
$channel->queue_bind($queue_name, RabbitMqEnum::EXCHANGE_DELAY_CACHE, $queue_name);
//延时处理队列声明及绑定
$channel->queue_declare(RabbitMqEnum::DELAY_HANDLE_QUEUE, false, true, false, false, false);
$channel->queue_bind(RabbitMqEnum::DELAY_HANDLE_QUEUE, RabbitMqEnum::EXCHANGE_DELAY, RabbitMqEnum::DELAY_HANDLE_QUEUE);
$message_id = $this->createMessageId();
$res = $this->setMessage($message_id, json_encode($message),$sec+3600);
if (!$res) {
throw new Exception('设置消息失败');
}
$body = new AMQPMessage(json_encode($message), array(
'expiration' => $micro_sec,
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
'message_id' => $message_id
));
$channel->basic_publish($body, RabbitMqEnum::EXCHANGE_DELAY_CACHE, $queue_name);
$channel->close();
$conn->close();
}
/**
* 延时队列生产
* @notice:此延时队列不能保证时间的精准,当业务处理出现阻塞,则在队列里已达到过期时间的消息并不会被发送到对应的队列。
* @param int $sec 延时秒数
* @param array $data 传递的数据
* @param string $handler_class 处理类
* @param string $method 处理方法
* @param array $params 处理类参数
* @throws Exception
* @return void
*/
public function delayTask(int $sec, array $data, string $handler_class = '', string $method = '', $consume_type_key = 'delay', array $params = [])
{
$message['data'] = $data;
$message['handler_class'] = $handler_class;
$message['method'] = $method;
$message['params'] = $params;
$micro_sec = $sec * 1000;
$consume_type = RabbitMqEnum::getConsumeType($consume_type_key);
/** @var AMQPStreamConnection $conn */
$conn = $this->connect();
$channel = $conn->channel();
$channel->exchange_declare($consume_type['exchange'], 'direct', false, false, false);
$channel->exchange_declare(RabbitMqEnum::EXCHANGE_DELAY_CACHE, 'direct', false, false, false);
//死信交换机和路由
$tale = new AMQPTable();
$tale->set('x-dead-letter-exchange', $consume_type['exchange']);
$tale->set('x-dead-letter-routing-key', $consume_type['queue_name']);
$tale->set('x-message-ttl', $micro_sec);
$queue_name = $consume_type_key == 'delay' ? 'delay_cache_queue_' . $sec . 's' : 'delay_'.$consume_type_key.'_' . $sec . 's';
//延时缓存队列声明及绑定
$channel->queue_declare($queue_name, false, true, false, true, false, $tale);
$channel->queue_bind($queue_name, RabbitMqEnum::EXCHANGE_DELAY_CACHE, $queue_name);
//延时处理队列声明及绑定
$channel->queue_declare($consume_type['queue_name'], false, true, false, false, false);
$channel->queue_bind($consume_type['queue_name'], $consume_type['exchange'], $consume_type['queue_name']);
$message_id = $this->createMessageId();
$res = $this->setMessage($message_id, json_encode($message),$sec+3600);
if (!$res) {
throw new Exception('设置消息失败');
}
$body = new AMQPMessage(json_encode($message), array(
'expiration' => $micro_sec,
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
'message_id' => $message_id
));
$channel->basic_publish($body, RabbitMqEnum::EXCHANGE_DELAY_CACHE, $queue_name);
$channel->close();
$conn->close();
}
/**
* 消费者监听
* @param string $exchange
* @param string $queue_name
* @param Closure $callback
* @param string $exchange_type 交换机类型:默认直连交换机
* @throws Exception
* @return void
*/
public function listen(string $exchange, string $queue_name, Closure $callback = null,$exchange_type = AMQPExchangeType::DIRECT)
{
/** @var AMQPStreamConnection $conn */
$conn = $this->connect();
if (!$conn) {
throw new Exception('Connect failed!');
}
$channel = $conn->channel();
$channel->exchange_declare($exchange, $exchange_type, false, false, false);
$channel->queue_declare($queue_name, false, true, false, false);
//绑定队列到交换机
$channel->queue_bind($queue_name, $exchange, $queue_name);
$channel->basic_qos(null, 1, null);
$channel->basic_consume($queue_name, '', false, false, false, false, function (AMQPMessage $message) use ($callback, $channel, $exchange, $queue_name) : void {
//消息确认
$message->getChannel()->basic_ack($message->getDeliveryTag());
//消息判断是否消费过
$message_id = $message->get('message_id');
//echo 'message_id:' . $message_id.PHP_EOL;
$message_content = $this->checkMessage($message_id);
if (!empty($message_content) && !empty($callback)) {
$res = $callback($message);
if (false === $res) {
//投递到 重试队列
$this->retry($message);
} else {
//消费成功,删除消息
$this->deleteMessage($message_id);
}
}
//!empty($message_content) && !empty($callback) && $callback($message);
});
while (count($channel->callbacks)) {
$channel->wait();
}
$channel->close();
$conn->close();
}
/**
* 取出一个队列里的消息
* @param string $queue_name
* @return AMQPMessage|null
* @throws
*/
public function pull(string $queue_name)
{
/** @var AMQPStreamConnection $conn */
$conn = $this->connect();
$channel = $conn->channel();
$channel->queue_declare($queue_name, false, true, false, false);
/* @var AMQPMessage $message */
$message = $channel->basic_get($queue_name, true);
return $message;
}
/**
* 消息添加延时重试
* @desc 消息消费失败,加入重试队列
* @param object $message
* @param string $exchange_type 交换机类型:默认直连交换机
* @throws Exception
* @return void
*/
public function retry($message,$exchange_type = AMQPExchangeType::DIRECT)
{
$message_id = $message->get('message_id');
//headersObject 是一个AMQPTable对象
$headersObject = $message->get_properties()['application_headers'];
$headersArr = $headersObject->getNativeData();
//投递到重试队列
$headersArr['retry'] = intval($headersArr['retry']);
$headersArr['retry']++;
$sec = $headersArr['retry'] * 20; //N个20秒后执行
$micro_sec = $sec * 1000;
if ($headersArr['retry'] <= RabbitMqEnum::MAX_RETRY_NUM) {
//这里加入 延时重试队列
$conn = $this->connect();
$channel = $conn->channel();
$channel->exchange_declare(RabbitMqEnum::EXCHANGE_DELAY_RETRY, $exchange_type, false, false, false);
$channel->exchange_declare(RabbitMqEnum::EXCHANGE_DELAY_RETRY_CACHE, $exchange_type, false, false, false);
//死信交换机和路由
$tale = new AMQPTable();
$tale->set('x-dead-letter-exchange', RabbitMqEnum::EXCHANGE_DELAY_RETRY);
$tale->set('x-dead-letter-routing-key', RabbitMqEnum::DELAY_RETRY_QUEUE);
//$tale->set('x-message-ttl', $micro_sec);
$queue_name = RabbitMqEnum::DELAY_RETRY_QUEUE . '_' . $sec . 's';
//延时缓存队列声明及绑定
$channel->queue_declare($queue_name, false, true, false, true, false, $tale);
$channel->queue_bind($queue_name, RabbitMqEnum::EXCHANGE_DELAY_RETRY_CACHE, $queue_name);
//延时处理队列声明及绑定
$channel->queue_declare(RabbitMqEnum::DELAY_RETRY_QUEUE, false, true, false, false, false);
$channel->queue_bind(RabbitMqEnum::DELAY_RETRY_QUEUE, RabbitMqEnum::EXCHANGE_DELAY_RETRY, RabbitMqEnum::DELAY_RETRY_QUEUE);
$body = new AMQPMessage($message->getBody(), array(
'expiration' => $micro_sec,
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
'message_id' => $message_id,
'application_headers' => new AMQPTable($headersArr)
));
$channel->basic_publish($body, RabbitMqEnum::EXCHANGE_DELAY_RETRY_CACHE, $queue_name);
$channel->close();
$conn->close();
} else {
echo 'retry too many times' . PHP_EOL;
//重试太多次
Yii::error('message retry too many times , message_id :'.$message_id.' ,message content:' . $message->getBody());
Yii::getLogger()->flush(true);
}
}
/**
* 生成MQ消息全局唯一ID
* @return mixed
*/
public function createMessageId()
{
$key = RabbitMqEnum::RABBITMQ_QUEUE_ID;
$config = \Yii::$app->getComponents()['redis'];
unset($config['class']);
$redis = new Connection($config);
$id = $redis->incr($key);
$redis->close();
return $id;
}
/**
* redis设置消息信息
* @param $message_id
* @param $data
* @return bool
* @throws
*/
public function setMessage($message_id, $data,$expire_time=86400)
{
$key = RabbitMqEnum::QUEUE_MESSAGE_KEY_PREFIX . $message_id;
$config = \Yii::$app->getComponents()['redis'];
unset($config['class']);
$redis = new Connection($config);
$res = $redis->executeCommand('SET', [$key, json_encode($data), 'EX', $expire_time, 'NX']);
$redis->close();
return $res;
}
/**
* 判断消息是否存在(不存在:已消费)
* @param $message_id
* @return mixed
* @throws
*/
public function checkMessage($message_id)
{
$key = RabbitMqEnum::QUEUE_MESSAGE_KEY_PREFIX . $message_id;
$config = \Yii::$app->getComponents()['redis'];
unset($config['class']);
$redis = new Connection($config);
$res = $redis->executeCommand('GET', [$key]);
$redis->close();
return $res;
}
/**
* 消息消费完,删除
* @param $message_id
* @return mixed
*/
public function deleteMessage($message_id)
{
$key = RabbitMqEnum::QUEUE_MESSAGE_KEY_PREFIX . $message_id;
$config = \Yii::$app->getComponents()['redis'];
unset($config['class']);
$redis = new Connection($config);
$res = $redis->del($key);
$redis->close();
return $res;
}
}
php rabbitmq生产者代码
最新推荐文章于 2024-10-14 00:28:24 发布