使用Spring Boot整合RabbitMQ來演示hello word
配置文件:application.properties
spring.rabbitmq.host=119.65.182.47
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
RabbitConfiguration配置類:主要配置三類bean
- ConnectionFactory和RabbitTemplate,
- 相關的隊列Queue,交換機Exchange和綁定Binding(也可以通過管理界面自己創建或者通過在@RabbitListener中創建)
- 異步處理消費消息的一個監聽容器SimpleMessageListenerContainer
package com.niwodai.mq.rabbit.config;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.DirectExchangeParser;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.annotation.Qualifier;
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.Configuration;
import org.springframework.context.annotation.Scope;
/**
* @author :
* @description:
* @date : 2019年08月10日
* @since: 1.0.0
*/
@Configuration
@Slf4j
public class RabbitConfiguration {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Value("${spring.rabbitmq.host}")
private String host;
@Value("${spring.rabbitmq.port}")
private int port;
@Value("${spring.rabbitmq.username}")
private String username;
@Value("${spring.rabbitmq.password}")
private String password;
public static final String EXCHANGE_A = "my-mq-exchange_A";
public static final String EXCHANGE_B = "my-mq-exchange_B";
public static final String EXCHANGE_C = "my-mq-exchange_C";
public static final String QUEUE_A = "QUEUE_A";
public static final String QUEUE_B = "QUEUE_B";
public static final String QUEUE_C = "QUEUE_C";
public static final String ROUTINGKEY_A = "spring-boot-routingKey_A";
public static final String ROUTINGKEY_B = "spring-boot-routingKey_B";
public static final String ROUTINGKEY_C = "spring-boot-routingKey_C";
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host,port);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost("/");
connectionFactory.setPublisherConfirms(true);
return connectionFactory;
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
//必須是prototype類型
public RabbitTemplate rabbitTemplate() {
RabbitTemplate template = new RabbitTemplate(connectionFactory());
return template;
}
/**
* 針對消費者配置
* 1. 設置交換機類型
* 2. 將隊列綁定到交換機
FanoutExchange: 將消息分發到所有的綁定隊列,無routingkey的概念
HeadersExchange :通過添加屬性key-value匹配
DirectExchange:按照routingkey分發到指定隊列
TopicExchange:多關鍵字匹配
*/
@Bean
public DirectExchange defaultExchange() {
return new DirectExchange(EXCHANGE_A);
}
/**
* 獲取隊列A
* @return
*/
@Bean
public Queue queueA() {
return new Queue(QUEUE_A, true); //隊列持久
}
@Bean
public Binding binding() {
return BindingBuilder.bind(queueA()).to(defaultExchange()).with(RabbitConfiguration.ROUTINGKEY_A);
}
/**
* 隊列B
* @return
*/
@Bean
public DirectExchange defaultExchangeB() {
return new DirectExchange(EXCHANGE_B);
}
@Bean
public Queue queueB() {
return new Queue(QUEUE_B, true); //隊列持久
}
@Bean
public Binding bindingB(){
return BindingBuilder.bind(queueB()).to(defaultExchangeB()).with(RabbitConfiguration.ROUTINGKEY_B);
}
@Bean
public SimpleMessageListenerContainer messageContainer() {
//加載處理消息A的隊列
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory());
//設置接收多個隊列裏面的消息,這裏設置接收隊列A
//假如想一個消費者處理多個隊列裏面的信息可以如下設置:
//container.setQueues(queueA(),queueB(),queueC());
container.setQueues(queueA());
container.setExposeListenerChannel(true);
//設置最大的併發的消費者數量
container.setMaxConcurrentConsumers(10);
//最小的併發消費者的數量
container.setConcurrentConsumers(1);
//設置確認模式手工確認
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
container.setMessageListener(new ChannelAwareMessageListener() {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
/**通過basic.qos方法設置prefetch_count=1,這樣RabbitMQ就會使得每個Consumer在同一個時間點最多處理一個Message,
換句話說,在接收到該Consumer的ack前,它不會將新的Message分發給它 */
channel.basicQos(1);
byte[] body = message.getBody();
logger.info("接收處理隊列A當中的消息:" + new String(body));
/**爲了保證永遠不會丟失消息,RabbitMQ支持消息應答機制。
當消費者接收到消息並完成任務後會往RabbitMQ服務器發送一條確認的命令,然後RabbitMQ纔會將消息刪除。*/
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
});
return container;
}
}
消息發送者:通過實現RabbitTemplate.ConfirmCallback 實現發送方消息確認機制
package com.niwodai.mq.rabbit.producter;
import com.niwodai.mq.rabbit.config.RabbitConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.UUID;
/**
* @author :yanpeidong371
* @description:
* @date : 2019年08月10日
* @since: 1.0.0
*/
@Component
public class RabbitProducter implements RabbitTemplate.ConfirmCallback{
private final Logger logger = LoggerFactory.getLogger(this.getClass());
//由於rabbitTemplate的scope屬性設置爲ConfigurableBeanFactory.SCOPE_PROTOTYPE,所以不能自動注入
private RabbitTemplate rabbitTemplate;
/**
* 構造方法注入rabbitTemplate
*/
@Autowired
public RabbitProducter(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
rabbitTemplate.setConfirmCallback(this); //rabbitTemplate如果爲單例的話,那回調就是最後設置的內容
}
public void sendMsg(String content) {
CorrelationData correlationId = new CorrelationData(UUID.randomUUID().toString());
//把消息放入ROUTINGKEY_A對應的隊列當中去,對應的是隊列A
rabbitTemplate.convertAndSend(RabbitConfiguration.EXCHANGE_A, RabbitConfiguration.ROUTINGKEY_A, content, correlationId);
}
public void sendMsgB(String content) {
CorrelationData correlationId = new CorrelationData(UUID.randomUUID().toString());
//把消息放入ROUTINGKEY_A對應的隊列當中去,對應的是隊列A
rabbitTemplate.convertAndSend(RabbitConfiguration.EXCHANGE_B, RabbitConfiguration.ROUTINGKEY_B, content, correlationId);
}
/**
* 回調
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
logger.info(" 回調id:" + correlationData);
if (ack) {
logger.info("消息成功消費");
} else {
logger.info("消息消費失敗:" + cause);
}
}
}
消息的消費者:
實現1:在configuration中定義的異步處理消費消息的一個監聽容器SimpleMessageListenerContainer來實現消費消息
實現2:通過@RabbitListener註解標記消費者方法(有的版本需要在啓動類添加@EnableRabbit來啓用@RabbitListener )
/**
* 可以直接通過註解聲明交換器、綁定、隊列。但是如果聲明的和rabbitMq中已經存在的不一致的話
* 會報錯便於測試,我這裏都是不使用持久化,沒有消費者之後自動刪除
* {@link RabbitListener}是可以重複的。並且聲明隊列綁定的key也可以有多個.
*
* @param headers
* @param msg
*/
@RabbitListener(
bindings = @QueueBinding(
exchange = @Exchange(value = RabbitMQConstant.DEFAULT_EXCHANGE, type = ExchangeTypes.TOPIC,
durable = RabbitMQConstant.FALSE_CONSTANT, autoDelete = RabbitMQConstant.true_CONSTANT),
value = @Queue(value = RabbitMQConstant.DEFAULT_QUEUE, durable = RabbitMQConstant.FALSE_CONSTANT,
autoDelete = RabbitMQConstant.true_CONSTANT),
key = DKEY
),
//手動指明消費者的監聽容器,默認Spring爲自動生成一個SimpleMessageListenerContainer
containerFactory = "container",
//指定消費者的線程數量,一個線程會打開一個Channel,一個隊列上的消息只會被消費一次(不考慮消息重新入隊列的情況),下面的表示至少開啓5個線程,最多10個。線程的數目需要根據你的任務來決定,如果是計算密集型,線程的數目就應該少一些
concurrency = "5-10"
)
public void process(@Headers Map<String, Object> headers, @Payload ExampleEvent msg) {
log.info("basic consumer receive message:{headers = [" + headers + "], msg = [" + msg + "]}");
}
/**
* {@link Queue#ignoreDeclarationExceptions}聲明隊列會忽略錯誤不聲明隊列,這個消費者仍然是可用的
*
* @param headers
* @param msg
*/
@RabbitListener(queuesToDeclare = @Queue(value = RabbitMQConstant.DEFAULT_QUEUE, ignoreDeclarationExceptions = RabbitMQConstant.true_CONSTANT))
public void process2(@Headers Map<String, Object> headers, @Payload ExampleEvent msg) {
log.info("basic2 consumer receive message:{headers = [" + headers + "], msg = [" + msg + "]}");
}
@Headers和@Payload註解將消息和消息頭注入消費者方法
在上面也看到了@Header @Payload註解用於注入消息。這些註解有:
- @Header 注入消息頭的單個屬性
- @Payload 注入消息體到一個JavaBean中
- @Headers 注入所有消息頭到一個Map中
注意如果是com.rabbitmq.client.Channel,org.springframework.amqp.core.Message和org.springframework.messaging.Message這些類型,可以不加註解,直接可以注入。如果不是這些類型,那麼不加註解的參數將會被當做消息體。不能多於一個消息體。如下方法ExampleEvent就是默認的消息體:
public void process2(@Headers Map<String, Object> headers,ExampleEvent msg);
@RabbitListener 和 @RabbitHandler
通過上面的,我們可以看到RabbitListener不僅可以標記在方法上用來作爲消費者監聽方法消費消息,還可以用來實現隊列和交換機的申明和綁定(註解的queue和bindings不能同時指定,否則報錯,指定bindings時若 RabbitMQ 中不存在該綁定所需要的 Queue、Exchange、RouteKey 則自動創建,若存在則拋出異常)
作用1:作爲消費者監聽註解,有以下兩個使用情況:
- @RabbitListener 可以直接標註在方法上,表示該方法會被當作消費者的消息監聽方法
- @RabbitListener 也可以標註在類上使用,需配合 @RabbitHandler 註解一起使用,@RabbitListener 標註在類上表示當收到消息的時候,就交給 @RabbitHandler 的方法處理,具體使用哪個方法處理,是根據 MessageConverter 轉換後的參數類型
@Component
@RabbitListener(queues = "consumer_queue")
public class Receiver {
@RabbitHandler
public void processMessage1(String message) {
System.out.println(message);
}
@RabbitHandler
public void processMessage2(byte[] message) {
System.out.println(new String(message));
}
}
MessageListenerAdapter
消息監聽適配器(adapter),通過反射將消息處理委託給目標監聽器的處理方法,並進行靈活的消息類型轉換。允許監聽器方法對消息內容類型進行操作,完全獨立於Rabbit API。
默認情況下,傳入Rabbit消息的內容在被傳遞到目標監聽器方法之前被提取,以使目標方法對消息內容類型進行操作以String或者byte類型進行操作,而不是原始Message類型。 (消息轉換器),消息類型轉換委託給MessageConverter接口的實現類。 默認情況下,將使用SimpleMessageConverter。 (如果您不希望進行這樣的自動消息轉換,那麼請自己通過#setMessageConverter MessageConverter設置爲null)
如果目標監聽器方法返回一個非空對象(通常是消息內容類型,例如String或byte數組),它將被包裝在一個Rabbit Message 中,併發送使用來自Rabbit ReplyTo屬性或通過#setResponseRoutingKey(String)指定的routingKey的routingKey來傳送消息。(使用rabbitmq 來實現異步rpc功能時候會使用到這個屬性)。注意:發送響應消息僅在使用ChannelAwareMessageListener入口點(通常通過Spring消息監聽器容器)時可用。 用作MessageListener不支持生成響應消息。
@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames("order","pay","zhihao.miao.order");
MessageListenerAdapter adapter = new MessageListenerAdapter(new MessageHandler());
//設置處理器的消費消息的默認方法,如果沒有設置,那麼默認的處理器中的默認方式是handleMessage方法
adapter.setDefaultListenerMethod("onMessage");
Map<String, String> queueOrTagToMethodName = new HashMap<>();
queueOrTagToMethodName.put("queue_A","onorder");
queueOrTagToMethodName.put("queue_B","onpay");
queueOrTagToMethodName.put("queue_C","oninfo");
adapter.setQueueOrTagToMethodName(queueOrTagToMethodName);
container.setMessageListener(adapter);
return container;
}
Handler類MessageHandler中定義的方法也就是目標上述的目標監聽器的處理方法:
public class MessageHandler {
public void handleMessage(String message){
System.out.println("---------handleMessage-------------");
System.out.println(new String(message)+"0");
}
//通過設置setDefaultListenerMethod時候指定的方法名
public void onMessage(String message){
System.out.println("---------onMessage-------------");
System.out.println(new String(message)+"1");
}
//以下指定不同的隊列不同的處理方法名
public void onorder(String message){
System.out.println("---------onorder-------------");
System.out.println(new String(message)+"2");
}
public void onpay(String message){
System.out.println("---------onpay-------------");
System.out.println(new String(message)+"3");
}
public void oninfo(String message){
System.out.println("---------oninfo-------------");
System.out.println(new String(message)+"4");
}
}
}
總結
使用MessageListenerAdapter處理器進行消息隊列監聽處理,如果容器沒有設置setDefaultListenerMethod,則處理器中默認的處理方法名是handleMessage,如果設置了setDefaultListenerMethod,則處理器中處理消息的方法名就是setDefaultListenerMethod方法參數設置的值。也可以通過setQueueOrTagToMethodName方法爲不同的隊列設置不同的消息處理方法。(隊列名和方法名映射成的Map)
MessageListenerAdapter的作用
1.可以把一個沒有實現MessageListener和ChannelAwareMessageListener接口的類適配成一個可以處理消息的處理器
2.默認的方法名稱爲:handleMessage,可以通過setDefaultListenerMethod設置新的消息處理方法
3.MessageListenerAdapter支持不同的隊列交給不同的方法去執行。使用setQueueOrTagToMethodName方法設置,當根據queue名稱沒有找到匹配的方法的時候,就會交給默認的方法去處理。
MessageConverter
源碼如下:主要包括兩個轉換的方法
public interface MessageConverter {
Message toMessage(Object var1, MessageProperties var2) throws MessageConversionException;
//將java對象和屬性對象轉換成Message對象。
default Message toMessage(Object object, MessageProperties messageProperties, @Nullable Type genericType) throws MessageConversionException {
return this.toMessage(object, messageProperties);
}
//將消息對象轉換成java對象。
Object fromMessage(Message var1) throws MessageConversionException;
}
- 涉及網絡傳輸的應用序列化不可避免,發送端以某種規則將消息轉成 byte 數組進行發送,接收端則以約定的規則進行 byte[] 數組的解析
- RabbitMQ 的序列化是指 Message 的 body 屬性,即我們真正需要傳輸的內容,RabbitMQ 抽象出一個 MessageConvert 接口處理消息的序列化,其實現有 SimpleMessageConverter(默認)、Jackson2JsonMessageConverter 等
- 當調用了 convertAndSend 方法時會使用 MessageConvert 進行消息的序列化
- SimpleMessageConverter 對於要發送的消息體 body 爲 byte[] 時不進行處理,如果是 String 則轉成字節數組,如果是 Java 對象,則使用 jdk 序列化將消息轉成字節數組,轉出來的結果較大,含class類名,類相應方法等信息。因此性能較差
- 當使用 RabbitMQ 作爲中間件時,數據量比較大,此時就要考慮使用類似 Jackson2JsonMessageConverter 等序列化形式以此提高性能
@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames("zhihao.miao.order");
MessageListenerAdapter adapter = new MessageListenerAdapter(new MessageHandler());
//指定消息轉換器
adapter.setMessageConverter(new TestMessageConverter());
//設置處理器的消費消息的默認方法
adapter.setDefaultListenerMethod("onMessage");
container.setMessageListener(adapter);
return container;
}
消息轉換器:實現MessageConverter,消費端接收的消息就從Message類型轉換成了String類型
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.support.converter.MessageConversionException;
import org.springframework.amqp.support.converter.MessageConverter;
public class TestMessageConverter implements MessageConverter {
@Override
public Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException {
System.out.println("=======toMessage=========");
return new Message(object.toString().getBytes(),messageProperties);
}
//消息類型轉換器中fromMessage方法返回的類型就是消費端處理器接收的類型
@Override
public Object fromMessage(Message message) throws MessageConversionException {
System.out.println("=======fromMessage=========");
return new String(message.getBody());
}
}
消費者處理消息的Handler
public class MessageHandler {
public void onMessage(String message){
System.out.println("---------onMessage-------------");
System.out.println(message);
}
}
執行:先執行MessageConverter,然後執行目標監聽器的處理方法處理消息
=======fromMessage=========
---------onMessage-------------
String類型的消息
從控制檯打印我們知道了在消費者處理消息之前會進行消息類型轉換,調用TestMessageConverter的fromMessage方法,然後執行消息處理器的onMessage方法,方法參數就是String類型。
Jackson2JsonMessageConverter
當生產者服務將JSON類型的數據傳遞到對應的隊列, 而消費端處理器中接收到的數據類型也是JSON類型。消息轉換器可以直接使用了RabbitMQ自帶的Jackson2JsonMessageConverter轉換器
//發送者1:不指定消息的contentType
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(order);
rabbitTemplate.convertAndSend("","queue_A",json);
//發送者2:指定消息的contentType
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(order);
MessageProperties messageProperties = new MessageProperties();
//指明消息的contentType
messageProperties.setContentType("application/json");
Message message = new Message(json.getBytes(),messageProperties);
rabbitTemplate.send("","queue_A",message);
//發送者3:指定消息的contentType並且發送list類型的消息
List<Order> orderList = new ArrayList<>();
orderList.add(order);
orderList.add(order2);
String jsonlist = mapper.writeValueAsString(orderList);
Message message2 = new Message(jsonlist.getBytes(),messageProperties);
rabbitTemplate.send("","queue_A",message2);
//消息消費者
public class MessageHandler {
public void onMessage(byte[] message){
System.out.println("---------onMessage----byte-------------");
System.out.println(new String(message));
}
public void onMessage(String message){
System.out.println("---------onMessage---String-------------");
System.out.println(message);
}
public void onMessage(Map order){
System.out.println("---------onMessage---map-------------");
System.out.println(order.toString());
}
}
發送者1發送的消息:消費端的轉換器將其轉換成byte[]類型
發送者2發送的消息:消費端的轉換器將其轉換成Map類型
發送者3發送的消息:消費端將消息轉換成List類型的消息
總結:
- 使用Jackson2JsonMessageConverter處理器,客戶端發送JSON類型數據,但是沒有指定消息的contentType類型,那麼Jackson2JsonMessageConverter就會將消息轉換成byte[]類型的消息進行消費。
- 如果指定了contentType爲application/json,那麼消費端就會將消息轉換成Map類型的消息進行消費。
- 如果指定了contentType爲application/json,並且生產端是List類型的JSON格式,那麼消費端就會將消息轉換成List類型的消息進行消費。
生產者在發送json數據的時候,需要指定這個json是哪個對象,否則消費者收到消息之後,不知道要轉換成哪個java對象 , 指定方法: 在消息header中,增加一個_TypeId_,value就是具體的java對象(全類名),一定是消費者所在系統的java對象全稱
//普通java對象類型
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentType("application/json");
//指定消費端配置的key值就行了
messageProperties.getHeaders().put("__TypeId__","user");
Message message = new Message(json.getBytes(),messageProperties);
//Map類型
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentType("application/json");
messageProperties.getHeaders().put("__TypeId__","java.util.Map");
messageProperties.getHeaders().put("__KeyTypeId__","java.lang.String");
messageProperties.getHeaders().put("__ContentTypeId__","order");
如果一個隊列有多個消費者
如上,如果是同一個隊列多個消費類型那麼就需要針對每種類型提供一個消費方法,否則找不到匹配的方法會報錯,多個消費者輪流收到消息達到負載均衡
@Component
@RabbitListener(queues = RabbitConfig.QUEUE_A)
public class MsgReceiverC_one {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@RabbitHandler
public void process(String content) {
logger.info("處理器one接收處理隊列A當中的消息: " + content);
}
}
@Component
@RabbitListener(queues = RabbitConfig.QUEUE_A)
public class MsgReceiverC_two {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@RabbitHandler
public void process(String content) {
logger.info("處理器two接收處理隊列A當中的消息: " + content);
}
}
或者使用@RabbitHandler來標明不同的方法
@Component
@Slf4j
@RabbitListener(queues = RabbitConfig.QUEUE_A)
public class MultipartConsumer {
/**
* RabbitHandler用於有多個方法時但是參數類型不能一樣,否則會報錯
*
* @param msg
*/
@RabbitHandler
public void process(ExampleEvent msg) {
log.info("param:{msg = [" + msg + "]} info:");
}
@RabbitHandler
public void processMessage2(ExampleEvent2 msg) {
log.info("param:{msg2 = [" + msg + "]} info:");
}
/**
* 下面的多個消費者,消費的類型不一樣沒事,不會被調用,但是如果缺了相應消息的處理Handler則會報錯
*
* @param msg
*/
@RabbitHandler
public void processMessage3(ExampleEvent3 msg) {
log.info("param:{msg3 = [" + msg + "]} info:");
}
}
關於消費者確認
RabbitMq消費者可以選擇手動和自動確認兩種模式,如果是自動,消息已到達隊列,RabbitMq對無腦的將消息拋給消費者,一旦發送成功,他會認爲消費者已經成功接收,在RabbitMq內部就把消息給刪除了。另外一種就是手動模式,手動模式需要消費者對每條消息進行確認(也可以批量確認),RabbitMq發送完消息之後,會進入到一個待確認(unacked)的隊列,如下圖紅框部分:
如果消費者發送了ack,RabbitMq將會把這條消息從待確認中刪除。如果是nack並且指明不要重新入隊列,那麼該消息也會刪除。但是如果是nack且指明瞭重新入隊列那麼這條消息將會入隊列,然後重新發送給消費者,被重新投遞的消息消息頭amqp_redelivered屬性會被設置成true,客戶端可以依靠這點來判斷消息是否被確認,可以好好利用這一點,如果每次都重新回隊列會導致同一消息不停的被髮送和拒絕。消費者在確認消息之前和RabbitMq失去了連接那麼消息也會被重新投遞。所以手動確認模式很大程度上提高可靠性。自動模式的消息可以提高吞吐量。
spring手動確認消息需要將SimpleRabbitListenerContainerFactory設置爲手動模式:
simpleRabbitListenerContainerFactory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
手動確認的消費者代碼如下:
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(value = RabbitMQConstant.CONFIRM_EXCHANGE, type = ExchangeTypes.TOPIC,
durable = RabbitMQConstant.FALSE_CONSTANT, autoDelete = RabbitMQConstant.true_CONSTANT),
value = @Queue(value = RabbitMQConstant.CONFIRM_QUEUE, durable = RabbitMQConstant.FALSE_CONSTANT,
autoDelete = RabbitMQConstant.true_CONSTANT),
key = RabbitMQConstant.CONFIRM_KEY),
containerFactory = "containerWithConfirm")
public void process(ExampleEvent msg, Channel channel, @Header(name = "amqp_deliveryTag") long deliveryTag,
@Header("amqp_redelivered") boolean redelivered, @Headers Map<String, String> head) {
try {
log.info("ConsumerWithConfirm receive message:{},header:{}", msg, head);
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
log.error("consume confirm error!", e);
//這一步千萬不要忘記,不會會導致消息未確認,消息到達連接的qos之後便不能再接收新消息
//一般重試肯定的有次數,這裏簡單的根據是否已經重發過來來決定重發。第二個參數表示是否重新分發
channel.basicReject(deliveryTag, !redelivered);
//這個方法我知道的是比上面多一個批量確認的參數
// channel.basicNack(deliveryTag, false,!redelivered);
}
}
關於spring的AcknowledgeMode需要說明,他一共有三種模式:NONE,MANUAL,AUTO,默認是AUTO模式。這比RabbitMq原生多了一種。這一點很容易混淆,這裏的NONE對應其實就是RabbitMq的自動確認,MANUAL是手動。而AUTO其實也是手動模式,只不過是Spring的一層封裝,他根據你方法執行的結果自動幫你發送ack和nack。如果方法未拋出異常,則發送ack。如果方法拋出異常,並且不是AmqpRejectAndDontRequeueException則發送nack,並且重新入隊列。如果拋出異常時AmqpRejectAndDontRequeueException則發送nack不會重新入隊列。
還有一點需要注意的是消費者有一個參數prefetch,它表示的是一個Channel(也就是SimpleMessageListenerContainer的一個線程)預取的消息數量,這個參數只會在手動確認的消費者才生效。可以客戶端利用這個參數來提高性能和做流量控制。如果prefetch設置的是10,當這個Channel上unacked的消息數量到達10條時,RabbitMq便不會在向你發送消息,客戶端如果處理的慢,便可以延遲確認在方法消息的接收。至於提高性能就非常容易理解,因爲這個是批量獲取消息,如果客戶端處理的很快便不用一個一個去等着去新的消息。SpringAMQP2.0開始默認是250,這個參數應該已經足夠了。注意之前的版本默認值是1所以有必要重新設置一下值。當然這個值也不能設置的太大,RabbitMq是通過round robin這個策略來做負載均衡的,如果設置的太大會導致消息不多時一下子積壓到一臺消費者,不能很好的均衡負載。另外如果消息數據量很大也應該適當減小這個值,這個值過大會導致客戶端內存佔用問題。如果你用到了事務的話也需要考慮這個值的影響,因爲事務的用處不大,所以我也沒做過多的深究。