面試官心理分析
其實(shí)這是很常見的一個(gè)問題,這倆問題基本可以連起來問。既然是消費(fèi)消息,那肯定要考慮會(huì)不會(huì)重復(fù)消費(fèi)?能不能避免重復(fù)消費(fèi)?或者重復(fù)消費(fèi)了也別造成系統(tǒng)異常可以嗎?這個(gè)是 MQ 領(lǐng)域的基本問題,其實(shí)本質(zhì)上還是問你使用消息隊(duì)列如何保證冪等性,這個(gè)是你架構(gòu)里要考慮的一個(gè)問題。
面試題剖析
回答這個(gè)問題,首先你別聽到重復(fù)消息這個(gè)事兒,就一無所知吧,你先大概說一說可能會(huì)有哪些重復(fù)消費(fèi)的問題。
首先,比如 RabbitMQ、RocketMQ、Kafka,都有可能會(huì)出現(xiàn)消息重復(fù)消費(fèi)的問題,正常。因?yàn)檫@問題通常不是 MQ 自己保證的,是由我們開發(fā)來保證的。挑一個(gè) Kafka 來舉個(gè)例子,說說怎么重復(fù)消費(fèi)吧。
Kafka 實(shí)際上有個(gè) offset 的概念,就是每個(gè)消息寫進(jìn)去,都有一個(gè) offset,代表消息的序號,然后consumer 消費(fèi)了數(shù)據(jù)之后,每隔一段時(shí)間(定時(shí)定期),會(huì)把自己消費(fèi)過的消息的 offset 提交一下,表示“我已經(jīng)消費(fèi)過了,下次我要是重啟啥的,你就讓我繼續(xù)從上次消費(fèi)到的 offset 來繼續(xù)消費(fèi)吧”。
但是凡事總有意外,比如我們之前生產(chǎn)經(jīng)常遇到的,就是你有時(shí)候重啟系統(tǒng),看你怎么重啟了,如果碰到點(diǎn)著急的,直接 kill 進(jìn)程了,再重啟。這會(huì)導(dǎo)致 consumer 有些消息處理了,但是沒來得及提交 offset,尷尬了。重啟之后,少數(shù)消息會(huì)再次消費(fèi)一次。
舉個(gè)栗子。
有這么個(gè)場景。數(shù)據(jù) 1/2/3 依次進(jìn)入 kafka,kafka 會(huì)給這三條數(shù)據(jù)每條分配一個(gè) offset,代表這條數(shù)據(jù)的序號,我們就假設(shè)分配的 offset 依次是 152/153/154。消費(fèi)者從 kafka 去消費(fèi)的時(shí)候,也是按照這個(gè)順序去消費(fèi)。假如當(dāng)消費(fèi)者消費(fèi)了 offset=153 的這條數(shù)據(jù),剛準(zhǔn)備去提交 offset 到 zookeeper,此時(shí)消費(fèi)者進(jìn)程被重啟了。那么此時(shí)消費(fèi)過的數(shù)據(jù) 1/2 的 offset 并沒有提交,kafka 也就不知道你已經(jīng)消費(fèi)了 offset=153 這條數(shù)據(jù)。那么重啟之后,消費(fèi)者會(huì)找 kafka 說,嘿,哥兒們,你給我接著把上次我消費(fèi)到的那個(gè)地方后面的數(shù)據(jù)繼續(xù)給我傳遞過來。由于之前的 offset 沒有提交成功,那么數(shù)據(jù) 1/2 會(huì)再次傳過來,如果此時(shí)消費(fèi)者沒有去重的話,那么就會(huì)導(dǎo)致重復(fù)消費(fèi)。

如果消費(fèi)者干的事兒是拿一條數(shù)據(jù)就往數(shù)據(jù)庫里寫一條,會(huì)導(dǎo)致說,你可能就把數(shù)據(jù) 1/2 在數(shù)據(jù)庫里插入了 2 次,那么數(shù)據(jù)就錯(cuò)啦。
其實(shí)重復(fù)消費(fèi)不可怕,可怕的是你沒考慮到重復(fù)消費(fèi)之后,怎么保證冪等性。
舉個(gè)例子吧。假設(shè)你有個(gè)系統(tǒng),消費(fèi)一條消息就往數(shù)據(jù)庫里插入一條數(shù)據(jù),要是你一個(gè)消息重復(fù)兩次,你不就插入了兩條,這數(shù)據(jù)不就錯(cuò)了?但是你要是消費(fèi)到第二次的時(shí)候,自己判斷一下是否已經(jīng)消費(fèi)過了,若是就直接扔了,這樣不就保留了一條數(shù)據(jù),從而保證了數(shù)據(jù)的正確性。
一條數(shù)據(jù)重復(fù)出現(xiàn)兩次,數(shù)據(jù)庫里就只有一條數(shù)據(jù),這就保證了系統(tǒng)的冪等性。
冪等性,通俗點(diǎn)說,就一個(gè)數(shù)據(jù),或者一個(gè)請求,給你重復(fù)來多次,你得確保對應(yīng)的數(shù)據(jù)是不會(huì)改變的,不能出錯(cuò)。
所以第二個(gè)問題來了,怎么保證消息隊(duì)列消費(fèi)的冪等性?
其實(shí)還是得結(jié)合業(yè)務(wù)來思考,我這里給幾個(gè)思路:
????????????比如你拿個(gè)數(shù)據(jù)要寫庫,你先根據(jù)主鍵查一下,如果這數(shù)據(jù)都有了,你就別插入了,update 一下好吧。
? ? ? ? ? ? ?比如你是寫 Redis,那沒問題了,反正每次都是 set,天然冪等性。
????????????比如你不是上面兩個(gè)場景,那做的稍微復(fù)雜一點(diǎn),你需要讓生產(chǎn)者發(fā)送每條數(shù)據(jù)的時(shí)候,里面加一個(gè)全局唯一的 id,類似訂單 id 之類的東西,然后你這里消費(fèi)到了之后,先根據(jù)這個(gè) id 去比如Redis 里查一下,之前消費(fèi)過嗎?如果沒有消費(fèi)過,你就處理,然后這個(gè) id 寫 Redis。如果消費(fèi)過了,那你就別處理了,保證別重復(fù)處理相同的消息即可。
比如基于數(shù)據(jù)庫的唯一鍵來保證重復(fù)數(shù)據(jù)不會(huì)重復(fù)插入多條。因?yàn)橛形ㄒ绘I約束了,重復(fù)數(shù)據(jù)插入只會(huì)報(bào)錯(cuò),不會(huì)導(dǎo)致數(shù)據(jù)庫中出現(xiàn)臟數(shù)據(jù)。

當(dāng)然,如何保證 MQ 的消費(fèi)是冪等性的,需要結(jié)合具體的業(yè)務(wù)來看。