在第 10 課時中講過“手寫消息隊列”,當時粗略的講了 Java API 中使用 Queue 實現自定義消息隊列,以及使用 Delayed 實現延遲隊列的示例;同時還講了 RabbitMQ 中的一些基礎概念。本課時我們將會更加深入的講解 MQ(Message Queue,消息隊列)中間件,以及這些熱門中間件的具體使用。
我們本課時的面試題是,MQ 常見的使用場景有哪些?你都用過哪些 MQ 中間件?
#### 典型回答
在介紹 MQ 的使用場景之前,先來回憶一下 MQ 的作用。MQ 可以用來實現削峰填谷,也就是使用它可以解決短時間內爆發式的請求任務,在不使用 MQ 的情況下會導致服務處理不過來,出現應用程序假死的情況,而使用了 MQ 之后可以把這些請求先暫存到消息隊列中,然后進行排隊執行,那么就不會出現應用程序假死的情況了,所以它的第一個應用就是商品秒殺以及產品搶購等使用場景,如下圖所示:

* [ ] 使用 MQ 實現消息通訊
使用 MQ 可以作為消息通訊的實現手段,利用它可以實現點對點的通訊或者多對多的聊天室功能。
點對點的消息通訊如下圖所示:

多對多的消息通訊如下圖所示:

* [ ] 使用 MQ 實現日志系統
可使用 MQ 實現對日志的采集和轉發,比如有多個日志寫入到程序中,然后把日志添加到 MQ,緊接著由日志處理系統訂閱 MQ,最后 MQ 將消息接收并轉發給日志處理系統,這樣就完成了日志的分析和保存功能,如下圖所示:

