Spring Boot整合RabbitMQ

一、RabbitMQ簡(jiǎn)介

??RabbitMQ是實(shí)現(xiàn)了高級(jí)消息隊(duì)列協(xié)議(AMQP)的開(kāi)源消息代理軟件(亦稱面向消息的中間件)。

百度百科-RabbitMQ

圖1-1 RabbitMQ主要組件.png
圖1-2 RabbitMQ主要組件.png

??相關(guān)組件介紹請(qǐng)參考:

SpringBoot與消息(RabbitMQ)
RabbitMQ

??Exchange分發(fā)消息時(shí),共四種類(lèi)型:direct、fanout、topic、headers,相關(guān)區(qū)別請(qǐng)參考:

RabbitMQ(一):RabbitMQ快速入門(mén)

二、Docker安裝RabbitMQ

??本文使用阿里云的主機(jī)進(jìn)行演示,使用docker安裝RabbitMQ。(安全組開(kāi)放5672和15672端口)

圖2-1 安全組端口配置.png

下載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));

RabbitMQ整合Springboot

四、消息確認(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相同)

圖5-1 死信Exchange流程.png

??配置延遲交換器、延遲隊(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)者處理或者人工處理。

圖5-2 消息重發(fā)流程圖.png

??按照?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)查看源碼。

https://github.com/just-right/rabbitmq

圖6-1 消息冪等性實(shí)現(xiàn)思路.png

??配置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è)試:

圖7-1 請(qǐng)求測(cè)試.png

??測(cè)試結(jié)果如下所示:

圖7-2 測(cè)試結(jié)果.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,786評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,986評(píng)論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,204評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,964評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,354評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,554評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,106評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,918評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,093評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,342評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,755評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,009評(píng)論 1 289
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,839評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,107評(píng)論 2 375