目录
一. 持久化
二. ACK
三. 事务处理:
Confirm模式
概述
producer端confirm模式的实现原理
开启confirm模式的方法
RabbitMQ支持消息的持久化,也就是数据写在磁盘上。
(1)exchange持久化,在声明时指定durable => 1
(2)queue持久化,在声明时指定durable => 1
(3)消息持久化,在投递时指定delivery_mode => 2(1是非持久化)
注意:如果消息持久化,queue不持久化,重启服务消息依然会丢失,但是exchange不持久化就不会有影响,所以在持久化消息的同时一定要持久化queue;
上面持久化保证了队列里的数据在服务器出现宕机的情况重启不会丢失,但是当消费者去除队列中的消息处理过程中失败了,这个消息出现服务器异常导致失败了,这也会导致数据丢失,ACK机制就是为了解决这类问题的,
在RabbitMQ中,消息确认有两种模式:
1. 自动模式,我们无需任何操作,在消息被消费者领取后,就会自动确认,消息也会被从队列删除。
2. 手动模式,消息被消费后,我们需要调用RabbitMQ提供的API来实现消息确认。
代码:
/**
* 测试Ack消费
*
* @throws \ErrorException
* @date 2019-06-18
*/
public function actionConsumeMessageAck()
{
$channel = RabbitmqBase::createChannel();
$callback = function ($msg) {
echo " [x] Received ", $msg->body, "\n";
var_dump($msg->delivery_info['delivery_tag']);//队列中每个消息的标记
sleep(1);
//处理消息之后确认(一定要确定开启了消息确认机制才能用这个)
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
};
//可告知RabbitMQ只有在consumer处理并确认了上一个message后才分配新的message给他,否则上一个没处理完会一直卡在这里,这个根据业务场景配置
//basic_qos($prefetch_size,$prefetch_count, //最重要的参数,未确认的消息同时存在的个数;(也就是未ack的消息数,我们可以以此来作为记录失败数据的个数;)$a_global)
$channel->basic_qos(null, 1, null);//取数据时按优先级取,但是取到的数据消费时按顺序消费,没有优先级概念
//no_ack为false(默认)开启确认机制
$channel->basic_consume('AQ1', '', false, false, false, false, $callback);
while (count($channel->callbacks)) {
$channel->wait();//这个在没有消息会等待,有消息就会自动运行
}
$channel->close();
RabbitmqBase::closeConnection();
}
看上面一段代码当basic_consume的第四个参数设置为fasle时需要手动确认,只有在确认完后队列才会将该消息删除,如果时自动模式,只要数据被去除,队列就会删除该数据,
注意:如果忘了ACK,后果会很严重,有发起ack就会导致服务器上的消息一直堆积.服务器会发送新的消息.同时会记录当前的这个链接有哪些消息一直还没回复.(服务器认为你会回复,一直等待)。如果消费者进程停止掉重启..就会重新接收所有消息!,然后Rabbitmq会占用越来越多的内存,由于Rabbitmq会长时间运行,这就会导致“内存泄漏”;
将消息设为持久化并不能完全保证不会丢失。持久化只是告诉了RabbitMq要把消息存到硬盘,但从RabbitMq收到消息到保存之间还是有一个很小的间隔时间。因为RabbitMq并不是所有的消息都使用同步IO—它有可能只是保存到缓存中,并不一定会写到硬盘中。并不能保证真正的持久化,但已经足够应付我们的简单工作队列。如果你一定要保证持久化,我们需要改写代码来支持事务(transaction)。
生产者模式事务:
事务会确定消息存入硬盘后再提交;
消费者模式使用事务
假设消费者模式中使用了事务,并且在消息确认之后进行了事务回滚,那么RabbitMQ会产生什么样的变化?
结果分为两种情况:
事务代码:
/**
* 生产者事务
*
* @date 2019-06-18
*/
public function actionProducerTransaction()
{
$channel = RabbitmqBase::createChannel();
$channel->exchange_declare('exchange_transaction', 'direct', false, true,
false);//这个是申明交换器,如果没有申明就给默认队列的这个交换器,而且发送的类型默认是direct)
$channel->queue_declare('TQ1', false, true, false, false, false);
$channel->queue_bind('TQ1', 'exchange_transaction');
$channel->tx_select();
try {
// 发送消息
$newMsg = new AMQPMessage('test_transaction',
[
'delivery_mode' => 1,
'message_id' => uniqid(),
]);
$channel->basic_publish($newMsg, 'exchange_transaction'); //这里的queue是消息名称
$channel->tx_commit();
} catch (Exception $e) {
echo sprintf("失败%s。。。\n", $e->getMessage());
$channel->tx_rollback();
} finally {
$channel->close();
RabbitmqBase::closeConnection();
}
}
/**
* 消费者事务:
*/
public function actionConsumeTransaction()
{
$channel = RabbitmqBase::createChannel();
$channel->tx_select();
try {
$channel->basic_qos(null, 1, null);
$msg = $channel->basic_get('AQ1', false);
if ($msg) {
echo " [x] Received ", $msg->body, "\n";
//处理消息之后确认(一定要却懂开启了消息确认机制才能用这个)
$channel->basic_ack($msg->delivery_info['delivery_tag']);
} else {
throw new UserException('未收到消息');
}
$channel->tx_commit();
} catch (Exception $e) {
$channel->tx_rollback();
} finally {
$channel->close();
RabbitmqBase::closeConnection();
}
}
上面我们介绍了RabbitMQ可能会遇到的一个问题,即生成者不知道消息是否真正到达broker,随后通过AMQP协议层面为我们提供了事务机制解决了这个问题,但是采用事务机制实现会降低RabbitMQ的消息吞吐量,那么有没有更加高效的解决方式呢?答案是采用Confirm模式。
生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会将消息写入磁盘之后发出,broker回传给生产者的确认消息中deliver-tag域包含了确认消息的序列号,此外broker也可以设置basic.ack的multiple域,表示到这个序列号之前的所有消息都已经得到了处理。
confirm模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息。
在channel 被设置成 confirm 模式之后,所有被 publish 的后续消息都将被 confirm(即 ack) 或者被nack一次。但是没有对消息被 confirm 的快慢做任何保证,并且同一条消息不会既被 confirm又被nack 。
生产者通过调用channel的confirmSelect方法将channel设置为confirm模式,如果没有设置no-wait标志的话,broker会返回confirm.select-ok表示同意发送者将当前channel信道设置为confirm模式(从目前RabbitMQ最新版本3.6来看,如果调用了channel.confirmSelect方法,默认情况下是直接将no-wait设置成false的,也就是默认情况下broker是必须回传confirm.select-ok的)。
confirm代码:
/**
* producer端confirm模式
*
* @date 2019-06-18
*/
public function actionProducerConfirm()
{
$channel = RabbitmqBase::createChannel();
$channel->exchange_declare('exchange_confirm', 'direct', false, true,
false);//这个是申明交换器,如果没有申明就给默认队列的这个交换器,而且发送的类型默认是direct)
$channel->queue_declare('CQ1', false, true, false, false, false);
$channel->queue_bind('CQ1', 'exchange_confirm');
try {
//设置异步回调消息确认 (生产者 防止信息丢失)
$channel->set_ack_handler(
function (AMQPMessage $message) {
echo "Message acked with content " . $message->body . PHP_EOL;
sleep(1);
}
);
//消息失败后处理方式
$channel->set_nack_handler(
function (AMQPMessage $message) {
throw new UserException($message->body);
}
);
//选择为 confirm 模式(此模式不可以和事务模式 兼容)
$channel->confirm_select();
// 发送消息
$newMsg = new AMQPMessage('hello test_confirm',
[
'delivery_mode' => 2,
'message_id' => uniqid(),
]);
$channel->basic_publish($newMsg, 'exchange_confirm'); //这里的queue是消息名称
//阻塞等待消息确认 (生产者 防止信息丢失)
$channel->wait_for_pending_acks();
} catch (Exception $e) {
echo sprintf("失败%s。。。\n", $e->getMessage());
} finally {
$channel->close();
RabbitmqBase::closeConnection();
}
}
public function actionProducerConfirms()
{
$channel = RabbitmqBase::createChannel();
$channel->exchange_declare('exchange_confirm', 'direct', false, true,
false);//这个是申明交换器,如果没有申明就给默认队列的这个交换器,而且发送的类型默认是direct)
$channel->queue_declare('CQ1', false, true, false, false, false);
$channel->queue_bind('CQ1', 'exchange_confirm');
try {
//设置异步回调消息确认 (生产者 防止信息丢失)
$channel->set_ack_handler(
function (AMQPMessage $message) {
echo "Message acked with content " . $message->body . PHP_EOL;
sleep(1);
}
);
//消息失败后处理方式
$channel->set_nack_handler(
function (AMQPMessage $message) {
throw new UserException($message->body);
}
);
//选择为 confirm 模式(此模式不可以和事务模式 兼容)
$channel->confirm_select();
// 发送消息
$newMsg = new AMQPMessage('hello test_confirm',
[
'delivery_mode' => 2,
'message_id' => uniqid(),
]);
$channel->basic_publish($newMsg, 'exchange_confirm'); //这里的queue是消息名称
//阻塞等待消息确认 (生产者 防止信息丢失)
$channel->wait_for_pending_acks_returns();
} catch (Exception $e) {
echo sprintf("失败%s。。。\n", $e->getMessage());
} finally {
$channel->close();
RabbitmqBase::closeConnection();
}
}