一、RabbitMQ簡(jiǎn)介
??RabbitMQ是實(shí)現(xiàn)了高級(jí)消息隊(duì)列協(xié)議(AMQP)的開(kāi)源消息代理軟件(亦稱面向消息的中間件)。
??相關(guān)組件介紹請(qǐng)參考:
??Exchange分發(fā)消息時(shí),共四種類(lèi)型:direct、fanout、topic、headers,相關(guān)區(qū)別請(qǐng)參考:
二、Docker安裝RabbitMQ
??本文使用阿里云的主機(jī)進(jìn)行演示,使用docker安裝RabbitMQ。(安全組開(kāi)放5672和15672端口)
下載RabbitMQ鏡像
docker pull rabbitmq:3-management
啟動(dòng)容器,設(shè)置用戶名、密碼(yourusername /yourpassword),不設(shè)置默認(rèn)為guest
docker run -d --hostname my-rabbit --name rabbit -e RABBITMQ_DEFAULT_USER=yourusername -e RABBITMQ_DEFAULT_PASS=yourpassword -p 15672:15672 -p 5672:5672 rabbitmq:3-management
進(jìn)入容器
docker exec -it rabbit bash
開(kāi)啟插件
rabbitmq-plugins enable rabbitmq_management
??至此就能在本地通過(guò)IP+端口號(hào)訪問(wèn)RabbitMQ。
三、Spring Boot整合RabbitMQ
示例源碼見(jiàn):https://github.com/just-right/rabbitmq
3.1 注入Queue、Exchange、Binding
??手動(dòng)注入隊(duì)列、交換器和綁定組件。
@Bean
public Queue queue1() {
return new Queue("queue1");
}
@Bean
public Queue queue2() {
return new Queue("queue2");
}
@Bean
public DirectExchange directExchange() {
return new DirectExchange("directExchange");
}
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("fanoutExchange");
}
@Bean
public TopicExchange topicExchange() {
return new TopicExchange("topicExchange");
}
//queue1和queue2綁定fanoutExchange
@Bean
public Binding bindingExchangeAndQueue1(Queue queue1, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(queue1).to(fanoutExchange);
}
@Bean
public Binding bindingExchangeAndQueue2(Queue queue2, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(queue2).to(fanoutExchange);
}
//queue1和queue2綁定topicExchange
@Bean
public Binding bindingExchangeAndQueue3(@Qualifier("queue1") Queue queue1, TopicExchange topicExchange) {
return BindingBuilder.bind(queue1).to(topicExchange).with("topic.match");
}
//#匹配0個(gè)或多個(gè) *匹配一個(gè)
@Bean
public Binding bindingExchangeAndQueue4(@Qualifier("queue2") Queue queue2, TopicExchange topicExchange) {
return BindingBuilder.bind(queue2).to(topicExchange).with("topic.#");
}
??自定義序列化:
//自定義序列化
@Bean
public MessageConverter getMessageConverter() {
return new Jackson2JsonMessageConverter();
}
??發(fā)送與接收消息:發(fā)送消息調(diào)用RabbitTemplate的消息發(fā)送方法,結(jié)合@RabbitListener注解接收消息。
3.2 AmqpAdmin使用
??使用AmqpAdmin的declare相關(guān)方法可以創(chuàng)建隊(duì)列、交換器和綁定組件,其他操作請(qǐng)查詢對(duì)應(yīng)的API。
@Resource
private AmqpAdmin amqpAdmin;
amqpAdmin.declareQueue(new Queue(queueName));
amqpAdmin.declareExchange(exchange);
amqpAdmin.declareBinding(new Binding(queueName,Binding.DestinationType.QUEUE, fanoutExchangeName, null, null));
四、消息確認(rèn)機(jī)制(ACK)
??yaml添加失敗返回、發(fā)送確認(rèn)等配置:
spring:
rabbitmq:
publisher-returns: true #失敗返回
publisher-confirms: true #發(fā)送確認(rèn)
listener:
simple:
acknowledge-mode: manual #手動(dòng)應(yīng)答
retry:
enabled: true #重試
max-attempts: 3 #重發(fā)次數(shù)--無(wú)效
direct:
acknowledge-mode: manual
??注入RabbitTemplate,配置ReturnCallback和ConfirmCallback方法。
@Bean
public RabbitTemplate getRabbitTemplate(ConnectionFactory connectionFactor) {
Logger log = LoggerFactory.getLogger(RabbitTemplate.class);
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactor);
// 消息發(fā)送失敗返回到隊(duì)列中, yml需要配置 publisher-returns: true
rabbitTemplate.setMandatory(true);
// 消息返回, yml需要配置 publisher-returns: true
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
String correlationId = message.getMessageProperties().getCorrelationId();
log.info("消息:{} 發(fā)送失敗, 應(yīng)答碼:{} 原因:{} 交換機(jī): {} 路由鍵: {}", correlationId, replyCode, replyText, exchange, routingKey);
});
// 消息確認(rèn), yml需要配置 publisher-confirms: true
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
log.info("消息發(fā)送到exchange成功");
} else {
log.info("消息發(fā)送到exchange失敗,原因: {}", cause);
}
});
return rabbitTemplate;
}
??配置消息接受者,調(diào)用basicAck方法手動(dòng)確認(rèn),如果發(fā)生異常根據(jù)情況判斷重發(fā)還是丟棄消息。
@RabbitListener(queues = "queue1")
public void getMsg(String msg,Message message, Channel channel) throws IOException {
try {
int a = 1 / 0;
//false只確認(rèn)當(dāng)前一個(gè)消息收到,true確認(rèn)所有consumer獲得的消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
//ack返回false,并重新回到隊(duì)列--注意次數(shù)限制 -- 最后一個(gè)參數(shù)為false則摒棄
logger.info("消息接收失敗,重發(fā)!");
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
//拒絕消息
//channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
springboot整合rabbitmq實(shí)現(xiàn)消息確認(rèn)機(jī)制
SpringBoot集成RabbitMQ消息隊(duì)列搭建與ACK消息確認(rèn)入門(mén)
rabbitmq監(jiān)控之消息確認(rèn)ack
五、延遲隊(duì)列 & 死信隊(duì)列
??使用延遲隊(duì)列和死信隊(duì)列完成消息延遲發(fā)送和消息重復(fù)發(fā)送需求。
5.1 消息延遲發(fā)送
??消息的TTL:消息的存活時(shí)間(Time To Live),當(dāng)消息在隊(duì)列中存在的時(shí)間超過(guò)這個(gè)設(shè)定值之后,系統(tǒng)會(huì)認(rèn)為這個(gè)消息死了,故稱為"死信"。
??死信Exchange:死信最后被轉(zhuǎn)發(fā)到的路由地址。(和普通Exchange相同)
??配置延遲交換器、延遲隊(duì)列、延時(shí)綁定。核心參數(shù)如下:
x-dead-letter-exchange //死信交換器
x-dead-letter-routing-key //死信路由鍵值
x-message-ttl //過(guò)期時(shí)間
//延遲隊(duì)列
@Bean
public Queue delayQueue() {
return QueueBuilder.durable(IRabbitMQConst.DELAY_QUEUE)
// DLX,dead letter發(fā)送到的exchange ,設(shè)置死信隊(duì)列交換器到處理交換器
.withArgument(IRabbitMQConst.DEAD_LETTER_EXCHANGE_ARGUMENT, IRabbitMQConst.DEAD_LETTER_EXCHANGE)
// dead letter攜帶的routing key,配置處理隊(duì)列的路由key
.withArgument(IRabbitMQConst.DEAD_LETTER_ROUTEKEY_ARGUMENT,IRabbitMQConst.DEAD_LETTER_ROUTEKEY)
// 設(shè)置過(guò)期時(shí)間
.withArgument(IRabbitMQConst.MESSAGE_TTL_ARGUMENT, IRabbitMQConst.EXPIRE_TIME)
.build();
}
//延遲交換器-主題
@Bean
public TopicExchange delayExchange() {
return new TopicExchange(IRabbitMQConst.DELAY_EXCHANGE);
}
//延遲綁定
@Bean
public Binding delayBinding(Queue delayQueue, TopicExchange delayExchange) {
return BindingBuilder.bind(delayQueue).to(delayExchange).with(IRabbitMQConst.DELAY_ROUTEKEY);
}
??配置死信交換器、死信隊(duì)列、死信綁定。
//死信隊(duì)列
@Bean
public Queue deadLetterQueue(){
return QueueBuilder.durable(IRabbitMQConst.DEAD_LETTER_QUEUE).build();
}
//死信交換器-主題
@Bean
public TopicExchange deadLetterExchange(){
return new TopicExchange(IRabbitMQConst.DEAD_LETTER_EXCHANGE);
}
//死信綁定
@Bean
public Binding deadLetterBinding(Queue deadLetterQueue, TopicExchange deadLetterExchange) {
return BindingBuilder.bind(deadLetterQueue).to(deadLetterExchange).with(IRabbitMQConst.DEAD_LETTER_ROUTEKEY);
}
??延遲發(fā)送消息測(cè)試,消息和隊(duì)列都可以設(shè)置過(guò)期時(shí)間,取兩者最小值:
@GetMapping(value = "delay")
public void delayTest() {
System.out.println("消息已發(fā)出:" + LocalDateTime.now().toString());
rabbitTemplate.convertAndSend(IRabbitMQConst.DELAY_EXCHANGE, IRabbitMQConst.DELAY_ROUTEKEY, "hello world!", message -> {
// 設(shè)置延遲毫秒值 -- 已消息和隊(duì)列設(shè)置的最小值為準(zhǔn)
message.getMessageProperties().setExpiration(String.valueOf(20000L));
return message;
});
}
??由于隊(duì)列的先進(jìn)先出特性,只有當(dāng)過(guò)期的消息到了隊(duì)列的頂端(隊(duì)首),才會(huì)被真正的丟棄或者進(jìn)入死信隊(duì)列。所以當(dāng)我們?cè)O(shè)置了一個(gè)較短的消息超時(shí)時(shí)間,但是因?yàn)樗坝嘘?duì)列尚未完結(jié),故此消息不會(huì)進(jìn)入死信。
5.2 消息重發(fā)
??消息重發(fā)流程如下所示,包括工作隊(duì)列、重發(fā)隊(duì)列和失敗隊(duì)列。在重發(fā)隊(duì)列中設(shè)置延遲發(fā)送時(shí)間,并將其死信隊(duì)列和私信交換器綁定為工作隊(duì)列和工作交換器,當(dāng)重試次數(shù)超過(guò)設(shè)置的次數(shù),將消息發(fā)送到失敗隊(duì)列等待特定消費(fèi)者處理或者人工處理。
??按照?qǐng)D5-2的流程圖分別設(shè)置工作隊(duì)列、重發(fā)隊(duì)列和失敗隊(duì)列以及其關(guān)聯(lián)的交換器和綁定組件。
??工作隊(duì)列、工作交換器以及工作綁定組件如下所示:
//工作隊(duì)列
@Bean
public Queue workQueue() {
return new Queue("workQueue");
}
//工作交換器
@Bean
public TopicExchange workExchange() {
return new TopicExchange("workExchange");
}
//工作綁定
@Bean
public Binding topicQueueBinding(Queue workQueue, TopicExchange workExchange) {
return BindingBuilder.bind(workQueue).to(workExchange).with("work.topic.*");
}
??重發(fā)隊(duì)列、重發(fā)交換器以及重發(fā)綁定組件如下所示:
//重發(fā)隊(duì)列
@Bean
public Queue retryQueue() {
return QueueBuilder.durable("retryQueue")
// DLX,dead letter發(fā)送到的exchange ,設(shè)置死信隊(duì)列交換器到處理交換器
.withArgument(IRabbitMQConst.DEAD_LETTER_EXCHANGE_ARGUMENT, "workExchange")
// dead letter攜帶的routing key,配置處理隊(duì)列的路由key
.withArgument(IRabbitMQConst.DEAD_LETTER_ROUTEKEY_ARGUMENT,"work.topic.retry")
// 設(shè)置過(guò)期時(shí)間
.withArgument(IRabbitMQConst.MESSAGE_TTL_ARGUMENT, 3000L)
.build();
}
//重發(fā)交換器
@Bean
public TopicExchange retryExchange() {
return ExchangeBuilder.topicExchange("retryExchange").durable(true).build();
}
//重發(fā)綁定
@Bean
public Binding retryDirectBinding(@Qualifier("retryQueue") Queue retryQueue,
@Qualifier("retryExchange") TopicExchange retryExchange) {
return BindingBuilder.bind(retryQueue).to(retryExchange).with("work.retry.*");
}
??失敗隊(duì)列、失敗交換器以及失敗綁定組件如下所示:
//失敗隊(duì)列
@Bean
public Queue failQueue() {
return QueueBuilder.durable("failQueue").build();
}
//失敗交換器
@Bean
public TopicExchange failExchange() {
return ExchangeBuilder.topicExchange("failExchange").durable(true).build();
}
//失敗綁定
@Bean
public Binding failDirectBinding(@Qualifier("failQueue") Queue failQueue,@Qualifier("failExchange") TopicExchange failExchange) {
return BindingBuilder.bind(failQueue).to(failExchange).with("work.fail.*");
}
??消息接收端接收消息,發(fā)送異常時(shí)將消息推送到重發(fā)隊(duì)列,重發(fā)超過(guò)3次將消息推送到失敗隊(duì)列。
@RabbitListener(queues = "workQueue")
public void getMsg3(Message message, Channel channel) throws IOException {
System.out.println("消息已收到:"+LocalDateTime.now().toString());
logger.info("收到消息");
try {
//模擬異常
int a = 1/0;
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
long retryCount = getRetryCount(message.getMessageProperties());
if(retryCount >= 3){
System.out.println("重發(fā)超過(guò)三次,發(fā)送到失敗隊(duì)列!");
rabbitTemplate.convertAndSend("failExchange","work.fail.queue",message);
}else{
System.out.println("第" + (retryCount+1) + "次重發(fā)!");
rabbitTemplate.convertAndSend("retryExchange","work.retry.queue",message);
}
}finally {
//避免死循環(huán)
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
??消息重發(fā)測(cè)試:
@GetMapping(value = "retry")
public void retryTest() {
rabbitTemplate.convertAndSend("workExchange", "work.topic.queue", "hello world!");
}
Spring Boot 整合——RabbitMQ配置延時(shí)隊(duì)列和消息重試
RabbitMQ實(shí)現(xiàn)消息的消費(fèi)確認(rèn),死信隊(duì)列、延遲重試隊(duì)列
六、消息冪等性
??冪等性:任意多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同。
??消息冪等性實(shí)現(xiàn)思路如下所示,客戶端實(shí)現(xiàn)思路類(lèi)似,不再贅述,具體實(shí)現(xiàn)細(xì)節(jié)請(qǐng)查看源碼。
??配置MQ時(shí)如果配置了json解析器,則程序會(huì)走自動(dòng)確認(rèn)消費(fèi),配置文件的配置不生效。手動(dòng)添加RabbitListenerContainerFactory的配置,設(shè)置手動(dòng)確認(rèn)。
@Bean
public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
return factory;
}
RabbitMQ消息冪等性問(wèn)題
RabbitMQ消息可靠性投遞與消費(fèi),消費(fèi)冪等
springBoot-rabbit MQ-設(shè)置手動(dòng)確認(rèn)ACK-Channel shutdown異常
七、綜合測(cè)試
??使用PostMan進(jìn)行測(cè)試:
??測(cè)試結(jié)果如下所示: