天天看點

如何保障消息中間件 100% 消息投遞成功?如何保證消息幂等性?

一、前言

二、分析問題

三、持久化

四、confirm機制

五、消息提前持久化 + 定時任務

六、幂等含義

6.1、為什麼要有幂等這種場景?

6.2、樂觀鎖方案

6.3、唯一id + 指紋碼

6.4、redis原子操作

如何保障消息中間件 100% 消息投遞成功?如何保證消息幂等性?

我們小夥伴應該都聽說夠消息中間件mq,如:rabbitmq,rocketmq,kafka等。引入中間件的好處可以起到抗高并發,削峰,業務解耦的作用。

如何保障消息中間件 100% 消息投遞成功?如何保證消息幂等性?

如上圖:

(1)訂單服務投遞消息給mq中間件(2)物流服務監聽mq中間件消息,進而進行消費

我們這篇文章讨論一下,如何保障訂單服務把消息成功投遞給mq中間件,以rabbitmq舉例。

小夥伴們對此會有些疑問,訂單服務發起消息服務,傳回成功不就成功了嗎?如下面的僞代碼:

如何保障消息中間件 100% 消息投遞成功?如何保證消息幂等性?

上面代碼中,一般發送消息就是這麼寫的,小夥伴們覺得有什麼問題嗎?

下邊說一個場景,如果mq伺服器突然當機了會出現什麼情況?是不是我們訂單服務發過去的消息全部沒有了嗎?是的,一般mq中間件為了提高系統的吞吐量會把消息儲存在記憶體中,如果不作其他處理,mq伺服器一旦當機,消息将全部丢失 。這個是業務不允許的,造成很大的影響。

有經驗的小夥伴會說,我知道一個方法就是把消息持久化,rabbitmq中發消息的時候會有個durable參數可以設定,設定為true,就會持久化。

如何保障消息中間件 100% 消息投遞成功?如何保證消息幂等性?

這樣的話mq伺服器即使當機,重新開機後磁盤檔案中有消息的存儲,這樣就不會丢失了吧 。是的這樣就一定機率的保障了消息不丢失。

但還會有個場景,就是消息剛剛儲存到mq記憶體中,但還沒有來得及更新到磁盤檔案中,突然當機了 。(我靠,這個時間這麼短,也會出現,機率太低了吧),這個場景在持續的大量消息投遞的過程中,會很常見。

那怎麼辦?我們如何作才能保障一定會持久化到磁盤上面呢?

上面問題出現在,沒有人告訴我們持久化是否成功。好在很多mq有回調通知的特性,rabbitmq就有confirm機制來通知我們是否持久化成功?

如何保障消息中間件 100% 消息投遞成功?如何保證消息幂等性?

confirm機制的原理:

(1)消息生産者把消息發送給mq,如果接收成功,mq會傳回一個ack消息給生産者;(2)如果消息接收不成功,mq會傳回一個nack消息給生産者;
如何保障消息中間件 100% 消息投遞成功?如何保證消息幂等性?

上面的僞代碼,有兩個處理消息方式,就是ack回調和nack回調。

這樣是不是就可以保障100%消息不丢失了呢?

我們看一下confirm的機制,試想一下,如果我們生産者每發一條消息,都要mq持久化到磁盤中,然後再發起ack或nack的回調。這樣的話是不是我們mq的吞吐量很不高,因為每次都要把消息持久化到磁盤中。 寫入磁盤這個動作是很慢的。這個在高并發場景下是不能夠接受的,吞吐量太低了。

是以mq持久化磁盤真實的實作,是通過異步調用處理的,他是有一定的機制,如:等到有幾千條消息的時候,會一次性的刷盤到磁盤上面。而不是每來一條消息,就刷盤一次。

是以comfirm機制其實是一個異步監聽的機制 ,是為了保證系統的高吞吐量 ,這樣就導緻了還是不能夠100%保障消息不丢失,因為即使加上了confirm機制,消息在mq記憶體中還沒有刷盤到磁盤就當機了,還是沒法處理。

說了這麼多,還是沒法確定,那怎麼辦呢???

其實本質的原因是無法确定是否持久化?那我們是不是可以自己讓消息持久化呢?答案是可以的,我們的方案再一步的演化。

如何保障消息中間件 100% 消息投遞成功?如何保證消息幂等性?

上圖流程:

