RabbitMQ
配置文件
配置文件路径
ubuntu,centeos
#默认路径
/etc/rabbitmq/
#如需更改配置文件,可通过常量更改
#主要配置
RABBITMQ_CONFIG_FILE = /path/to/location/rabbitmq.conf
#高级配置
RABBITMQ_ADVANCED_CONFIG_FILE = /path/to/location/advanced.config
#环境变量配置
RABBITMQ_CONF_ENV_FILE = /path/to/location/rabbitmq-env.conf
LOG
log location
#两种方法定义日志文件
#1 config文件定义
vim /etc/rabbitmq/rabbitmq.conf
log.file=rabbit.log [false关闭日志]
log.dir=/path/to/rabbit
log.file.level=debug []
#rabbitmq常量
RABBITMQ_LOGS=/path/to/log
log rotation(日志循环,更换日志文件)
rabbit日志默认是往文件后追加,当安装了debian 和 rpm包后,就会自动切换成日志循环
#时间切换
log.file.rotation.date=$D0
#日志个数
log.file.rotation.count=5
#文件大小切换 bit单位
log.file.size=1024*1000(1MB)
console log
#控制台log,直接打印出来,也会记录到开启的log日志中
log.console=true
log.console.level=debug
Logging to Syslog
log日志转发到rabbit系统日志上,需要用到RFC3164 或者 RFC5424协议
#日志记录到 系统日志内
log.syslog = true
log.syslog.transport = tls
log.syslog.protocol = rfc5424
#TLS证书
log.syslog.ssl_options.cacertfile = /path/to/ca_certificate.pem
log.syslog.ssl_options.certfile = /path/to/client_certificate.pem
log.syslog.ssl_options.keyfile = /path/to/client_key.pem
#syslog配置
log.syslog.host = my.syslog-server.local (本地机器名,不能用本地ip)
log.syslog.ip = 10.10.10.10
log.syslog.port = 1514
消息分发策略
- 发布订阅
- 轮询分发
- 公平分发
- 重发
- 消息拉取
如何组成部分
核心概念
- Server:又称broker,接收客户端的连接,实现AMQP实体服务
- connection:连接,应用程序与Broker的网络连接TCP/IP 三次握手/四次挥手
- channel:网络管道,几乎所有操作都在channel中进行,channel是进行消息读写的通道,客户端可以建立多个channel,每个channel代表一个会话任务
- message:消息,服务与应用程序之间传输的数据,由properties和body组成,properties消息的特性,例如优先级,延迟等,body则是消息体的内容
- virtual host:虚拟主机,用户进行逻辑隔离,是最上层的消息路由。一个虚拟主机可以有多个exchange和queue,但是同一个虚拟主机内不能有同名的exchange
- exchange:交换机,接收消息,根据路由发送消息到绑定的queue。他不具备储存消息的能力
- bindings:exchange和queue之间的虚拟连接,bindings中可以多个routing key
- routing key:是一个路由规则,虚拟机用它来确定如何路由一个特定消息。类似于分类,一个queue能有多个routing key相当于属于多个分类
交换机exchange
交换是发送消息的AMQP 0-9-1实体。交换接收一条消息并将其路由到零个或多个队列中。使用的路由算法取决于 交换类型和称为绑定的规则。
交换机类型
- 默认交换是broker预先声明的不带名称(空字符串)的直接交换。它具有一个特殊的属性,使其对于简单的应用程序非常有用:每个创建的队列都使用与队列名称相同的路由键自动绑定到该队列。
- direct 直接交换机:根据routingkey发送到指定的零个或多个队列
- fanout 广播交换机:不需要绑定routing key直接把消息广播到绑定在此交换机上的所有队列
- topic 主题交换机:模糊匹配路由,
- headers heander交换机
交换机属性
-
name
-
durability:broker重启,交互机依旧存活
-
auto-deleted:当最后一个连接断开,交互机自动删除。如果UI界面exchange上有个“D”代表持久
-
arguments:参数,用户特殊功能,UI界面可以查看有什么参数。
队列Queue
属性
-
name:AMQP 0-9-1代理(broker)可以代表应用程序生成唯一的队列名称。要使用此功能,请传递一个空字符串作为队列名称参数。生成的名称将与队列声明响应一起返回给客户端。
-
durability持久性:队列将在broker重新启动后依旧存活
-
Exclusive独占性:仅由一个连接使用,并且该连接关闭时,队列将被删除
-
auto-delete自动删除:当最后一个使用者退订时,具有至少一个使用者的队列将被删除
-
参数:可选;由插件和特定于代理的功能使用,例如消息TTL,队列长度限制等。UI界面可以查看有什么参数
消费者consumer
在AMQP 0-9-1模型中,应用程序可以通过两种方式执行此操作:
- 订阅队列以将消息传递给他们(“推送API”):这是推荐的选项
- 轮询(“拉动API”):这种方式效率很低,在大多数情况下应避免使用
//推
$channle->basic_consume($callback); // callback回调函数,不需要用while(true),自动回调的
//拉
for(1) {
$queue->basic_get();
}
绑定binding
绑定是交换使用(除其他事项外)将消息路由到队列的规则。为了指示交换机E将消息路由到队列Q,必须将Q绑定到E。绑定可能具有某些交换机类型使用的可选 路由键属性。路由键的目的是选择发布到交换机的某些消息,以将其路由到绑定队列。换句话说,路由键的作用类似于过滤器。
消息确认机制
消费者应用程序有时可能无法处理单个消息,有时甚至会崩溃。网络问题也有可能引起问题。这就提出了一个问题:**代理broker何时从队列中删除消息?**有两种确认模式
- 代理将消息发送到应用程序后(使用basic.deliver或basic.get-ok方法)。
- 应用程序发送回确认后(使用basic.ack方法)。
五种模式
简单模式
生产者使用默认的exchange投递到队列中
发布订阅publish/subscribe
多个队列绑定一个交互机,交互机没有给队列绑定路由,直接分发给多个队列(exchange type 是 fanout)
producter 生产者
//创建一个连接
$connection = connectRabbitMq();
//创建一个通道
$channel = $connection->channel();
//创建exchange
$exchange_name = "fanout_exchange";
$exchange_type = "fanout"; //广播模式
$channel->exchange_declare($exchange_name, $exchange_type);
//连接queue,如果没有queue则会创建
/**
* @param string queue 队列名称
* @param boolean passive
* @param boolean durable 持久化 是否存盘,如果重启后队列没了就是非持久化
* @param boolean exclusive 排他性
* @param boolean auto_delete 自动删除 最后一个连接释放,自动删除队列
* @param
*/
$queue_name = 'order';
$channel->queue_declare($queue_name, false, false, false, false);
$channel->queue_bind($queue_name, $exchange_name); //一个交互机绑定多个队列,并且不指定路由
$queue_name = 'login';
$channel->queue_declare($queue_name, false, false, false, false);
$channel->queue_bind($queue_name, $exchange_name);
//创建消息对象
$msg = new AMQPMessage('is order module!');
//投递消息
$channel->basic_publish($msg, $exchange_name);
//关闭连接
closeConnect($channel, $connection);
echo "finish!!!";
效果
-
生成 1个 fanout 交互机
-
生成 2个队列,绑定到同一个交互机上,不指定路由
-
消息广播到2个队列中
路由模式route
多个队列绑定一个交互机,每个queue有多个路由,交互机根据路由分配任务到指定的队列(exchange type是direct)
producter 生产者
//创建一个连接
$connection = connectRabbitMq();
//创建一个通道
$channel = $connection->channel();
//创建exchange
$exchange_name = "direct_exchange";
$exchange_type = "direct"; //指定路由模式
$channel->exchange_declare($exchange_name, $exchange_type);
#-----------------------------------创建order_queue-----------------------------------------------------#
//连接queue,如果没有queue则会创建
/**
* @param string queue 队列名称
* @param boolean passive
* @param boolean durable 持久化 是否存盘,如果重启后队列没了就是非持久化
* @param boolean exclusive 排他性
* @param boolean auto_delete 自动删除 最后一个连接释放,自动删除队列
* @param
*/
$queue_name = 'order';
$channel->queue_declare($queue_name, false, false, false, false);
//交换机-队列 绑定路由,一个交互机于同一个队列绑定两个路由,相当于给同一个队列加了2个分类
$channel->queue_bind($queue_name, $exchange_name, 'email_routing');
$channel->queue_bind($queue_name, $exchange_name, 'sms_routing');
//创建消息对象
$msg = new AMQPMessage('is order module!');
//投递消息
$channel->basic_publish($msg, $exchange_name, 'sms_routing');
#-----------------------------------创建login_queue----------------------------------------------------------#
$queue_name = 'login';
$channel->queue_declare($queue_name, false, false, false, false);
//交换机-队列 绑定路由
$routing_key = 'wechat_routing';
$channel->queue_bind($queue_name, $exchange_name, $routing_key);
//创建消息对象
$msg = new AMQPMessage('is login module!');
//投递消息
$channel->basic_publish($msg, $exchange_name, $routing_key);
//关闭连接
closeConnect($channel, $connection);
echo "finish!!!";
效果
- 创建了一个direct交换机
- 2个队列:login,order
主题模式Topics
work模式-轮询模式
一个队列对应着N个消费者。rabbitmq默认消息不会被广播到多个消费者,M个消息会平均的分给N个消费者(每个消费者获取到M/N个消息)。如果需要广播,就使用一个fanout,一个交互机绑定多个队列,每个队列绑定一个或多个消费者。
work模式-公平分发模式
一个队列对应着N个消费者。每个消费者的消费能力不同,公平分发模式 queue监听哪个消费者空闲 就会把任务投递到此消费者
$channel->basic_qos( null, prefetch_count=1, null );
prefetch_count=1
设置每次只有给消费者发送一个消息,需要等待消费者确认(自动或者手动确认,如需了解查看下面的消费者确认)后再发送下一个,来确保消费者已经空闲
rabbitmq高级-队列过期时间ttl
过期时间ttl表示对消息设置预期时间,在这个时间内都可以被消费者获取消费,过了这个时间会被移除/删除。
有队列和消息都可以设置过期时间,可是只有队列设置过期时间才能使用死信队列
rabbitmq高级-死信队列
概述
DLX【Dead-Letter-Exchange】,也称为私信交互机。当消息在一个队列中变成死信(dead message )之后,被发送到另外一台交换机,这个交互机就是DLX,绑定DLX的队列称之为死信队列。
消息被变成死信的原因
- 消息被拒绝,消费者使用
basic.reject
或basic.nack
并将requeue参数设置为false
来否定该消息 - 消息过期ttl
- 队列达到最大长度
消息死信后的具体步骤
前提:X
交互机绑定A
队列, A
队列 指定了过期后的 Y
死信交互机, Y
死信交互机 绑定了 B
队列
步骤:
- 消息通过某个
X
交互机投递到A
队列 - 消息在某个时间变成死信,因为在
A
队列中,所以被投递到Y
死信交互机 Y
死信交互机绑定了B
队列,所以投递到B
队列中
当无法路由消息时,消息可能会返回给发布者,被丢弃,或者,如果代理实施了扩展,则可能被放入所谓的“死信队列”中
$connection = new AMQPStreamConnection($host, $port, $user, $pass);
$channel = $connection->channel();
# 创建X交互机
$channel->exchange_declare($exchange_name, self::$exchanger_type, self::$exchanger_passive, self::$exchanger_durable, self::$exchanger_auto_delete);
# 创建Y死信交互机
$channel->exchange_declare("dead_exchanger", self::$dead_exchanger_type, self::$exchanger_passive, self::$exchanger_durable, self::$exchanger_auto_delete);
if (empty($timeout_queue_name)) {
$channel->queue_declare($queue_name, self::$queue_passive, self::$queue_durable, false, self::$queue_auto_delete);
$channel->queue_bind($queue_name, $exchange_name);
} else if ($queue_timeout != -1) { // 如果有 dead queue,且 timeout 的定义不为空
// 定义超时删除并自动进入死信(超时)队列的消息属性
$route_key = $queue_name;
# A队列 指定 Y死信交互机
$queue_args = new AMQPTable([
'x-message-ttl' => $queue_timeout,
'x-dead-letter-exchange' => self::$dead_exchanger_name,
'x-dead-letter-routing-key' => $route_key
]);
# 创建A队列,并指定Y死信交互机
$channel->queue_declare($queue_name, self::$queue_passive, self::$queue_durable, false, self::$queue_auto_delete, false, $queue_args);
# 创建B队列
$channel->queue_declare($timeout_queue_name, self::$queue_passive, self::$queue_durable, false, self::$queue_auto_delete); // 声明一个延时队列
# X交互机绑定A队列
$channel->queue_bind($queue_name, $exchange_name);
# Y死信交互机 绑定 B队列,B队列变成死信队列
$channel->queue_bind($timeout_queue_name, self::$dead_exchanger_name, $route_key); // 绑定死信(超时)队列的路径
}
$channel->basic_qos(null, 1, null); // 设置一次只从queue取一条信息,在该信息处理完(消费者没有发送ack给mq),queue将不会推送信息给该消费者
// no_ack:false 表示该队列的信息必须接收到消费者信号才能被删除
// 消费者从queue拿到信息之后,该信息不会从内存中删除,需要消费者处理完之后发送信号通知mq去删除消息(如果没此通知,queue会不断积累旧的信息不会删除)
// 超时队列:推送message到消息队列,但不主动去该队列获取message,等到ttl超时,自动进入绑定的死信队列,在死信队列处理业务
if (empty($timeout_queue_name)) {
$channel->basic_consume($queue_name, '', false, false, false, false, $consume);
} else {
# 如果定义了$timeout_queue_name,跳过了前面一个if,导致queue_name是不会有消费者去消费信息的,信息过期后死信,进入到这个else
$channel->basic_consume($timeout_queue_name, '', false, false, false, false, $consume);
}
rabbitmq高级-延时队列(倒计时)
在rabbitmq中不存在延时队列,但是我们可以通过设置消息的过期时间和死信队列来模拟出延时队列。消费者监听死信交换器绑定的队列,而不要监听消息发送的队列。
单个死信队列完成:
- 随意起一个队列A和交互机A,队列不开消费者,
- 再起一个死信交互B及和死信队列B。
- 消息投递(设置了消息过期时间的消息)到A交换机进入A队列,等待消息过期
- 消息过期后,进入到死信队列中,死信队列的消费者获取到数据
- 数据中保存着业务队列的消息,死信队列投递到业务队列进行消费
不足之处:
- 如果整个系统都用这个队列A作为等待消息过期的中转站,有可能会超出队列长度
多个死信队列的做法
-
创建天,时,1分为时间粒度的 交换机,队列,死信交换机,死信队列,一共3种粒度* 4种类型 = 12个。并设置每个队列的过期时间,例如天队列过期时间设置成24*3600
-
在创建业务逻辑的交换机和队列
-
开启 死信队列的消费者 和 业务逻辑的消费者
-
消息根据业务设置的过期时间expire,判断投递到哪个时间粒度的交换机中。例如15天(expire = 15 * 24 2600)过期,那么投递到天的交换机,然后会进入天的队列,且消息内的expire = expire - 360024
-
因为天队列没有消费者,所以消息一直再队列中。当过了1天消息过期,进入死信队列。
-
死信队列消费者消费,消费者代码判断 expire>243600进入天队列,243600>expire>3600进入时队列,expire的值再次减少一个时间粒度的时间戳。
-
如此循环6步骤,来到分为单位的粒度的队列中,如果24*3600>expire>60大于1分再次进去分队列,如果小于expire<60投递到业务交换机,进入业务队列,完成消费
网络协议
rabbitmq支持多种协议,他们的传输层都是TCP协议,
- AMQP 0-9-1
- AMQP 1.0
- RABBITMQ STREAM
- MQTT
- STOMP
AMQP 0-9-1 Model
官网资料https://www.rabbitmq.com/tutorials/amqp-concepts.html
AMQP 0-9-1 提供了一种通过单个 TCP 连接进行tcp多路复用的连接方式。这意味着应用程序可以在单个连接上打开多个称为通道的“轻量级连接”。AMQP 0-9-1 客户端在连接后打开一个或多个通道,并在通道上执行协议操作(管理拓扑、发布、消费)。
TCP的单个channel之间是有序性的
消息确认机制
将消息传递给消费者时,消费者会自动或在应用程序开发人员选择后立即通知broker。使用消息确认时,broker只有在收到该消息(或消息组)的通知时,才会从队列中完全删除该消息。
消费者确认和发布者确认
消费者确认(交货确认)
当RabbitMQ将消息传递给消费者时,rabbitmq需要知道何时考虑成功发送消息。哪种逻辑最佳取决于系统,因此,它主要是一个应用程序决策。在AMQP 0-9-1当消费者使用注册它是由basic.consume方法或消息是与所述需求取出basic.get方法。
/**
* @param no_ack 不需要确认,需要确认机制时,设置为false
* no_ack == true 的时候是自动应答,投递成功后就会删除消息,在生成环境不推荐使用
* no_ack == false 手动应答,也是确认机制,需要业务代码返回true后才会删除消息,推荐使用
*/
$channel->basic_consume($queue_name, '', false, false, false, false, $consume);
采用消息确认机制后,只要令noAck=false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为RabbitMQ会一直持有消息直到消费者显式调用basicAck为止。
自动确认
在自动确认模式下,消息在发送后立即被认为是发送成功。 这种模式可以提高吞吐量(只要消费者能够跟上),不过会降低投递和消费者处理的安全性。 这种模式通常被称为“发后即忘”。 与手动确认模式不同,如果消费者的TCP连接或信道在成功投递之前关闭,该消息则会丢失。
手动发送确认消息
channel->basic_ack
用于 肯定确认,告诉broker可以删除消息channel->basic_nack
用于 否定确认,消息可以重新入队basic.reject
用于 否定确认,它虽然是否定但是也是通知broker删除消息
$consume = function($msg){
// 手动确认消息,发送消息
$channel->basic_ack($msg->delivery_info['delivery_tag']);
}
//通道预期值设置,work模式内使用了
$channel->basic_qos(null, 1, null);
//AMQP_AUTOACK
$consume_tag = ''; //手动
$consume_tag = AMQP_AUTOACK; //自动,128
$channel->basic_consume($queue_name, $consume_tag, false, false, false, false, $consume);
消息的推送和拉取
//推
$channle->basic_consume($callback); // callback回调函数,不需要用while(true),自动回调的
//拉
for(1) {
$queue->basic_get();
}
通道预期值设置
消费者的手动确认本质上也是异步的,存在一个未确认消息的窗口。开发人员通常会希望限制此窗口的大小,以避免在用户端出现无限制的缓冲区问题。这是通过使用basic.qos
方法设置“预取计数”值来完成的
例如,假设在通道Ch上有未确认信息a,b,c,d,并且通道 Ch的预取计数设置为4,RabbitMQ将不会在Ch上传递任何信息,除非至少有一个未完成的传递被确认
$channel->basic_qos(prefetch_size, prefetch_count=4, a_gloabl);
发布者确认
发布者事务确保持消息可靠性
使用标准AMQP 0-9-1,确保消息不会丢失的唯一方法是使用事务-使通道具有事务性,然后对每个消息或一组消息进行发布,提交。在这种情况下,会不必要地增加重量,并使吞吐量降低250倍。
客户端阻塞等待事务完成
发布者publicsher confirm模式 确保消息可靠性
为此,引入了一种确认机制publicsher confirm模式。它模仿了协议中已经存在的消费者确认机制。一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),这就使得生产者知晓消息已经正确到达了目的地了。
// 开启 publisher confirm 模式
$channel->confirm_select();
当客户端socket是异步处理,当等待前一条消息的ack回复时,可以发送下一条消息。如果rabbitmq内部错误,会触发nack命令,可以再回调方法中处理nack命令
$nackCallback = function ($callback) use ($queue_name, $param, $obj_id, &$status) {
//todo:重新投递消息或者打错误log
return;
};
$channel->set_nack_handler($nackCallback);
消息的可靠性
发布者 到 交互机的可靠机制
publicsher confirm机制,具体查看前面的发布者确认
交互机 到 队列 的可靠机制
mandatory
和 immediate
是channel.basicPublish
方法中的两个参数。他们两个都有消息未到达指定队列时将消息返回给生产者的功能。
-
immediate
因为性能原因再rabbitmq3.0淘汰。 -
mandatory
设置为true
,则进入队列失败时,会调用basic.return命令将消息返回,通过监听信号的方式。如果设置成false,消息将会丢弃// 交换机路由不到正确的队列,会触发该回调 $failure = function ($reply_code, $reply_text, $exchange, $routing_key) use ($queue_name, $param, $obj_id, &$status) { $status = false; $reply = compact("reply_code", "reply_text", "exchange", "routing_key"); $log = "publish exception - {$queue_name} not found, exchange={$exchange},param={$param},obj_id={$obj_id},reply:" . json_encode($reply); Loggers::getInstance("rabbitmq_task")->warning($log); return; }; $channel->set_return_listener($failure);// basic.return回调 $channel->basic_publish($msg, $exchange_name, '', self::$message_mandatory); // mandatory属性:如果交换机无法根据路由找到队列,为true会将消息以basic.return信号返回生产者,为false直接丢弃信息 $channel->wait_for_pending_acks_returns(); // 主要监听 basic.return 事件信号
消息在队列储存时的可靠机制
-
队列持久化,
durable
设置为true
。$channel->queue_declare($queue_name, self::$queue_passive, self::$queue_durable=true, false, self::$queue_auto_delete);
-
消息持久化,delivery_mode设置为2
$attributes['delivery_mode'] = AMQPMessage::DELIVERY_MODE_PERSISTENT; // 声明消息持久化 $msg = new AMQPMessage($param, $attributes);
队列 到 消费者 的可靠机制
消息确认机制 message acknowledgement。具体查看上面的消费者确认
参考
https://blog.csdn.net/u013256816/article/details/79147591
消息队列高可用和高可靠
高可用:集群,不会一台机器宕机影响到整个服务不可用
集群模式
- master-slave 主从共享数据的部署方式
master和slave都是 rabbitmq-server,他们共享消息存储数据,master是统一接收写入的message,然后在集群内部分发的不同主机上的slave-rabbitmq-server,就算master挂掉了,其他slave-server还能将消息推到consumer(消费者),但是无法在接受新的消息。
- master-slave主从同步部署方式
这个就跟mysql的主从一样,是异步将消息拷贝到不同的slave-server机上,他们并不是共享消息存储数据的。这样能做到负载均衡的效果,多个消费者去不同的slave机器上进行消费,但是master挂了,还是无法写入数据
- 多主集群同步部署模式
多写的模式,每一台server都会写入同一个文件,消费者根据策略去某一台机器就行读取数据(但是其他机器的消息怎么办,会自动被过滤掉吗??)
- 多主集群转发部署模式
这个并不是把消息投入到所有的server上,A-server接受到了x消息后,会将x消费的元数据(数据的相关描述和存放主机记录)同步到B-server上,当消费者到B-server拉取数据时,B-server会转发到A-server上完成响应。这样就不会一个数据存放多个位置。
元数据其实同文件系统内的差不多,是一些文件基本信息
完整代码
broker + 消费者
public static function doListen($queue_index)
{
$_SERVER['SERVER_ADDR'] = getHostByName(getHostName());
list($servers, $queue_name, $timeout_queue_name, $exchange_name, $callback, $consumers, $trace, $queue_timeout, $memory_limit, $db_pconnect) = static::getQueueConfig($queue_index);
// 随机选一个 server
$server = self::selectServer($servers);
$host = RabbitmqConfig::$arrServers[$server]['host'];
$port = RabbitmqConfig::$arrServers[$server]['port'];
$user = RabbitmqConfig::$arrServers[$server]['user'];
$pass = RabbitmqConfig::$arrServers[$server]['password'];
// 直连本机
//$host = '0.0.0.0';
//$port = 5672;
//$user = 'guest';
//$pass = 'uB8!yDEmpeh9';
$connection = new AMQPStreamConnection($host, $port, $user, $pass);
$channel = $connection->channel();
$channel->exchange_declare("dead_exchanger", self::$dead_exchanger_type, self::$exchanger_passive, self::$exchanger_durable, self::$exchanger_auto_delete);
$channel->exchange_declare($exchange_name, self::$exchanger_type, self::$exchanger_passive, self::$exchanger_durable, self::$exchanger_auto_delete);
if (empty($timeout_queue_name)) {
$channel->queue_declare($queue_name, self::$queue_passive, self::$queue_durable, false, self::$queue_auto_delete);
$channel->queue_bind($queue_name, $exchange_name);
} else if ($queue_timeout != -1) { // 如果有 dead queue,且 timeout 的定义不为空
// 定义超时删除并自动进入死信(超时)队列的消息属性
$route_key = $queue_name;
$queue_args = new AMQPTable([
'x-message-ttl' => $queue_timeout,
'x-dead-letter-exchange' => self::$dead_exchanger_name,
'x-dead-letter-routing-key' => $route_key
]);
$channel->queue_declare($queue_name, self::$queue_passive, self::$queue_durable, false, self::$queue_auto_delete, false, $queue_args);
$channel->queue_declare($timeout_queue_name, self::$queue_passive, self::$queue_durable, false, self::$queue_auto_delete); // 声明一个延时队列
$channel->queue_bind($queue_name, $exchange_name);
$channel->queue_bind($timeout_queue_name, self::$dead_exchanger_name, $route_key); // 绑定死信(超时)队列的路径
}
$consume = function ($msg) use ($callback, $channel, $trace, $queue_name, $db_pconnect, $exchange_name) {
try {
// 设置上下文 LogId
$contextLogId = json_decode($msg->body, true)['context_log_id'];
if ($contextLogId) {
Loggers::getInstance("rabbitmq_task")->setLogId($contextLogId);
}
if ($trace) {
Loggers::getInstance("rabbitmq_task")->notice("run rabbitmq handler: [queue_name {$queue_name}] param=" . $msg->body);
}
// 前置调用
RabbitmqTask::beforeCallback($msg->body);
// 方法实现
$res = call_user_func($callback, $msg->body);
if ((is_bool($res) && $res == false) || (is_array($res) && $res['status'] != 0)) {
Loggers::getInstance('rabbitmq_task')->warning("rabbitmq task run error: {$msg->body}, res: " . json_encode($res));
}
// 后置调用
RabbitmqTask::afterCallback($msg->body);
} catch (RabbitmqRequeueException $queue_exception) { // 重新入队(该消息的 handler 会重新运行)
Loggers::getInstance('rabbitmq_task')->warning("rabbitmq task requeue: [queue_name {$queue_name}] {$msg->body}");
$channel->basic_nack($msg->delivery_info['delivery_tag'], false, true); // 发送信号提醒mq该消息不能被删除,且重新入队列
return;
} catch (Throwable $e) {
$exceptionInfoStr = "{$e->getFile()} {$e->getLine()} {$e->getMessage()}";
Loggers::getInstance('rabbitmq_task')->warning("rabbitmq task catch error: [queue_name {$queue_name}] {$msg->body}, exception: {$exceptionInfoStr}");
}
// 一般只有一直在频繁处理消息、需要频繁创建数据库连接的队列才需要考虑设置 db_pconnect 为 true,
// 这样可以在多次消息处理时不重新创建连接
//
// 但类似导出数据之类的消息较少、空闲时间较多的队列,每条消息被消费完成后,就可以关闭数据库和缓存连接,
// 处理下一条消息时会重新创建连接,这样可以防止消费者进程一直占用连接,导致连接数过高
if (!$db_pconnect) {
DBProxy::closeConnects("");
RedisProxy::closeConnects("");
}
$channel->basic_ack($msg->delivery_info['delivery_tag']); // 发送信号提醒mq可删除该信息
};
$channel->basic_qos(null, 1, null); // 设置一次只从queue取一条信息,在该信息处理完(消费者没有发送ack给mq),queue将不会推送信息给该消费者
// no_ack:false 表示该队列的信息必须接收到消费者信号才能被删除
// 消费者从queue拿到信息之后,该信息不会从内存中删除,需要消费者处理完之后发送信号通知mq去删除消息(如果没此通知,queue会不断积累旧的信息不会删除)
// 超时队列:推送message到消息队列,但不主动去该队列获取message,等到ttl超时,自动进入绑定的死信队列,在死信队列处理业务
if (empty($timeout_queue_name)) {
$channel->basic_consume($queue_name, '', false, false, false, false, $consume);
} else {
$channel->basic_consume($timeout_queue_name, '', false, false, false, false, $consume);
}
while (count($channel->callbacks)) {
$channel->wait();
}
$channel->close();
$connection->close();
exit;
}
生产者
/**
* AMQP-0-9-1 协议支持 事务和publisher confirms机制,都是能够确认消息能够到达队列,但二者只能二选一;事务可保证消息一定被确认,但事务机制效率为正常的1/250,且如果是镜像队列,需要全部入队才有回调返回
* 以下采用 publisher confirms 机制确认生产者消息是否入队
* 确认顺序: broker -> 交换机 -> 队列
*
* @param $queue_index
* @param $param
* @param string $obj_id
* @return bool 返回true表示入队成功
*/
public static function publish($queue_index, $param, $obj_id = '')
{
if (!is_array($param)) {
$param = json_decode($param, 1);
}
$param['_add_time'] = time();
// 传入上下文 LogId
$param['context_log_id'] = Loggers::getInstance("rabbitmq_task")->getLogId();
if (is_array($param)) {
$param = json_encode($param);
}
list($servers, $queue_name, $timeout_queue_name, $exchange_name, $callback, $consumers, $trace, $queue_timeout) = static::getQueueConfig($queue_index);
$server = static::selectServer($servers, $obj_id);
$host = RabbitmqConfig::$arrServers[$server]['host'];
$port = RabbitmqConfig::$arrServers[$server]['port'];
$user = RabbitmqConfig::$arrServers[$server]['user'];
$pass = RabbitmqConfig::$arrServers[$server]['password'];
try {
// 同时持久化`交换机`和`消息`,可以大概率保证mq重启或服务器宕机之后,消息不会丢失(如果mq重启或者服务器宕机前没能即时将新的消息持久化,也会造成丢失消息的情况)
$connection = new AMQPStreamConnection($host, $port, $user, $pass);
$channel = $connection->channel();
$channel->exchange_declare($exchange_name, self::$exchanger_type, self::$exchanger_passive, self::$exchanger_durable, self::$exchanger_auto_delete);
// 开启 publisher confirm 模式
$channel->confirm_select();
// 交换机路由不到正确的队列,会触发该回调
$failure = function ($reply_code, $reply_text, $exchange, $routing_key) use ($queue_name, $param, $obj_id, &$status) {
$status = false;
$reply = compact("reply_code", "reply_text", "exchange", "routing_key");
$log = "publish exception - {$queue_name} not found, exchange={$exchange},param={$param},obj_id={$obj_id},reply:" . json_encode($reply);
Loggers::getInstance("rabbitmq_task")->warning($log);
return;
};
$channel->set_return_listener($failure); // basic.return回调
$nackCallback = function ($callback) use ($queue_name, $param, $obj_id, &$status) {
$status = false;
$log = "publish exception - {$queue_name} server nack, param:" . json_encode($callback);
\Loggers::getInstance("rabbitmq_task")->warning($log);
return;
};
$channel->set_nack_handler($nackCallback);
$attributes['delivery_mode'] = AMQPMessage::DELIVERY_MODE_PERSISTENT; // 声明消息持久化
$msg = new AMQPMessage($param, $attributes);
$try = 10; // 重试N次
while ($try-- >= 0) {
$status = true; // 推送消息状态标志
$channel->basic_publish($msg, $exchange_name, '', self::$message_mandatory); // mandatory属性:如果交换机无法根据路由找到队列,为true会将消息以basic.return信号返回生产者,为false直接丢弃信息
$channel->wait_for_pending_acks_returns(); // 主要监听 basic.return 事件信号
if ($status) {
break;
}
/**
* @description 交换机路由查找不到队列,考虑是队列异常,重新定义队列
*/
if (empty($timeout_queue_name)) {
$channel->queue_declare($queue_name, self::$queue_passive, self::$queue_durable, false, self::$queue_auto_delete);
$channel->queue_bind($queue_name, $exchange_name);
} else if ($queue_timeout != -1) { // 如果有 dead queue,且 timeout 的定义不为空
// 定义超时删除并自动进入死信(超时)队列的消息属性
$route_key = $queue_name;
$queue_args = new AMQPTable([
'x-message-ttl' => $queue_timeout,
'x-dead-letter-exchange' => self::$dead_exchanger_name,
'x-dead-letter-routing-key' => $route_key
]);
$channel->queue_declare($queue_name, self::$queue_passive, self::$queue_durable, false, self::$queue_auto_delete, false, $queue_args);
$channel->queue_declare($timeout_queue_name, self::$queue_passive, self::$queue_durable, false, self::$queue_auto_delete); // 声明一个延时队列
$channel->queue_bind($queue_name, $exchange_name);
$channel->queue_bind($timeout_queue_name, self::$dead_exchanger_name, $route_key); // 绑定死信(超时)队列的路径
}
}
} catch (AMQPProtocolChannelException $exception) { // broker 无法找到交换机,直接抛出异常
$_exception = [
"message" => $exception->getMessage(),
"code" => $exception->getCode(),
"file" => $exception->getFile(),
"line" => $exception->getLine(),
"trace" => $exception->getTraceAsString()
];
$status = false;
Loggers::getInstance("rabbitmq_task")->warning("publish exception - {$exchange_name} not found, queue_index={$queue_index},param={$param},obj_id={$obj_id},exception:" . json_encode($_exception));
} catch (Throwable $e) {
$_exception = [
"message" => $e->getMessage(),
"code" => $e->getCode(),
"file" => $e->getFile(),
"line" => $e->getLine(),
"trace" => $e->getTraceAsString()
];
$status = false;
Loggers::getInstance("rabbitmq_task")->warning("publish exception, queue_index={$queue_index},param={$param},obj_id={$obj_id},exception:" . json_encode($_exception));
} finally {
$connection->close();
}
if ($trace) {
Loggers::getInstance("rabbitmq_task")->notice("add rabbitmq task: queue_name={$queue_name} queue_index={$queue_index}, param={$param}, host={$host}, status: {$status}");
}
return $status;
}
常见问题
问题1:队列被阻塞blocking,
- 内存查出预警,解决方法:加内存,或者提高报警阀值
- 磁盘空间预警,解决方法:加磁盘空间,或者提高报警阀值
参考
https://www.rabbitmq.com/documentation.html
https://my.oschina.net/chinaliuhan/blog/3221789