什麼鬼,面試官竟然讓我用Redis實現一個消息隊列?

[ 轉載 ]原文鏈接:https://blog.csdn.net/yelvgou9995/article/details/103348313

衆所周知,redis是一個高性能的分佈式key-value存儲系統,在NoSQL數據庫市場上,redis自己就佔據了將近半壁江山,足以見到其強大之處。同時,由於redis的單線程特性,我們可以將其用作爲一個消息隊列。本篇文章就來講講如何將redis整合到spring boot中,並用作消息隊列的……

一、什麼是消息隊列

“消息隊列”是在消息的傳輸過程中保存消息的容器。——《百度百科》

消息我們可以理解爲在計算機中或在整個計算機網絡中傳遞的數據。

隊列是我們在學習數據結構的時候學習的基本數據結構之一,它具有先進先出的特性。

所以,消息隊列就是一個保存消息的容器,它具有先進先出的特性。

爲什麼會出現消息隊列?

  1. 異步:常見的B/S架構下,客戶端向服務器發送請求,但是服務器處理這個消息需要花費的時間很長的時間,如果客戶端一直等待服務器處理完消息,會造成客戶端的系統資源浪費;而使用消息隊列後,服務器直接將消息推送到消息隊列中,由專門的處理消息程序處理消息,這樣客戶端就不必花費大量時間等待服務器的響應了;
  2. 解耦:傳統的軟件開發模式,模塊之間的調用是直接調用,這樣的系統很不利於系統的擴展,同時,模塊之間的相互調用,數據之間的共享問題也很大,每個模塊都要時時刻刻考慮其他模塊會不會掛了;使用消息隊列以後,模塊之間不直接調用,而是通過數據,且當某個模塊掛了以後,數據仍舊會保存在消息隊列中。最典型的就是生產者-消費者模式,本案例使用的就是該模式;
  3. 削峯填谷:某一時刻,系統的併發請求暴增,遠遠超過了系統的最大處理能力後,如果不做任何處理,系統會崩潰;使用消息隊列以後,服務器把請求推送到消息隊列中,由專門的處理消息程序以合理的速度消費消息,降低服務器的壓力。

下面一張圖我們來簡單瞭解一下消息隊列

什麼鬼,面試官竟然讓我用Redis實現一個消息隊列?

由上圖可以看到,消息隊列充當了一箇中間人的角色,我們可以通過操作這個消息隊列來保證我們的系統穩定。

二、環境準備

Java環境:jdk1.8

spring boot版本:2.2.1.RELEASE

redis-server版本:3.2.100

三、相關依賴

這裏只展示與redis相關的依賴,

什麼鬼,面試官竟然讓我用Redis實現一個消息隊列?

這裏解釋一下這兩個依賴:

  • 第一個依賴是對redis NoSQL的支持
  • 第二個依賴是spring integration與redis的結合,這裏添加這個代碼主要是爲了實現分佈式鎖

四、配置文件

這裏只展示與redis相關的配置

什麼鬼,面試官竟然讓我用Redis實現一個消息隊列?

五、代碼配置

redis用作消息隊列,其在spring boot中的主要表現爲一RedisTemplate.convertAndSend()方法和一個MessageListener接口。所以我們要在IOC容器中注入一個RedisTemplate和一個實現了MessageListener接口的類。話不多說,先看代碼

配置RedisTemplate

配置RedisTemplate的主要目的是配置序列化方式以解決亂碼問題,同時合理配置序列化方式還能降低一點性能開銷。

什麼鬼,面試官竟然讓我用Redis實現一個消息隊列?

代碼第12行,我們配置默認的序列化方式爲GenericJackson2JsonRedisSerializer

代碼第13行,我們配置鍵的序列化方式爲StringRedisSerializer

代碼第14行,我們配置哈希表的值的序列化方式爲GenericJackson2JsonRedisSerializer

RedisTemplate幾種序列化方式的簡要介紹

什麼鬼,面試官竟然讓我用Redis實現一個消息隊列?

六、redis隊列監聽器(消費者)

上面說了,與redis隊列監聽器相關的類爲一個名爲MessageListener的接口,下面是該接口的源碼

什麼鬼,面試官竟然讓我用Redis實現一個消息隊列?

可以看到,該接口僅有一個onMessage(Message message, @Nullable byte[] pattern)方法,該方法便是監聽到隊列中消息後的回調方法。下面解釋一下這兩個參數:

  • message:redis消息類,該類中僅有兩個方法
    • byte[] getBody()以二進制形式獲取消息體
    • byte[] getChannel()以二進制形式獲取消息通道
  • pattern:二進制形式的消息通道,和message.getChannel()返回值相同

介紹完接口,我們來實現一個簡單的redis隊列監聽器

什麼鬼,面試官竟然讓我用Redis實現一個消息隊列?

代碼很簡單,就是輸出參數中包含的關鍵信息。需要注意的是,RedisSerializer的實現要與上面配置的序列化方式一致。

隊列監聽器實現完以後,我們還需要將這個監聽器添加到redis隊列監聽器容器中,代碼如下:

什麼鬼,面試官竟然讓我用Redis實現一個消息隊列?

這幾行代碼大概意思就是新建一個Redis消息監聽器容器,然後將監聽器和管道名想綁定,最後返回這個容器。

這裏要注意的是,這個管道名和下面將要說的推送消息時的管道名要一致,不然監聽器監聽不到消息。

七、redis隊列推送服務(生產者)

上面我們配置了RedisTemplate將要在這裏使用到。

代碼如下:

什麼鬼,面試官竟然讓我用Redis實現一個消息隊列?