(1)訂單服務生産者再投遞消息之前,先把消息持久化到redis或db中,建議redis,高性能。消息的狀态為發送中。(2)confirm機制監聽消息是否發送成功?如ack成功消息,删除redis中此消息。(3)如果nack不成功的消息,這個可以根據自身的業務選擇是否重發此消息。也可以删除此消息,由自己的業務決定。(4)這邊加了個定時任務,來拉取隔一定時間了,消息狀态還是為發送中的,這個狀态就表明,訂單服務是沒有收到ack成功消息。(5)定時任務會作補償性的投遞消息。這個時候如果mq回調ack成功接收了,再把redis中此消息删除。

這樣的機制其實就是一個補償機制 ,我不管mq有沒有真正的接收到,隻要我的redis中的消息狀态也是為【發送中】,就表示此消息沒有正确成功投遞。再啟動定時任務去監控,發起補償投遞。

當然定時任務那邊我們還可以加上一個補償的次數,如果大于3次,還是沒有收到ack消息 ,那就直接把消息的狀态設定為【失敗】,由人工去排查到底是為什麼?

這樣的話方案就比較完美了,保障了100%的消息不丢失 (當然不包含磁盤也壞了,可以做主從方案)。

不過這樣的方案,就會有可能發送多次相同的消息 ,很有可能mq已經收到了消息,就是ack消息回調時出現網絡故障,沒有讓生産者收到。

那就要要求消費者一定在消費的時候保障幂等性!

我們先了解一下什麼叫幂等?在分布式應用中,幂等是非常重要的,也就是相同條件下對一個業務的操作,不管操作多少次,結果都是一樣。

為什麼要有幂等這種場景?因為在大的系統中,都是分布式部署,如:訂單業務 和 庫存業務有可能都是獨立部署的,都是單獨的服務。使用者下訂單,會調用到訂單服務和庫存服務。

如何保障消息中間件 100% 消息投遞成功?如何保證消息幂等性?

因為分布式部署,很有可能在調用庫存服務時,因為網絡等原因,訂單服務調用失敗,但其實庫存服務已經處理完成,隻是傳回給訂單服務處理結果時出現了異常。這個時候一般系統會作補償方案,也就是訂單服務再此放起庫存服務的調用,庫存減1。

如何保障消息中間件 100% 消息投遞成功?如何保證消息幂等性?

這樣就出現了問題,其實上一次調用已經減了1,隻是訂單服務沒有收到處理結果。現在又調用一次,又要減1,這樣就不符合業務了,多扣了。

幂等這個概念就是,不管庫存服務在相同條件下調用幾次,處理結果都一樣。這樣才能保證補償方案的可行性。

借鑒資料庫的樂觀鎖機制,如:

如何保障消息中間件 100% 消息投遞成功?如何保證消息幂等性?

根據version版本,也就是在操作庫存前先擷取目前商品的version版本号,然後操作的時候帶上此version号。我們梳理下,我們第一次操作庫存時,得到version為1,調用庫存服務version變成了2;但傳回給訂單服務出現了問題,訂單服務又一次發起調用庫存服務,當訂單服務傳如的version還是1,再執行上面的sql語句時,就不會執行;因為version已經變為2了,where條件就不成立。這樣就保證了不管調用幾次,隻會真正的處理一次。

原理就是利用資料庫主鍵去重,業務完成後插入主鍵辨別

如何保障消息中間件 100% 消息投遞成功?如何保證消息幂等性?

唯一id就是業務表的唯一的主鍵,如商品id

指紋碼就是為了差別每次正常操作的碼,每次操作時生成指紋碼;可以用時間戳+業務編号的方式。

上面的sql語句:

傳回如果為0 表示沒有操作過,那業務操作後就可以insert into t_check(唯一id+指紋碼)

傳回如果大于0 表示操作過,就直接傳回

好處:實作簡單

壞處:高并發下資料庫瓶頸

解決方案:根據id進行分庫分表進行算法路由

利用redis的原子操作,做個操作完成的标記。這個性能就比較好。但會遇到一些問題。

第一:我們是否需要把業務結果進行資料落庫,如果落庫,關鍵解決的問題時資料庫和redis操作如何做到原子性?

這個意思就是庫存減1了,但redis進行操作完成标記時,失敗了怎麼辦?也就是一定要保證落庫和redis 要麼一起成功,要麼一起失敗

第二:如果不進行落庫,那麼都存儲到緩存中,如何設定定時同步政策?

這個意思就是庫存減1,不落庫,直接先操作redis操作完成标記,然後由另外的同步服務進行庫存落庫,這個就是增加了系統複雜性,而且同步政策如何設定

以上我們結束了幂等相關的解決方案,以後文章中我們會重點介紹方案的實作。