spring集成RabbitMQ以及隊列模式(附源碼.配置)

1.背景
RabbitMQ是一個由erlang開發的AMQP(Advanved Message Queue)的開源實現。

2.應用場景
2.1異步處理
場景說明:用戶註冊後,需要發註冊郵件和註冊短信,傳統的做法有兩種1.串行的方式;2.並行的方式
(1)串行方式:將註冊信息寫入數據庫後,發送註冊郵件,再發送註冊短信,以上三個任務全部完成後才返回給客戶端。 這有一個問題是,郵件,短信並不是必須的,它只是一個通知,而這種做法讓客戶端等待沒有必要等待的東西.
在這裏插入圖片描述
(2)並行方式:將註冊信息寫入數據庫後,發送郵件的同時,發送短信,以上三個任務完成後,返回給客戶端,並行的方式能提高處理的時間。
在這裏插入圖片描述
假設三個業務節點分別使用50ms,串行方式使用時間150ms,並行使用時間100ms。雖然並性已經提高的處理時間,但是,前面說過,郵件和短信對我正常的使用網站沒有任何影響,客戶端沒有必要等着其發送完成才顯示註冊成功,應該是寫入數據庫後就返回.
(3)消息隊列
引入消息隊列後,把發送郵件,短信不是必須的業務邏輯異步處理
在這裏插入圖片描述
由此可以看出,引入消息隊列後,用戶的響應時間就等於寫入數據庫的時間+寫入消息隊列的時間(可以忽略不計),引入消息隊列後處理後,響應時間是串行的3倍,是並行的2倍。

2.2 應用解耦
場景:雙11是購物狂節,用戶下單後,訂單系統需要通知庫存系統,傳統的做法就是訂單系統調用庫存系統的接口.
在這裏插入圖片描述
這種做法有一個缺點:

當庫存系統出現故障時,訂單就會失敗。(這樣馬雲將少賺好多好多錢^ ^)

訂單系統和庫存系統高耦合. 

引入消息隊列 

在這裏插入圖片描述
訂單系統:用戶下單後,訂單系統完成持久化處理,將消息寫入消息隊列,返回用戶訂單下單成功。

· 庫存系統:訂閱下單的消息,獲取下單消息,進行庫操作。

· 就算庫存系統出現故障,消息隊列也能保證消息的可靠投遞,不會導致消息丟失(馬雲這下高興了).

 流量削峯:流量削峯一般在秒殺活動中應用廣泛 
場景:秒殺活動,一般會因爲流量過大,導致應用掛掉,爲了解決這個問題,一般在應用前端加入消息隊列。 
作用: 
1.可以控制活動人數,超過此一定閥值的訂單直接丟棄(我爲什麼秒殺一次都沒有成功過呢^^) 
2.可以緩解短時間的高流量壓垮應用(應用程序按自己的最大處理能力獲取訂單) 

在這裏插入圖片描述
1.用戶的請求,服務器收到之後,首先寫入消息隊列,加入消息隊列長度超過最大值,則直接拋棄用戶請求或跳轉到錯誤頁面.
2.秒殺業務根據消息隊列中的請求信息,再做後續處理.

3.系統架構
在這裏插入圖片描述
幾個概念說明:
Broker:它提供一種傳輸服務,它的角色就是維護一條從生產者到消費者的路線,保證數據能按照指定的方式進行傳輸,
Exchange:消息交換機,它指定消息按什麼規則,路由到哪個隊列。
Queue:消息的載體,每個消息都會被投到一個或多個隊列。
Binding:綁定,它的作用就是把exchange和queue按照路由規則綁定起來.
Routing Key:路由關鍵字,exchange根據這個關鍵字進行消息投遞。
vhost:虛擬主機,一個broker裏可以有多個vhost,用作不同用戶的權限分離。
Producer:消息生產者,就是投遞消息的程序.
Consumer:消息消費者,就是接受消息的程序.
Channel:消息通道,在客戶端的每個連接裏,可建立多個channel.

RabbitMQ提供了三種Exchange:fanout,direct,topic

性能排序:fanout > direct > topic

路由鍵(routing key):消息發送給交換器時,消息將擁有一個路由鍵(默認爲空),交換器根據這個路由鍵將消息發送到匹配的隊列中。路由鍵是偏向生產端的概念。

綁定鍵(binding key):隊列需要通過綁定鍵(默認爲空)綁定到交換器上,交換器將消息的路由鍵與所綁定隊列的綁定鍵進行匹配,正確匹配的消息將發送到隊列中。綁定鍵是偏向消費端的概念。

每條消息都有 routing key,當然可能爲空,當消息到達 exchange 的時候, rabbitmq 會匹配消息的 routing key 和 binding key,如果匹配,就會轉發到對應的 queue,如果不匹配,就會丟棄消息。

在這裏插入圖片描述
任何發送到Direct Exchange的消息都會被轉發到RouteKey中指定的Queue。

    1.一般情況可以使用rabbitMQ自帶的Exchange:”"(該Exchange的名字爲空字符串,下文稱其爲default Exchange)。

    2.這種模式下不需要將Exchange進行任何綁定(binding)操作

    3.消息傳遞時需要一個“RouteKey”,可以簡單的理解爲要發送到的隊列名字。

    4.如果vhost中不存在RouteKey中指定的隊列名,則該消息會被拋棄。

如果路由鍵(routing key)匹配成功,消息就被投遞到對應的各個隊列,綁定鍵(binding key)不支持“*”和“#”。消費者在接受消息的信道上可以給生產者反饋。

當 routing key 和 binding key 相同時,消息就會轉發到綁定的 queue。

需要注意的是:如果有兩個 queue 綁定到一個 exchange,並且 binding key 一樣,那麼消息會發到兩個 queue。默認情況下,rabbitmq 會有 name 爲空的 exchange,類型爲 direct,當 declare 一個 queue 的時候,默認會綁定到這個 exchange,binding key 是 queue name。
在這裏插入圖片描述
任何發送到Fanout Exchange的消息都會被轉發到與該Exchange綁定(Binding)的所有Queue上。

    1.可以理解爲路由表的模式

    2.這種模式不需要RouteKey

    3.這種模式需要提前將Exchange與Queue進行綁定,一個Exchange可以綁定多個Queue,一個Queue可以同多個Exchange進行綁定。

    4.如果接受到消息的Exchange沒有與任何Queue綁定,則消息會被拋棄。

fanout 當 exchange 類型是 fanout 的時候,並不需要(routing key)路由鍵,發送到該 exchange 的消息會自動轉發給所有綁定的 queues ,也就是說每個 queue 都會收到一份消息。隊列不存在綁定鍵(binding key),消費者在接受消息的當前信道上可以給生產者反饋。

在這裏插入圖片描述
任何發送到Topic Exchange的消息都會被轉發到所有關心RouteKey中指定話題的Queue上

    1.這種模式較爲複雜,簡單來說,就是每個隊列都有其關心的主題,所有的消息都帶有一個“標題”(RouteKey),Exchange會將消息轉發到所有關注主題能與RouteKey模糊匹配的隊列。

    2.這種模式需要RouteKey,也許要提前綁定Exchange與Queue。

    3.在進行綁定時,要提供一個該隊列關心的主題,如“#.log.#”表示該隊列關心所有涉及log的消息(一個RouteKey爲”MQ.log.error”的消息會被轉發到該隊列)。

    4.“#”表示0個或若干個關鍵字,“*”表示一個關鍵字。如“log.*”能與“log.warn”匹配,無法與“log.warn.timeout”匹配;但是“log.#”能與上述兩者匹配。

    5.同樣,如果Exchange沒有發現能夠與RouteKey匹配的Queue,則會拋棄此消息。

存在(routing key)路由鍵,消息以廣播的形式發送到綁定鍵(bing key)匹配的各個隊列,綁定鍵(binding key)支持“*”和“#”。

routing key 和原來一樣,不過 binding key 的名字中可以使用三種特殊的符號:.、*、#。. 把 binding key 分割成不同的單詞(word),* 匹配一個單詞,# 可以匹配 0 個或者任意多個單詞。

1,首先引入配置文件org.springframework.amqp,如下:

<dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
            <version>1.2.0.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.2.0</version>
        </dependency>

2,準備工作:安裝好rabbitmq,並在項目中增加配置文件 rabbit.properties 內容如下:

rmq.ip=192.188.113.114   
rmq.port=5672
rmq.producer.num=20
rmq.manager.user=admin
rmq.manager.password=admin

3,配置spring-rabbitmq.xml,內容如下:

<!-- 公共部分 -->
<!-- 創建連接類 連接安裝好的 rabbitmq -->
<bean id="connectionFactory"  class="org.springframework.amqp.rabbit.connection.CachingConnectionFactory">
    <constructor-arg value="localhost" />    
    <!-- username,訪問RabbitMQ服務器的賬戶,默認是guest -->
    <property name="username" value="${rmq.manager.user}" />
    <!-- username,訪問RabbitMQ服務器的密碼,默認是guest -->   
    <property name="password" value="${rmq.manager.password}" />
    <!-- host,RabbitMQ服務器地址,默認值"localhost" -->   
    <property name="host" value="${rmq.ip}" />   
    <!-- port,RabbitMQ服務端口,默認值爲5672 -->
    <property name="port" value="${rmq.port}" />
    <!-- channel-cache-size,channel的緩存數量,默認值爲25 -->
    <property name="channel-cache-size" value="50" />
    <!-- cache-mode,緩存連接模式,默認值爲CHANNEL(單個connection連接,連接之後關閉,自動銷燬) -->
    <property name="cache-mode" value="CHANNEL" />   
</bean>
<!--或者這樣配置,connection-factory元素實際就是註冊一個org.springframework.amqp.rabbit.connection.CachingConnectionFactory實例
<rabbit:connection-factory id="connectionFactory" host="${rmq.ip}" port="${rmq.port}"
username="${rmq.manager.user}" password="${rmq.manager.password}" />-->
<rabbit:admin connection-factory="connectionFactory"/>

<!--定義消息隊列,durable:是否持久化,如果想在RabbitMQ退出或崩潰的時候,不會失去所有的queue和消息,需要同時標誌隊列(queue)和交換機(exchange)是持久化的,即rabbit:queue標籤和rabbit:direct-exchange中的durable=true,而消息(message)默認是持久化的可以看類org.springframework.amqp.core.MessageProperties中的屬性public static final MessageDeliveryMode DEFAULT_DELIVERY_MODE = MessageDeliveryMode.PERSISTENT;exclusive: 僅創建者可以使用的私有隊列,斷開後自動刪除;auto_delete: 當所有消費客戶端連接斷開後,是否自動刪除隊列 -->
<rabbit:queue name="spittle.alert.queue.1" id="queue_1" durable="true" auto-delete="false" exclusive="false" />
<rabbit:queue name="spittle.alert.queue.2" id="queue_2" durable="true" auto-delete="false" exclusive="false" />
<rabbit:queue name="spittle.alert.queue.3" id="queue_3" durable="true" auto-delete="false" exclusive="false" />

<!--綁定隊列,rabbitmq的exchangeType常用的三種模式:direct,fanout,topic三種,我們用direct模式,即rabbit:direct-exchange標籤,Direct交換器很簡單,如果是Direct類型,就會將消息中的RoutingKey與該Exchange關聯的所有Binding中的BindingKey進行比較,如果相等,則發送到該Binding對應的Queue中。有一個需要注意的地方:如果找不到指定的exchange,就會報錯。但routing key找不到的話,不會報錯,這條消息會直接丟失,所以此處要小心,auto-delete:自動刪除,如果爲Yes,則該交換機所有隊列queue刪除後,自動刪除交換機,默認爲false -->
<rabbit:direct-exchange id="spittle.fanout" name="spittle.fanout" durable="true" auto-delete="false">
    <rabbit:bindings>
        <rabbit:binding queue="spittle.alert.queue.1" key="{alert.queue.1}"></rabbit:binding>
        <rabbit:binding queue="spittle.alert.queue.2" key="{alert.queue.2}"></rabbit:binding>
        <rabbit:binding queue="spittle.alert.queue.3" key="{alert.queue.3}"></rabbit:binding>
    </rabbit:bindings>
</rabbit:fanout-exchange>

<!-- 生產者部分 -->
<!-- 發送消息的producer類,也就是生產者 -->
<bean id="msgProducer" class="com.asdf.sdf.ClassA">
    <!-- value中的值就是producer中的的routingKey,也就是隊列名稱,它與上面的rabbit:bindings標籤中的key必須相同 -->
    <property name="queueName" value="{alert.queue.1}"/>
</bean>

<!-- spring amqp默認的是jackson 的一個插件,目的將生產者生產的數據轉換爲json存入消息隊列,由於fastjson的速度快於jackson,這裏替換爲fastjson的一個實現 -->
<bean id="jsonMessageConverter" class="com.jy.utils.FastJsonMessageConverter"></bean>
<!-- 或者配置jackson -->
<!--
<bean id="jsonMessageConverter" class="org.springframework.amqp.support.converter.Jackson2JsonMessageConverter" />
-->

<rabbit:template exchange="test-exchange" id="rabbitTemplate" connection-factory="connectionFactory" message-converter="jsonMessageConverter" />

<!-- 消費者部分 -->
<!-- 自定義接口類 -->
<bean id="testHandler" class="com.rabbit.TestHandler"></bean>

<!-- 用於消息的監聽的代理類MessageListenerAdapter -->
<bean id="testQueueListenerAdapter" class="org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter" >
        <!-- 類名 -->
    <constructor-arg ref="testHandler" />
        <!-- 方法名 -->
    <property name="defaultListenerMethod" value="handlerTest"></property>
                <property name="messageConverter" ref="jsonMessageConverter"></property>
</bean>

<!-- 配置監聽acknowledeg="manual"設置手動應答,它能夠保證即使在一個worker處理消息的時候用CTRL+C來殺掉這個worker,或者一個consumer掛了(channel關閉了、connection關閉了或者TCP連接斷了),也不會丟失消息。因爲RabbitMQ知道沒發送ack確認消息導致這個消息沒有被完全處理,將會對這條消息做re-queue處理。如果此時有另一個consumer連接,消息會被重新發送至另一個consumer會一直重發,直到消息處理成功,監聽容器acknowledge="auto" concurrency="30"設置發送次數,最多發送30次 -->
<rabbit:listener-container connection-factory="connectionFactory" acknowledge="auto" concurrency="20">
        <rabbit:listener queues="spittle.alert.queue.1" ref="testQueueListenerAdapter" />
    <rabbit:listener queues="spittle.alert.queue.2" ref="testQueueListenerAdapter" />
    <rabbit:listener queues="spittle.alert.queue.2" ref="testQueueListenerAdapter" />
</rabbit:listener-container>

4,生產者(發送端)代碼:

@Resource  
private RabbitTemplate rabbitTemplate;
private String queueName;  
public void sendMessage(CommonMessage msg){
         try {  
              logger.error("發送信息開始");
              System.out.println(rabbitTemplate.getConnectionFactory().getHost());  
             //發送信息  queueName交換機,就是上面的routingKey msg.getSource() 爲 test_key 
             rabbitTemplate.convertAndSend(queueName,msg.getSource(), msg);
             //如果是普通字符串消息需要先序列化,再發送消息
             //rabbitTemplate.convertAndSend(queueName,msg.getSource(), SerializationUtils.serialize(msg));
             logger.error("發送信息結束");
         } catch (Exception e) {  
             e.printStackTrace();
         }
    }

public void setQueueName(String queueName) {
    this.queueName = queueName;
}

5,消費端代碼:TestHandler 類

public class TestHandler  {
    @Override
    public void handlerTest(CommonMessage commonMessage) {
        System.out.println("DetailQueueConsumer: " + new String(message.getBody()));
    }
}

其他exchangeType介紹:

fanOut:

<!-- Fanout 扇出,顧名思義,就是像風扇吹麪粉一樣,吹得到處都是。如果使用fanout類型的exchange,那麼routing key就不重要了。因爲凡是綁定到這個exchange的queue,都會受到消息。 -->
<rabbit:fanout-exchange name="delayed_message_exchange" durable="true" auto-delete="false" id="delayed_message_exchange">  
        <rabbit:bindings>  
            <rabbit:binding queue="test_delay_queue"/>  
        </rabbit:bindings>  
</rabbit:fanout-exchange>

topic:如果說direct是將消息放到exchange綁定的一個queue裏(一對一);fanout是將消息放到exchange綁定的所有queue裏(一對所有);那麼topic類型的exchange就可以實現(一對部分),應用場景就是打印不同級別的錯誤日誌,我們的系統出錯後會根據不同的錯誤級別生成error_levelX.log日誌,我們在後臺首先要把所有的error保存在一個總的queue(綁定了一個*.error的路由鍵)裏,然後再按level分別存放在不同的queue。

<!-- 發送端不是按固定的routing key發送消息,而是按字符串“匹配”發送,接收端同樣如此 -->
<rabbit:topic-exchange name="message-exchange" durable="true" auto-delete="false" id="message-exchange">
        <rabbit:bindings>
            <rabbit:binding queue="Q1" pattern="error.*.log" />
            <rabbit:binding queue="Q2" pattern="error.level1.log" />
            <rabbit:binding queue="Q3" pattern="error.level2.log" />
        </rabbit:bindings>
</rabbit:topic-exchange>

routing key綁定如下圖:
在這裏插入圖片描述
參考大佬博客:

https://blog.csdn.net/nandao158/article/details/81065892

https://www.cnblogs.com/LipeiNet/p/6079427.html

https://www.toutiao.com/a6598154241037042189/?tt_from=mobile_qq&utm_campaign=client_share&timestamp=1536277584&app=news_article&utm_source=mobile_qq&iid=43157585039&utm_medium=toutiao_android&group_id=6598154241037042189
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章