消息通過 ACK 確認是否被正確接收,每個 Message 都要被確認(acknowledged),可以手動去 ACK 或自動 ACK
自動確認會在消息發送給消費者後立即確認,但存在丟失消息的可能,如果消費端消費邏輯拋出異常,也就是消費端沒有處理成功這條消息,那麼就相當於丟失了消息。
如果消息已經被處理,但後續代碼拋出異常,使用 Spring 進行管理的話消費端業務邏輯會進行回滾,這也同樣造成了實際意義的消息丟失。
如果手動確認則當消費者調用 ack、nack、reject 幾種方法進行確認,手動確認可以在業務失敗後進行一些操作,如果消息未被 ACK 則會發送到下一個消費者。
如果某個服務忘記 ACK 了,則 RabbitMQ 不會再發送數據給它,因爲 RabbitMQ 認爲該服務的處理能力有限
ACK 機制還可以起到限流作用,比如在接收到某條消息時休眠幾秒鐘。
下面我們代碼演示:
配置類:
package com.lwl.rabbitmq.config;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
public class RabbitConfig {
@Value("${spring.rabbitmq.host}")
private String addresses;
@Value("${spring.rabbitmq.port}")
private String port;
@Value("${spring.rabbitmq.username}")
private String username;
@Value("${spring.rabbitmq.password}")
private String password;
@Value("${spring.rabbitmq.virtual-host}")
private String virtualHost;
@Value("${spring.rabbitmq.publisher-confirms}")
private boolean publisherConfirms;
@Value("${spring.rabbitmq.publisher-returns}")
private boolean publisherReturns;
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setAddresses(addresses+":"+port);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost(virtualHost);
/** 如果要進行消息回調,則這裏必須要設置爲true */
connectionFactory.setPublisherConfirms(publisherConfirms);
connectionFactory.setPublisherReturns(publisherReturns);
return connectionFactory;
}
@Bean
/** 因爲要設置回調類,所以應是prototype類型,如果是singleton類型,則回調類爲最後一次設置 */
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public RabbitTemplate rabbitTemplatenew() {
RabbitTemplate template = new RabbitTemplate(connectionFactory());
return template;
}
}
package com.lwl.rabbitmq.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.lwl.rabbitmq.constant.Constants;
/**
* 發送消息 配置發送消息的隊列queue
* @author lwl
* @create 2018年8月10日 下午2:37:38
* @version 1.0
*/
@Configuration
public class SendMessageConfig {
@Bean
public Queue topicQueue() {
return new Queue(Constants.TOPIC_QUEUE);
}
@Bean
TopicExchange exchange() {
return new TopicExchange(Constants.TOPIC_NAME);
}
/**
* 使用主題交換機,
* 將隊列Constants.TOPIC_QUEUE與exchange綁定,binding_key爲topic.queue.key,就是完全匹配
* @param topicQueue
* @param exchange
* @return
* @author lwl
* @create 2019年6月14日 上午10:51:21
*/
@Bean
Binding bindingExchangeMessage(Queue topicQueue, TopicExchange exchange) {
return BindingBuilder.bind(topicQueue).to(exchange).with(Constants.ROUTING_KEY);
}
}
# 配置rabbitmq
spring:
rabbitmq:
host: 192.168.3.66
port: 5672
username: lwl
password: 123456
publisher-confirms: true
publisher-returns: true
virtual-host: /
listener:
simple:
acknowledge-mode: manual
生產者:
package com.lwl.rabbitmq.producer;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import com.lwl.rabbitmq.constant.Constants;
/**
* 生成者
* @author lwl
* @create 2019年6月14日 上午10:56:41
* @version 1.0
*/
@Component
public class AckProducer implements RabbitTemplate.ConfirmCallback , RabbitTemplate.ReturnCallback{
@Autowired
private RabbitTemplate rabbitTemplatenew;
/**
* 使用主題交換機
* @param message
* @author lwl
* @create 2019年6月14日 上午10:54:54
*/
public void send(Object message){
rabbitTemplatenew.setConfirmCallback(this);
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
System.out.println();
System.out.println("callbackSender UUID: " + correlationData.getId());
System.out.println();
this.rabbitTemplatenew.convertAndSend(Constants.TOPIC_NAME,Constants.ROUTING_KEY, message, correlationData);
}
/**
*
* 通過實現 ConfirmCallback 接口,消息發送到 Broker 後觸發回調,確認消息是否到達 Broker 服務器,也就是隻確認是否正確到達 Exchange 中
* @param correlationData
* @param ack
* @param cause
* @author lwl
* @create 2019年6月17日 上午9:46:59
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
//
System.out.println();
System.out.println();
System.out.println("callbakck confirm: " + correlationData.getId());
System.out.println();
System.out.println();
}
/**
* 通過實現 ReturnCallback 接口,啓動消息失敗返回,比如路由不到隊列時觸發回調
* @param message
* @param replyCode
* @param replyText
* @param exchange
* @param routingKey
* @author lwl
* @create 2019年6月17日 上午10:01:47
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("消息主體 message : "+message);
System.out.println("消息主體 message : "+replyCode);
System.out.println("描述:"+replyText);
System.out.println("消息使用的交換器 exchange : "+exchange);
System.out.println("消息使用的路由鍵 routing : "+routingKey);
}
}
消費者:
package com.lwl.rabbitmq.consumer;
import java.io.IOException;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
import org.springframework.amqp.support.AmqpHeaders;
import com.lwl.rabbitmq.constant.Constants;
import com.rabbitmq.client.Channel;
/**
* 消費者
* @author lwl
* @create 2019年6月14日 上午10:57:11
* @version 1.0
*/
@Component
@RabbitListener(queues = Constants.TOPIC_QUEUE)
public class TopicAckConsumer {
@RabbitHandler
public void processMessage2(String message,Channel channel,@Header(AmqpHeaders.DELIVERY_TAG) long tag) {
System.out.println("-----------------------客戶端 1 收到數據 -----------------------");
System.out.println(message);
System.out.println();
try {
/**
* 需要注意的 basicAck 方法需要傳遞兩個參數
deliveryTag(唯一標識 ID):當一個消費者向 RabbitMQ 註冊後,會建立起一個 Channel ,
RabbitMQ 會用 basic.deliver 方法向消費者推送消息,這個方法攜帶了一個 delivery tag,
它代表了 RabbitMQ 向該 Channel 投遞的這條消息的唯一標識 ID,是一個單調遞增的正整數,delivery tag 的範圍僅限於 Channel
multiple:爲了減少網絡流量,手動確認可以被批處理,當該參數爲 true 時,則可以一次性確認 delivery_tag 小於等於傳入值的所有消息
*/
channel.basicAck(tag,false); // 確認消息
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.lwl.rabbitmq.consumer;
import java.io.IOException;
import java.util.Map;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import org.springframework.amqp.support.AmqpHeaders;
import com.lwl.rabbitmq.constant.Constants;
import com.rabbitmq.client.Channel;
/**
* 消費者
* @author lwl
* @create 2019年6月14日 上午10:57:11
* @version 1.0
*/
@Component
@RabbitListener(queues = Constants.TOPIC_QUEUE)
public class NackTopicAckConsumer {
/**
* 消費者獲取消息時檢查到頭部包含 error 則 nack 消息
* @param message
* @param channel
* @param map
* @author lwl
* @create 2019年6月17日 上午10:52:54
*/
@RabbitHandler
public void processMessage2(String message, Channel channel,@Headers Map<String,Object> map) {
System.out.println("-----------------------客戶端 nack 收到數據 -----------------------");
System.out.println(message);
if (map.get("error")!= null){
System.out.println("錯誤的消息");
try {
//此時控制檯重複打印,說明該消息被 nack 後一直重新入隊列然後一直重新消費
channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true); //否認消息
//也可以拒絕該消息,消息會被丟棄,不會重回隊列
// channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //拒絕消息
} catch (IOException e) {
e.printStackTrace();
}
}
try {
channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //確認消息
// channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true); //否認消息
// channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //拒絕消息
} catch (IOException e) {
e.printStackTrace();
}
}
}
測試用例:
/**
* 把(NackTopicAckConsumer 和 TopicAckConsumer ) 2個消費者註釋掉,保證數據發送到隊列中
* @author lwl
* @create 2019年6月29日 上午9:47:39
*/
@Test
public void send() {
String message = " message with ack C2 ";
ackProducer.send(message );
}
我們看到消息隊列中有2條數據,那麼我們先開啓一個消費者NackTopicAckConsumer, 但是我們將確認回覆註釋掉,看看運行結果
package com.lwl.rabbitmq.consumer;
import java.io.IOException;
import java.util.Map;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import org.springframework.amqp.support.AmqpHeaders;
import com.lwl.rabbitmq.constant.Constants;
import com.rabbitmq.client.Channel;
/**
* 消費者
* @author lwl
* @create 2019年6月14日 上午10:57:11
* @version 1.0
*/
@Component
@RabbitListener(queues = Constants.TOPIC_QUEUE)
public class NackTopicAckConsumer {
/**
* 消費者獲取消息時檢查到頭部包含 error 則 nack 消息
* @param message
* @param channel
* @param map
* @author lwl
* @create 2019年6月17日 上午10:52:54
*/
@RabbitHandler
public void processMessage2(String message, Channel channel,@Headers Map<String,Object> map) {
System.out.println("-----------------------客戶端 nack 收到數據 -----------------------");
System.out.println(message);
if (map.get("error")!= null){
System.out.println("錯誤的消息");
try {
//此時控制檯重複打印,說明該消息被 nack 後一直重新入隊列然後一直重新消費
channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true); //否認消息
//也可以拒絕該消息,消息會被丟棄,不會重回隊列
// channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //拒絕消息
} catch (IOException e) {
e.printStackTrace();
}
}
// try {
// channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //確認消息
//
//// channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true); //否認消息
//
//// channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //拒絕消息
//
// } catch (IOException e) {
// e.printStackTrace();
// }
}
}
/**
* 開啓消費者 NackTopicAckConsumer , 但是我們將確認回覆註釋掉,看看運行結果
* @author lwl
* @create 2019年6月29日 上午9:50:26
*/
@Test
public void send2() {
}
-----------------------客戶端 nack 收到數據 -----------------------
message with ack C2
-----------------------客戶端 nack 收到數據 -----------------------
message with ack C3
我們看到運行結果是消費隊列中的2個消息,但是真的是這樣子嘛?
看隊列中的數據還是在的,所以雖然他消費了消息,但是並沒有給確認信息,隊列的中數據都沒有被刪除,那麼我們在把確認信息都打開看看。
/**
* 開啓消費者 NackTopicAckConsumer , 我們將確認回覆註釋掉去掉,看看運行結果
* @author lwl
* @create 2019年6月29日 上午9:50:26
*/
@Test
public void send3() {
}
package com.lwl.rabbitmq.consumer;
import java.io.IOException;
import java.util.Map;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;
import org.springframework.amqp.support.AmqpHeaders;
import com.lwl.rabbitmq.constant.Constants;
import com.rabbitmq.client.Channel;
/**
* 消費者
* @author lwl
* @create 2019年6月14日 上午10:57:11
* @version 1.0
*/
@Component
@RabbitListener(queues = Constants.TOPIC_QUEUE)
public class NackTopicAckConsumer {
/**
* 消費者獲取消息時檢查到頭部包含 error 則 nack 消息
* @param message
* @param channel
* @param map
* @author lwl
* @create 2019年6月17日 上午10:52:54
*/
@RabbitHandler
public void processMessage2(String message, Channel channel,@Headers Map<String,Object> map) {
System.out.println("-----------------------客戶端 nack 收到數據 -----------------------");
System.out.println(message);
if (map.get("error")!= null){
System.out.println("錯誤的消息");
try {
//此時控制檯重複打印,說明該消息被 nack 後一直重新入隊列然後一直重新消費
channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true); //否認消息
//也可以拒絕該消息,消息會被丟棄,不會重回隊列
// channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //拒絕消息
} catch (IOException e) {
e.printStackTrace();
}
}
try {
channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //確認消息
// channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true); //否認消息
// channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //拒絕消息
} catch (IOException e) {
e.printStackTrace();
}
}
}
運行結果:
-----------------------客戶端 nack 收到數據 -----------------------
message with ack C2
-----------------------客戶端 nack 收到數據 -----------------------
message with ack C3
看,消息都被消費掉了,而且也從隊列中刪除了。
那麼剩下的幾種情況,你們可以自己嘗試看看。
channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //確認消息
// channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true); //否認消息
// channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //拒絕消息