上一篇文章《互聯網面試必殺:如何保證消息中間件全鏈路數據100%不丟失:第一篇》,咱們初步介紹了以前制定的那些消息中間件數據不丟失的技術方案遺留的問題。html
一個最大的問題,就是生產者投遞出去的消息,可能會丟失。java
丟失的緣由有不少,好比消息在網絡傳輸到一半的時候由於網絡故障就丟了,或者是消息投遞到MQ的內存時,MQ突發故障宕機致使消息就丟失了。面試
針對這種生產者投遞數據丟失的問題,RabbitMQ其實是提供了一些機制的。服務器
好比,有一種重量級的機制,就是事務消息機制。採用類事務的機制把消息投遞到MQ,能夠保證消息不丟失,可是性能極差,通過測試性能會呈現幾百倍的降低。微信
因此說如今通常是不會用這種過於重量級的機制,而是會用輕量級的confirm機制。網絡
可是咱們這篇文章還不能直接講解生產者保證消息不丟失的confirm機制,由於這種confirm機制其實是採用了相似消費者的ack機制來實現的。架構
因此,要深刻理解confirm機制,咱們得先從這篇文章開始,深刻的分析一下消費者手動ack機制保證消息不丟失的底層原理。性能
其實手動ack機制很是的簡單,必需要消費者確保本身處理完畢了一個消息,才能手動發送ack給MQ,MQ收到ack以後纔會刪除這個消息。測試
若是消費者還沒發送ack,本身就宕機了,此時MQ感知到他的宕機,就會從新投遞這條消息給其餘的消費者實例。3d
經過這種機制保證消費者實例宕機的時候,數據是不會丟失的。
再次提醒一下你們,若是還對手動ack機制不太熟悉的同窗,能夠回頭看一下以前的一篇文章:《扎心!線上服務宕機時,如何保證數據100%不丟失?》。而後這篇文章,咱們將繼續深刻探討一下ack機制的實現原理。
若是你寫好了一個消費者服務的代碼,讓他開始從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會重發消息給另一個倉儲服務實例,保證數據不丟。
以前就有同窗提出過這個問題,可是其實要搞清楚這個問題,其實不須要深刻的探索底層,只要本身大體的思考和推測一下就能夠了。
若是你的倉儲服務實例接收到了消息,可是沒有來得及調度發貨,沒有發送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%不丟失:第一篇
互聯網面試必殺:如何保證消息中間件全鏈路數據100%不丟失:第三篇
互聯網面試必殺:如何保證消息中間件全鏈路數據100%不丟失:第四篇
來源:【微信公衆號 - 石杉的架構筆記】