消费端限流
什么是消费端限流?
假设一个场景,首先,我们rabbitmq服务器有上万条未处理的消息,我们随便打开一个消费者客户端,会出现下边的情况:
- 巨量的消息瞬间全部推送过来,但是我们单个客户端无法同时处理这么多数据。
rabbitmq提供了一种qos(服务质量保证)功能,即在非自动确认消息的前提下,如果一定数目的消息(通过基于consumer或者channel设置qos的值)未被确认前,不进行消费新的消息
* @param prefetchSize maximum amount of content (measured in
* octets) that the server will deliver, 0 if unlimited
* @param prefetchCount maximum number of messages that the server
* will deliver, 0 if unlimited
* @param global true if the settings should be applied to the
* entire channel rather than each consumer
void basicQos(int prefetchSize, int prefetchCount, boolean global)
perfetchSize和global这两项,rabbitmq没有实现,暂且不研究
perfetch_count在no_ask=false的时候不生效,即在自动应答的情况下这两个值是不生效的。
创建生产者
public static final String EXCHANGE_NAME = "test_exchange_qos";
public static final String ROUTING_KEY = "routingkey.qos";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionsUtils.getConnection();
// 创建信道
Channel channel = connection.createChannel();
// 发送消息
String message = "test message for qos";
for (int i = 0; i < 5; i++) {
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY,true, null, message.getBytes());
}
System.out.println("发送成功:" + message);
}
创建消费者
public static final String EXCHANGE_NAME = "test_exchange_qos";
public static final String EXCHANGE_TYPE = "direct";
public static final String ROUTING_KEY = "routingkey.qos";
public static final String QUEUE_NAME = "test_queue_qos";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionsUtils.getConnection();
// 创建信道
final Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, EXCHANGE_TYPE, true, false, false ,null);
// 声明队列
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 绑定
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
// 限流方式
/**
* prefetchCount:1 处理完1条之后继续处理
*/
channel.basicQos(0,1,false);
// 创建消费者进行监听获取消息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("接收消息message: " + new String(body));
// 主动回送应答
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume(QUEUE_NAME, false, consumer);
}
我们先注释掉channel.basicAck(envelope.getDeliveryTag(), false);关闭发送回调。看一下效果
消费者只接收到了一条消息,没有发送回调,所以不会给这个消费者再次投递,这里可以查看控制台
我们将注释打来,再次启动
这里每接收到一条消息后立即主动发送回调告知,然后获取下一条消息,通过管控台可以发现,已经没有待消费的消息了
消费端ACK和NACK
- 消费端进行消费的时候,如果由于业务异常,我们可以进行日志的记录,然后进行补偿
- 如果由于服务器宕机等严重问题,那么我们就需要手工进行ack,保障消费端消费成功。
重回队列
消费端重复你队列是为了对没有处理成功的消息,吧消息重新递给broker
一般实际应用中,都会关闭重回队列,设置为false。我们看下设置为true的情况
生产端
public static final String EXCHANGE_NAME = "test_exchange_ack";
public static final String ROUTING_KEY = "routingkey.ack";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionsUtils.getConnection();
// 创建信道
Channel channel = connection.createChannel();
// 发送消息
for (int i = 0; i < 5; i++) {
String message = "test message for ack " + i;
Map<String,Object> header = new HashMap<String, Object>();
header.put("num", i);
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.deliveryMode(2)
.headers(header)
.build();
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY,true, properties, message.getBytes());
System.out.println("发送成功:" + message+" "+ i);
}
}
消费端
public static final String EXCHANGE_NAME = "test_exchange_ack";
public static final String EXCHANGE_TYPE = "direct";
public static final String ROUTING_KEY = "routingkey.ack";
public static final String QUEUE_NAME = "test_queue_ack";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接
Connection connection = ConnectionsUtils.getConnection();
// 创建信道
final Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, EXCHANGE_TYPE, true, false, false ,null);
// 声明队列
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 绑定
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
// 限流方式 prefetchCount:1 处理完1条之后继续处理
channel.basicQos(0,1,false);
// 创建消费者进行监听获取消息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
Map<String,Object> map = properties.getHeaders();
if((Integer)map.get("num") == 2){
// requeue:是否重回队列,添加到队列的原始位置
System.out.println("消费失败"+map.get("num"));
channel.basicNack(envelope.getDeliveryTag(),false, true);
}else{
System.out.println("消费成功"+map.get("num"));
// 主动回送应答
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
// 手工签收,一定要设置autoack为false
channel.basicConsume(QUEUE_NAME, false, consumer);
}
我们加了延时2秒,更好的看下效果
根据重新排队的消息在队列中的位置以及具有活动使用者的通道使用的预取值,可以立即准备好重新发送。这意味着,如果所有消费者都因为暂时性的情况而无法处理交付而重新排队,他们将创建一个重新排队/重新交付循环。这样的循环在网络带宽和CPU资源方面可能很昂贵。使用者实现可以跟踪重新传递的次数并永久拒绝消息(丢弃它们)或在延迟后安排重新排队。
我们在网上或者其它书籍看到的大多都是说,重回队列,会回到队列的尾部,等待后续消息被消费后,才会轮到重回队列的那个消息被继续投递消费,结果应该是
消费成功0 >消费成功1 >消费成功2>消费失败3 >消费成功4> 消费失败3> 消费失败3> 消费失败3>…
目前我们使用的是3.7.8版本的rabbitmq了。因此版本升级也带来了一些细节上的变动。查阅官网我们发现这样一段话: When a message is requeued, it will be placed to its original position in its queue, if possible. If not (due to concurrent deliveries and acknowledgements from other consumers when multiple consumers share a queue), the message will be requeued to a position closer to queue head. 当消息重新排队时,如果可能的话,它将被放置到队列中的原始位置。如果没有(由于多个消费者共享队列时来自其他消费者的并发传递和确认),则消息将被重新排队到更靠近队列头的位置。
TTL队列、消息
- TTL是TIme To Live 的缩写,生存时间
- RabbtiMQ 支持消息的过期时间,在消息发送时可以进行制定过期时间
- RabbitMQ 支持队列的过期时间,从消息入队开始计算,只要超过了队列的超时时间配置,那么消息会被自动删除