面試官心理分析
這個(gè)是肯定的贾富,用 MQ 有個(gè)基本原則歉眷,就是數(shù)據(jù)不能多一條,也不能少一條颤枪,不能多汗捡,就是前面說(shuō)的重復(fù)消費(fèi)和冪等性問(wèn)題。不能少畏纲,就是說(shuō)這數(shù)據(jù)別搞丟了扇住。那這個(gè)問(wèn)題你必須得考慮一下。
如果說(shuō)你這個(gè)是用 MQ 來(lái)傳遞非常核心的消息盗胀,比如說(shuō)計(jì)費(fèi)艘蹋、扣費(fèi)的一些消息,那必須確保這個(gè) MQ 傳遞過(guò)程中絕對(duì)不會(huì)把計(jì)費(fèi)消息給弄丟读整。
面試題剖析
數(shù)據(jù)的丟失問(wèn)題簿训,可能出現(xiàn)在生產(chǎn)者、MQ米间、消費(fèi)者中强品,咱們從 RabbitMQ 和 Kafka 分別來(lái)分析一下吧。
生產(chǎn)者弄丟了數(shù)據(jù)
生產(chǎn)者將數(shù)據(jù)發(fā)送到 RabbitMQ 的時(shí)候屈糊,可能數(shù)據(jù)就在半路給搞丟了的榛,因?yàn)榫W(wǎng)絡(luò)問(wèn)題啥的,都有可能逻锐。
此時(shí)可以選擇用 RabbitMQ 提供的事務(wù)功能夫晌,就是生產(chǎn)者發(fā)送數(shù)據(jù)之前開啟 RabbitMQ 事務(wù)
channel.txSelect,然后發(fā)送消息昧诱,如果消息沒(méi)有成功被 RabbitMQ 接收到晓淀,那么生產(chǎn)者會(huì)收到異常報(bào)錯(cuò),此時(shí)就可以回滾事務(wù) channel.txRollback盏档,然后重試發(fā)送消息凶掰;如果收到了消息,那么可以提交事務(wù)channel.txCommit。
// 開啟事務(wù)
channel.txSelecttry {
// 這里發(fā)送消息
} catch (Exception e) {
channel.txRollback
// 這里再次重發(fā)這條消息
}
// 提交事務(wù)
channel.txCommit
但是問(wèn)題是懦窘,RabbitMQ 事務(wù)機(jī)制(同步)一搞前翎,基本上吞吐量會(huì)下來(lái),因?yàn)樘男阅堋?/b>
所以一般來(lái)說(shuō)畅涂,如果你要確保說(shuō)寫 RabbitMQ 的消息別丟港华,可以開啟 confirm 模式,在生產(chǎn)者那里設(shè)置開啟 confirm 模式之后午衰,你每次寫的消息都會(huì)分配一個(gè)唯一的 id立宜,然后如果寫入了 RabbitMQ 中,RabbitMQ 會(huì)給你回傳一個(gè) ack 消息苇经,告訴你說(shuō)這個(gè)消息 ok 了赘理。如果 RabbitMQ 沒(méi)能處理這個(gè)消息,會(huì)回調(diào)你的一個(gè) nack 接口扇单,告訴你這個(gè)消息接收失敗商模,你可以重試。而且你可以結(jié)合這個(gè)機(jī)制自己在內(nèi)存里維護(hù)每個(gè)消息 id 的狀態(tài)蜘澜,如果超過(guò)一定時(shí)間還沒(méi)接收到這個(gè)消息的回調(diào)施流,那么你可以重發(fā)。
事務(wù)機(jī)制和 confirm 機(jī)制最大的不同在于鄙信,事務(wù)機(jī)制是同步的瞪醋,你提交一個(gè)事務(wù)之后會(huì)阻塞在那兒,但是 confirm 機(jī)制是異步的装诡,你發(fā)送個(gè)消息之后就可以發(fā)送下一個(gè)消息银受,然后那個(gè)消息 RabbitMQ 接收了之后會(huì)異步回調(diào)你的一個(gè)接口通知你這個(gè)消息接收到了。
所以一般在生產(chǎn)者這塊避免數(shù)據(jù)丟失鸦采,都是用 confirm 機(jī)制的宾巍。
RabbitMQ 弄丟了數(shù)據(jù)
就是 RabbitMQ 自己弄丟了數(shù)據(jù),這個(gè)你必須開啟 RabbitMQ 的持久化渔伯,就是消息寫入之后會(huì)持久化到磁盤顶霞,哪怕是 RabbitMQ 自己掛了,恢復(fù)之后會(huì)自動(dòng)讀取之前存儲(chǔ)的數(shù)據(jù)锣吼,一般數(shù)據(jù)不會(huì)丟选浑。除非極其罕見的是,RabbitMQ 還沒(méi)持久化玄叠,自己就掛了古徒,可能導(dǎo)致少量數(shù)據(jù)丟失,但是這個(gè)概率較小读恃。
設(shè)置持久化有兩個(gè)步驟:
????????????????創(chuàng)建 queue 的時(shí)候?qū)⑵湓O(shè)置為持久化? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 這樣就可以保證 RabbitMQ 持久化 queue 的元數(shù)據(jù)描函,但是它是不會(huì)持久化 queue 里的數(shù)據(jù)的崎苗。? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 第二個(gè)是發(fā)送消息的時(shí)候?qū)⑾⒌?deliveryMode 設(shè)置為 2就是將消息設(shè)置為持久化的,此時(shí) RabbitMQ 就會(huì)將消息持久化到磁盤上去舀寓。
必須要同時(shí)設(shè)置這兩個(gè)持久化才行,RabbitMQ 哪怕是掛了肌蜻,再次重啟互墓,也會(huì)從磁盤上重啟恢復(fù) queue,恢復(fù)這個(gè) queue 里的數(shù)據(jù)蒋搜。
注意篡撵,哪怕是你給 RabbitMQ 開啟了持久化機(jī)制,也有一種可能豆挽,就是這個(gè)消息寫到了 RabbitMQ 中育谬,但是還沒(méi)來(lái)得及持久化到磁盤上,結(jié)果不巧帮哈,此時(shí) RabbitMQ 掛了膛檀,就會(huì)導(dǎo)致內(nèi)存里的一點(diǎn)點(diǎn)數(shù)據(jù)丟失。
所以娘侍,持久化可以跟生產(chǎn)者那邊的 confirm 機(jī)制配合起來(lái)咖刃,只有消息被持久化到磁盤之后,才會(huì)通知生產(chǎn)者 ack 了憾筏,所以哪怕是在持久化到磁盤之前嚎杨,RabbitMQ 掛了,數(shù)據(jù)丟了氧腰,生產(chǎn)者收不到 ack枫浙,你也是可以自己重發(fā)的。
消費(fèi)端弄丟了數(shù)據(jù)
RabbitMQ 如果丟失了數(shù)據(jù)古拴,主要是因?yàn)槟阆M(fèi)的時(shí)候箩帚,剛消費(fèi)到,還沒(méi)處理斤富,結(jié)果進(jìn)程掛了膏潮,比如重啟了,那么就尷尬了满力,RabbitMQ 認(rèn)為你都消費(fèi)了焕参,這數(shù)據(jù)就丟了。
這個(gè)時(shí)候得用 RabbitMQ 提供的 ack 機(jī)制油额,簡(jiǎn)單來(lái)說(shuō)叠纷,就是你必須關(guān)閉 RabbitMQ 的自動(dòng) ack,可以通過(guò)一個(gè) api 來(lái)調(diào)用就行潦嘶,然后每次你自己代碼里確保處理完的時(shí)候涩嚣,再在程序里 ack 一把。這樣的話,如果你還沒(méi)處理完航厚,不就沒(méi)有 ack 了顷歌?那 RabbitMQ 就認(rèn)為你還沒(méi)處理完,這個(gè)時(shí)候 RabbitMQ 會(huì)把這個(gè)消費(fèi)分配給別的 consumer 去處理幔睬,消息是不會(huì)丟的眯漩。
Kafka
消費(fèi)端弄丟了數(shù)據(jù)
唯一可能導(dǎo)致消費(fèi)者弄丟數(shù)據(jù)的情況,就是說(shuō)麻顶,你消費(fèi)到了這個(gè)消息赦抖,然后消費(fèi)者那邊自動(dòng)提交了 offset,讓 Kafka 以為你已經(jīng)消費(fèi)好了這個(gè)消息辅肾,但其實(shí)你才剛準(zhǔn)備處理這個(gè)消息队萤,你還沒(méi)處理,你自己就掛了矫钓,此時(shí)這條消息就丟咯要尔。
這不是跟 RabbitMQ 差不多嗎,大家都知道 Kafka 會(huì)自動(dòng)提交 offset份汗,那么只要關(guān)閉自動(dòng)提交 offset盈电,在處理完之后自己手動(dòng)提交 offset,就可以保證數(shù)據(jù)不會(huì)丟杯活。但是此時(shí)確實(shí)還是可能會(huì)有重復(fù)消費(fèi)匆帚,比如你剛處理完,還沒(méi)提交 offset旁钧,結(jié)果自己掛了吸重,此時(shí)肯定會(huì)重復(fù)消費(fèi)一次,自己保證冪等性就好了歪今。
生產(chǎn)環(huán)境碰到的一個(gè)問(wèn)題嚎幸,就是說(shuō)我們的 Kafka 消費(fèi)者消費(fèi)到了數(shù)據(jù)之后是寫到一個(gè)內(nèi)存的 queue 里先緩沖一下,結(jié)果有的時(shí)候寄猩,你剛把消息寫入內(nèi)存 queue嫉晶,然后消費(fèi)者會(huì)自動(dòng)提交 offset。然后此時(shí)我們重啟了系統(tǒng)田篇,就會(huì)導(dǎo)致內(nèi)存 queue 里還沒(méi)來(lái)得及處理的數(shù)據(jù)就丟失了替废。
Kafka 弄丟了數(shù)據(jù)
這塊比較常見的一個(gè)場(chǎng)景,就是 Kafka 某個(gè) broker 宕機(jī)泊柬,然后重新選舉 partition 的 leader椎镣。大家想想,要是此時(shí)其他的 follower 剛好還有些數(shù)據(jù)沒(méi)有同步兽赁,結(jié)果此時(shí) leader 掛了状答,然后選舉某個(gè) follower成 leader 之后冷守,不就少了一些數(shù)據(jù)?這就丟了一些數(shù)據(jù)啊惊科。
生產(chǎn)環(huán)境也遇到過(guò)拍摇,我們也是,之前 Kafka 的 leader 機(jī)器宕機(jī)了馆截,將 follower 切換為 leader 之后授翻,就會(huì)發(fā)現(xiàn)說(shuō)這個(gè)數(shù)據(jù)就丟了。
所以此時(shí)一般是要求起碼設(shè)置如下 4 個(gè)參數(shù):
? 給 topic 設(shè)置 replication.factor 參數(shù):這個(gè)值必須大于 1孙咪,要求每個(gè) partition 必須有至少 2 個(gè)副本。
在 Kafka 服務(wù)端設(shè)置 min.insync.replicas 參數(shù):這個(gè)值必須大于 1巡语,這個(gè)是要求一個(gè) leader至少感知到有至少一個(gè) follower 還跟自己保持聯(lián)系翎蹈,沒(méi)掉隊(duì),這樣才能確保 leader 掛了還有一個(gè) follower 吧男公。
? 在 producer 端設(shè)置 acks=all:這個(gè)是要求每條數(shù)據(jù)荤堪,必須是寫入所有 replica 之后,才能認(rèn)為是寫成功了枢赔。
? 在 producer 端設(shè)置 retries=MAX(很大很大很大的一個(gè)值澄阳,無(wú)限次重試的意思):這個(gè)是要求一旦寫入失敗,就無(wú)限重試踏拜,卡在這里了碎赢。
我們生產(chǎn)環(huán)境就是按照上述要求配置的,這樣配置之后速梗,至少在 Kafka broker 端就可以保證在 leader 所在 broker 發(fā)生故障肮塞,進(jìn)行 leader 切換時(shí),數(shù)據(jù)不會(huì)丟失姻锁。
生產(chǎn)者會(huì)不會(huì)弄丟數(shù)據(jù)枕赵?
如果按照上述的思路設(shè)置了 acks=all,一定不會(huì)丟位隶,要求是拷窜,你的 leader 接收到消息,所有的 follower都同步到了消息之后涧黄,才認(rèn)為本次寫成功了篮昧。如果沒(méi)滿足這個(gè)條件,生產(chǎn)者會(huì)自動(dòng)不斷的重試弓熏,重試無(wú)限次恋谭。