關鍵代碼爲第7行,redis.convertAndSend()這個方法的作用爲,向某個通道(參數1)推送一條消息(第二個參數)。

這裏還是要注意上面所說的,生產者和消費者的通道名要相同。

至此,消息隊列的生產者和消費者已經全部編寫完成。

八、遇到的問題及解決辦法

1、spring boot使用log4j2日誌框架問題

在我添加了spring-boot-starter-log4j2依賴並在spring-boot-starter-web中排除了spring-boot-starter-logging後,運行項目,還是會提示下面的錯誤:

<pre style="box-sizing: border-box; outline: 0px; margin: 0px 0px 24px; padding: 8px; position: relative; font-family: Consolas, Inconsolata, Courier, monospace; white-space: pre-wrap; word-wrap: break-word; overflow-x: auto; font-size: 14px; line-height: 22px; color: rgb(0, 0, 0);">SLF4J: Class path contains multiple SLF4J bindings.

SLF4J: Found binding in [jar:file:.....m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class]

SLF4J: Found binding in [jar:file:.....m2/repository/org/apache/logging/log4j/log4j-slf4j-impl/2.12.1/log4j-slf4j-impl-2.12.1.jar!/org/slf4j/impl/StaticLoggerBinder.class]

SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.

SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder]</pre>

這個錯誤就是maven中有多個日誌框架導致的。後來通過依賴分析,發現在spring-boot-starter-data-redis中,也依賴了spring-boot-starter-logging,解決辦法也很簡單,下面貼出詳細代碼

什麼鬼,面試官竟然讓我用Redis實現一個消息隊列?

什麼鬼,面試官竟然讓我用Redis實現一個消息隊列?

2、redis隊列監聽器線程安全問題

redis隊列監聽器的監聽機制是:使用一個線程監聽隊列,隊列有未消費的消息則取出消息並生成一個新的線程來消費消息。如果你還記得,我開頭說的是由於redis單線程特性,因此我們用它來做消息隊列,但是如果監聽器每次接受一個消息就生成新的線程來消費信息的話,這樣就完全沒有使用到redis的單線程特性,同時還會產生線程安全問題。

單一消費者(一個通道只有一個消費者)的解決辦法

最簡單的辦法莫過於爲onMessage()方法加鎖,這樣簡單粗暴卻很有用,不過這種方式無法控制隊列監聽的速率,且無限制的創造線程最終會導致系統資源被佔光。

那如何解決這種情況呢?線程池。

在將監聽器添加到容器的配置的時候,RedisMessageListenerContainer類中有一個方法setTaskExecutor(Executor taskExecutor)可以爲監聽容器配置線程池。配置線程池以後,所有的線程都會由該線程池產生,由此,我們可以通過調節線程池來控制隊列監聽的速率。

多個消費者(一個通道有多個消費者)的解決辦法

單一消費者的問題相比於多個消費者來說還是較爲簡單,因爲Java內置的鎖都是隻能控制自己程序的運行,不能干擾其他的程序的運行;然而現在很多時候我們都是在分佈式環境下進行開發,這時處理多個消費者的情況就很有意義了。

那麼這種問題如何解決呢?分佈式鎖。

下面來簡要科普一下什麼是分佈式鎖:

分佈式鎖是指在分佈式環境下,同一時間只有一個客戶端能夠從某個共享環境中(例如redis)獲取到鎖,只有獲取到鎖的客戶端才能執行程序。

然後分佈式鎖一般要滿足:排他性(即同一時間只有一個客戶端能夠獲取到鎖)、避免死鎖(即超時後自動釋放)、高可用(即獲取或釋放鎖的機制必須高可用且性能佳)

上面講依賴的時候,我們導入了一個spring-integration-redis依賴,這個依賴裏面包含了很多實用的工具類,而我們接下來要講的分佈式鎖就是這個依賴下面的一個工具包RedisLockRegistry。

首先講一下如何使用,導入了依賴以後,首先配置一個Bean

什麼鬼,面試官竟然讓我用Redis實現一個消息隊列?

RedisLockRegistry的構造函數,第一個參數是redis連接池,第二個參數是鎖的前綴,即取出的鎖,鍵名爲“demo-lock:KEY_NAME”,第三個參數爲鎖的過期時間(秒),默認爲60秒,當持有鎖超過該時間後自動過期。

使用鎖的方法,下面是對監聽器的修改

什麼鬼,面試官竟然讓我用Redis實現一個消息隊列?

上面代碼的代碼比起前面的監聽器代碼,只是多了一個注入的RedisLockRegistry,一個通過redisLockRegistry.obtain()方法獲取鎖,一個加鎖一個解鎖,然後這就完成了分佈式鎖的使用。

注意這個獲取鎖的方法redisLockRegistry.obtain(),其返回的是一個名爲RedisLock的鎖,這是一個私有內部類,它實現了Lock接口,因此我們不能從代碼外部創建一個他的實例,只能通過obtian()方法來獲取這個鎖。

文章來源:https://blog.csdn.net/yelvgou9995/article/details/103348313


超值推薦:

阿里雲雙12已開啓,雲產品冰點價,新用戶專享1折起,1核2G雲服務器僅需89元/年229元/3年。買了對於提升技術或者在服務器上搭建自由站點,都是很不錯的,如果自己有實際操作,面試+工作中肯定是加分項。(老用戶可以用家人或朋友的賬號購買,真心便宜&划算)

可“掃碼”或者“點擊購買 "

什麼鬼,面試官竟然讓我用Redis實現一個消息隊列?


END

什麼鬼,面試官竟然讓我用Redis實現一個消息隊列?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章