互聯網面試必殺:如何保證消息中間件全鏈路數據100%不丟失:第二篇
前情提示
上一篇文章《互聯網面試必殺:如何保證消息中間件全鏈路數據100%不丟失:第一篇》,我們初步介紹了之前制定的那些消息中間件數據不丟失的技術方案遺留的問題。
一個最大的問題,就是生產者投遞出去的消息,可能會丟失。
丟失的原因有很多,比如消息在網絡傳輸到一半的時候因為網絡故障就丟了,或者是消息投遞到MQ的內存時,MQ突發故障宕機導致消息就丟失了。
針對這種生產者投遞數據丟失的問題,RabbitMQ實際上是提供了一些機制的。
比如,有一種重量級的機制,就是事務消息機制。采用類事務的機制把消息投遞到MQ,可以保證消息不丟失,但是性能極差,經過測試性能會呈現幾百倍的下降。
所以說現在一般是不會用這種過於重量級的機制,而是會用輕量級的confirm機制
但是我們這篇文章還不能直接講解生產者保證消息不丟失的confirm機制,因為這種confirm機制實際上是采用了類似消費者的ack機制來實現的。
所以,要深入理解confirm機制,我們得先從這篇文章開始,深入的分析一下消費者手動ack機制保證消息不丟失的底層原理。
ack機制回顧
其實手動ack機制非常的簡單,必須要消費者確保自己處理完畢了一個消息,才能手動發送ack給MQ,MQ收到ack之後才會刪除這個消息。
如果消費者還沒發送ack,自己就宕機了,此時MQ感知到他的宕機,就會重新投遞這條消息給其他的消費者實例。
通過這種機制保證消費者實例宕機的時候,數據是不會丟失的。
再次提醒一下大家,如果還對手動ack機制不太熟悉的同學,可以回頭看一下之前的一篇文章:《紮心!線上服務宕機時,如何保證數據100%不丟失?》。然後這篇文章,我們將繼續深入探討一下ack機制的實現原理。
ack機制實現原理:delivery tag
如果你寫好了一個消費者服務的代碼,讓他開始從RabbitMQ消費數據,這時這個消費者服務實例就會自己註冊到RabbitMQ。
所以,RabbitMQ其實是知道有哪些消費者服務實例存在的。
大家看看下面的圖,直觀的感受一下:
接著,RabbitMQ就會通過自己內部的一個“basic.delivery”方法來投遞消息到倉儲服務裏去,讓他消費消息。
投遞的時候,會給這次消息的投遞帶上一個重要的東西,就是“delivery tag”,你可以認為是本次消息投遞的一個唯一標識。
這個所謂的唯一標識,有點類似於一個ID,比如說消息本次投遞到一個倉儲服務實例的唯一ID。通過這個唯一ID,我們就可以定位一次消息投遞。
所以這個delivery tag機制不要看很簡單,實際上他是後面要說的很多機制的核心基礎。
而且這裏要給大家強調另外一個概念,就是每個消費者從RabbitMQ獲取消息的時候,都是通過一個channel的概念來進行的。
大家回看一下下面的消費者代碼片段,我們必須是先對指定機器上部署的RabbitMQ建立連接,然後通過這個連接獲取一個channel。
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
而且如果大家還有點印象的話,我們在倉儲服務裏對消息的消費、ack等操作,全部都是基於這個channel來進行的,channel又有點類似於是我們跟RabbitMQ進行通信的這麽一個句柄,比如看看下面的代碼:
另外這裏提一句:之前寫那篇文章講解手動ack保證數據不丟失的時候,有很多人提出疑問:為什麽上面代碼裏直接是try finally,如果代碼有異常,那還是會直接執行finally裏的手動ack?其實很簡單,自己加上catch就可以了。
好的,咱們繼續。你大概可以認為這個channel就是進行數據傳輸的一個管道吧。對於每個channel而言,一個“delivery tag”就可以唯一的標識一次消息投遞,這個delivery tag大致而言就是一個不斷增長的數字。
大家來看看下面的圖,相信會很好理解的:
如果采用手動ack機制,實際上倉儲服務每次消費了一條消息,處理完畢完成調度發貨之後,就會發送一個ack消息給RabbitMQ服務器,這個ack消息是會帶上自己本次消息的delivery tag的。
咱們看看下面的ack代碼,是不是帶上了一個delivery tag?
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
然後,RabbitMQ根據哪個channel的哪個delivery tag,不就可以唯一定位一次消息投遞了?
接下來就可以對那條消息刪除,標識為已經處理完畢。
這裏大家必須註意的一點,就是delivery tag僅僅在一個channel內部是唯一標識消息投遞的。
所以說,你ack一條消息的時候,必須是通過接受這條消息的同一個channel來進行。
大家看看下面的圖,直觀的感受一下。
其實這裏還有一個很重要的點,就是我們可以設置一個參數,然後就批量的發送ack消息給RabbitMQ,這樣可以提升整體的性能和吞吐量。
比如下面那行代碼,把第二個參數設置為true就可以了。
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), true);
看到這裏,大家應該對這個ack機制的底層原理有了稍微進一步的認識了。起碼是知道delivery tag是啥東西了,他是實現ack的一個底層機制。
然後,我們再來簡單回顧一下自動ack、手動ack的區別。
實際上默認用自動ack,是非常簡單的。RabbitMQ只要投遞一個消息出去給倉儲服務,那麽他立馬就把這個消息給標記為刪除,因為他是不管倉儲服務到底接收到沒有,處理完沒有的。
所以這種情況下,性能很好,但是數據容易丟失。
如果手動ack,那麽就是必須等倉儲服務完成商品調度發貨以後,才會手動發送ack給RabbitMQ,此時RabbitMQ才會認為消息處理完畢,然後才會標記消息為刪除。
這樣在發送ack之前,倉儲服務宕機,RabbitMQ會重發消息給另外一個倉儲服務實例,保證數據不丟。
RabbitMQ如何感知到倉儲服務實例宕機
之前就有同學提出過這個問題,但是其實要搞清楚這個問題,其實不需要深入的探索底層,只要自己大致的思考和推測一下就可以了。
如果你的倉儲服務實例接收到了消息,但是沒有來得及調度發貨,沒有發送ack,此時他宕機了。
我們想一想就知道,RabbitMQ之前既然收到了倉儲服務實例的註冊,因此他們之間必然是建立有某種聯系的。
一旦某個倉儲服務實例宕機,那麽RabbitMQ就必然會感知到他的宕機,而且對發送給他的還沒ack的消息,都發送給其他倉儲服務實例。
所以這個問題以後有機會我們可以深入聊一聊,在這裏,大家其實先建立起來這種認識即可。
我們再回頭看看下面的架構圖:
倉儲服務處理失敗時的消息重發
首先,我們來看看下面一段代碼:
假如說某個倉儲服務實例處理某個消息失敗了,此時會進入catch代碼塊,那麽此時我們怎麽辦呢?難道還是直接ack消息嗎?
當然不是了,你要是還是ack,那會導致消息被刪除,但是實際沒有完成調度發貨。
這樣的話,數據不是還是丟失了嗎?因此,合理的方式是使用nack操作。
就是通知RabbitMQ自己沒處理成功消息,然後讓RabbitMQ將這個消息再次投遞給其他的倉儲服務實例嘗試去完成調度發貨的任務。
我們只要在catch代碼塊裏加入下面的代碼即可:
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), true);
註意上面第二個參數是true,意思就是讓RabbitMQ把這條消息重新投遞給其他的倉儲服務實例,因為自己沒處理成功。
你要是設置為false的話,就會導致RabbitMQ知道你處理失敗,但是還是刪除這條消息,這是不對的。
同樣,我們還是來一張圖,大家一起來感受一下:
階段總結
這篇文章對之前的ack機制做了進一步的分析,包括底層的delivery tag機制,以及消息處理失敗時的消息重發。
通過ack機制、消息重發等這套機制的落地實現,就可以保證一個消費者服務自身突然宕機、消息處理失敗等場景下,都不會丟失數據。
來源:【微信公眾號 - 石杉的架構筆記】
互聯網面試必殺:如何保證消息中間件全鏈路數據100%不丟失:第二篇