常用的 MQ 中間件有 RabbitMQ、Kafka 和 Redis 等,其中 Redis 屬于輕量級的消息隊列,而 RabbitMQ、Kafka 屬于比較成熟且比較穩定和高效的 MQ 中間件。
#### 考點分析
MQ 屬于中高級或優秀的程序員必備的技能,對于 MQ 中間件掌握的數量則是你技術廣度和編程經驗的直接體現信息之一。值得慶幸的是,關于 MQ 中間件的實現原理和使用方式都比較類似,因此如果開發者掌握一項 MQ 中間件再去熟悉其他 MQ 中間件時,會非常的容易。
MQ 相關的面試題還有這些:
* MQ 的特點是什么?引入 MQ 中間件會帶來哪些問題?
* 常見的 MQ 中間件的優缺點分析。
#### 知識擴展
* [ ] MQ 的特點及注意事項
MQ 具有以下 5 個特點。
* **先進先出**:消息隊列的順序一般在入列時就基本確定了,最先到達消息隊列的信息,一般情況下也會先轉發給訂閱的消費者,我們把這種實現了先進先出的數據結構稱之為隊列。
* **發布、訂閱工作模式**:生產者也就是消息的創建者,負責創建和推送數據到消息服務器;消費者也就是消息的接收方,用于處理數據和確認消息的消費;消息隊列也是 MQ 服務器中最重要的組成元素之一,它負責消息的存儲,這三者是 MQ 中的三個重要角色。而它們之間的消息傳遞與轉發都是通過發布以及訂閱的工作模式來進行的,即生產者把消息推送到消息隊列,消費者訂閱到相關的消息后進行消費,在消息非阻塞的情況下,此模式基本可以實現同步操作的效果。并且此種工作模式會把請求的壓力轉移給 MQ 服務器,以減少了應用服務器本身的并發壓力。
* **持久化**:持久化是把消息從內存存儲到磁盤的過程,并且在服務器重啟或者發生宕機的情況下,重新啟動服務器之后是保證數據不會丟失的一種手段,也是目前主流 MQ 中間件都會提供的重要功能。
* **分布式**:MQ 的一個主要特性就是要應對大流量、大數據的高并發環境,一個單體的 MQ 服務器是很難應對這種高并發的壓力的,所以 MQ 服務器都會支持分布式應用的部署,以分攤和降低高并發對 MQ 系統的沖擊。
* **消息確認**:消息消費確認是程序穩定性和安全性的一個重要考核指標,假如消費者在拿到消息之后突然宕機了,那么 MQ 服務器會誤認為此消息已經被消費者消費了,從而造成消息丟失的問題,而目前市面上的主流 MQ 都實現了消息確認的功能,保證了消息不會丟失,從而保證了系統的穩定性。
* [ ] 引入 MQ 系統會帶來的問題
任何系統的引入都是有兩面性的,MQ 也不例外,在引入 MQ 之后,可能會帶來以下兩個問題。
* **增加了系統的運行風險**:引入 MQ 系統,則意味著新增了一套系統,并且其他的業務系統會對 MQ 系統進行深度依賴,系統部署的越多則意味著發生故障的可能性就越大,如果 MQ 系統掛掉的話可能會導致整個業務系統癱瘓。
* **增加了系統的復雜度**:引入 MQ 系統后,需要考慮消息丟失、消息重復消費、消息的順序消費等問題,同時還需要引入新的客戶端來處理 MQ 的業務,增加了編程的運維門檻,增加了系統的復雜性。
使用 MQ 需要注意的問題,不要過度依賴 MQ,比如發送短信驗證碼或郵件等功能,這種低頻但有可能比較耗時的功能可以使用多線程異步處理即可,不用任何的功能都依賴 MQ 中間件來完成,但像秒殺搶購可能會導致超賣(也就是把貨賣多了,庫存變成負數了)等短時間內高并發的請求,此時建議使用 MQ 中間件。
* [ ] 常用的 MQ 中間件
常用的 MQ 中間件有 Redis、RabbitMQ、Kafka,下來我們分別來看看各自的作用。
* Redis 輕量級的消息中間件
Redis 是一個高效的內存性數據庫中間件,但使用 Redis 也可以實現消息隊列的功能。
早期的 Redis(Redis 5.0 之前)是不支持消息確認的,那時候我們可以通過 List 數據類型的 lpush 和 rpop 方法來實現隊列消息的存入和讀取功能,或者使用 Redis 提供的發布訂閱(pub/sub)功能來實現消息隊列,但這種模式不支持持久化,List 雖然支持持久化但不能設置復雜的路由規則來匹配多個消息,并且他們二者都不支持消息消費確認。
于是在 Redis 5.0 之后提供了新的數據類型 Stream 解決了消息確認的問題,但它同樣不能提供復雜的路由匹配規則,因此在業務不復雜的場景下可以嘗試性的使用 Redis 提供的消息隊列。
* RabbitMQ
在第 10 課時中,我們對 RabbitMQ 有過初步的講解,它是一個實現了標準的高級消息隊列協議(AMQP,Advanced Message Queuing Protocol)的老牌開源消息中間件,最初起源于金融系統,后來被普遍應用在了其他分布式系統中,它支持集群部署,和多種客戶端調用。
之前主要介紹了 RabbitMQ 的基礎功能,本課時我們重點來看 RabbitMQ 集群相關的內容。
RabbitMQ 集群是由多個節點組成,但默認情況下每個節點并不是存儲所有隊列的完整拷貝,這是出于存儲空間和性能的考慮,因為如果存儲了隊列的完整拷貝,那么就會有很多冗余的重復數據,并且在新增節點的情況下,不但沒有新增存儲空間,反而需要更大的空間來存儲舊的數據;同樣的道理,如果每個節點都保存了所有隊列的完整信息,那么非查詢操作的性能就會很慢,就會需要更多的網絡帶寬和磁盤負載來存儲這些數據。
為了能兼顧性能和穩定性,RabbitMQ 集群的節點分為兩種類型,即磁盤節點和內存節點,對于磁盤節點來說顯然它的優勢就是穩定,可以把相關數據保存下來,若 RabbitMQ 因為意外情況宕機,重啟之后保證了數據不丟失;而內存節點的優勢是快,因為是在內存中進行數據交換和操作,因此性能比磁盤節點要高出很多倍。
如果是單個 RabbitMQ 那么就必須要求是磁盤節點,否則當 RabbitMQ 服務器重啟之后所有的數據都會丟失,這樣顯然是不能接受的。在 RabbitMQ 的集群中,至少需要一個磁盤節點,這樣至少能保證集群數據的相對可靠性。
如果集群中的某一個磁盤節點崩潰了,此時整個 RabbitMQ 服務也不會處于崩潰的狀態,不過部分操作會受影響,比如不能創建隊列、交換器、也不能添加用戶及修改用戶權限,更不能添加和刪除集群的節點等功能。
> 小貼士:對于 RabbitMQ 集群來說,我們啟動集群節點的順序應該是先啟動磁盤節點再啟動內存節點,而關閉的順序正好和啟動的順序相反,不然可能會導致 RabbitMQ 集群啟動失敗或者是數據丟失等異常問題。
* Kafka
Kafka 是 LinkedIn 公司開發的基于 ZooKeeper 的多分區、多副本的分布式消息系統,它于 2010 年貢獻給了 Apache 基金會,并且成為了 Apache 的頂級開源項目。其中 ZooKeeper 的作用是用來為 Kafka 提供集群元數據管理以及節點的選舉和發現等功能。
與 RabbitMQ 比較類似,一個典型的 Kafka 是由多個 Broker、多個生產者和消費者,以及 ZooKeeper 集群組成的,其中 Broker 可以理解為一個代理,Kafka 集群中的一臺服務器稱之為一個 Broker,其組成框架圖如下所示:

* [ ] Kafka VS RabbitMQ
Kafka(2.0.0)和 RabbitMQ(3.6.10)的區別主要體現在以下幾點:
* Kafka 支持消息回溯,它可以根據 Offset(消息偏移量)、TimeStamp(時間戳)等維度進行消息回溯,而 RabbitMQ 并不支持消息回溯;
* Kafka 的消息消費是基于拉取數據的模式,也就是消費者主動向服務器端發送拉取消息請求,而 RabbitMQ 支持拉取數據模式和主動推送數據的模式,也就說 RabbitMQ 服務器會主動把消息推送給訂閱的消費者;
* 在相同配置下,Kafka 的吞吐量通常會比 RabbitMQ 高一到兩個級別,比如在單機模式下,RabbitMQ 的吞吐量大概是萬級別的處理能力,而 Kafka 則可以到達十萬甚至是百萬的吞吐級別;
* Kafka 從 0.11 版本就開始支持冪等性了,當然所謂的冪等性指的是對單個生產者在單分區上的單會話的冪等操作,但對于全局冪等性則還需要結合業務來處理,比如,消費者在消費完一條消息之后沒有來得及確認就發生異常了,等到恢復之后又得重新消費原來消費過的消息,類似這種情況,是無法在消息中間件層面來保證的,這個時候則需要引入更多的外部資源來保證全局冪等性,比如唯一的訂單號、消費之前先做去重判斷等;而 RabbitMQ 是沒有冪等性功能支持的;
* RabbitMQ 支持多租戶的功能,也就是常說的 Virtual Host(vhost),每一個 vhost 相當于一個獨立的小型 RabbitMQ 服務器,它們擁有自己獨立的交換器、消息隊列及綁定關系等,并且擁有自己獨立權限,而且多個 vhost 之間是絕對隔離的,但 Kafka 并不支持多租戶的功能。
Kafka 和 RabbitMQ 都支持分布式集群部署,并且都支持數據持久化和消息消費確認等 MQ 的核心功能,對于 MQ 的選型要結合自己團隊本身的情況,從性能、穩定性及二次開發的難易程度等維度來進行綜合的考量并選擇。
#### 小結
本課時我們講了 MQ 的常見使用場景,以及常見的 MQ 中間件(Redis、RabbitMQ、Kafka)及其優缺點分析;同時還了解了 MQ 的五大特點:先進先出、發布和訂閱的模式、持久化、分布式和消息確認等;接著講了 MQ 引入對系統可能帶來的風險;最后講了 MQ 在使用時需要注意的問題。希望本課時對你整體了解 MQ 系統有所幫助。
- 前言
- 開篇詞
- 開篇詞:大廠技術面試“潛規則”
- 模塊一:Java 基礎
- 第01講:String 的特點是什么?它有哪些重要的方法?
- 第02講:HashMap 底層實現原理是什么?JDK8 做了哪些優化?
- 第03講:線程的狀態有哪些?它是如何工作的?
- 第04講:詳解 ThreadPoolExecutor 的參數含義及源碼執行流程?
- 第05講:synchronized 和 ReentrantLock 的實現原理是什么?它們有什么區別?
- 第06講:談談你對鎖的理解?如何手動模擬一個死鎖?
- 第07講:深克隆和淺克隆有什么區別?它的實現方式有哪些?
- 第08講:動態代理是如何實現的?JDK Proxy 和 CGLib 有什么區別?
- 第09講:如何實現本地緩存和分布式緩存?
- 第10講:如何手寫一個消息隊列和延遲消息隊列?
- 模塊二:熱門框架
- 第11講:底層源碼分析 Spring 的核心功能和執行流程?(上)
- 第12講:底層源碼分析 Spring 的核心功能和執行流程?(下)
- 第13講:MyBatis 使用了哪些設計模式?在源碼中是如何體現的?
- 第14講:SpringBoot 有哪些優點?它和 Spring 有什么區別?
- 第15講:MQ 有什么作用?你都用過哪些 MQ 中間件?
- 模塊三:數據庫相關
- 第16講:MySQL 的運行機制是什么?它有哪些引擎?
- 第17講:MySQL 的優化方案有哪些?
- 第18講:關系型數據和文檔型數據庫有什么區別?
- 第19講:Redis 的過期策略和內存淘汰機制有什么區別?
- 第20講:Redis 怎樣實現的分布式鎖?
- 第21講:Redis 中如何實現的消息隊列?實現的方式有幾種?
- 第22講:Redis 是如何實現高可用的?
- 模塊四:Java 進階
- 第23講:說一下 JVM 的內存布局和運行原理?
- 第24講:垃圾回收算法有哪些?
- 第25講:你用過哪些垃圾回收器?它們有什么區別?
- 第26講:生產環境如何排除和優化 JVM?
- 第27講:單例的實現方式有幾種?它們有什么優缺點?
- 第28講:你知道哪些設計模式?分別對應的應用場景有哪些?
- 第29講:紅黑樹和平衡二叉樹有什么區別?
- 第30講:你知道哪些算法?講一下它的內部實現過程?
- 模塊五:加分項
- 第31講:如何保證接口的冪等性?常見的實現方案有哪些?
- 第32講:TCP 為什么需要三次握手?
- 第33講:Nginx 的負載均衡模式有哪些?它的實現原理是什么?
- 第34講:Docker 有什么優點?使用時需要注意什么問題?
- 彩蛋
- 彩蛋:如何提高面試成功率?