您的位置:首頁(yè) > 軟件教程 > 教程 > 這就叫“面試造火箭,工作擰螺絲!”

這就叫“面試造火箭,工作擰螺絲!”

來(lái)源:好特整理 | 時(shí)間:2024-06-24 15:49:23 | 閱讀:135 |  標簽: 擰螺絲 面試 火箭 工作   | 分享到:

你好呀,我是歪歪。 我想再討論一下上次的這篇文章《哎,被這個(gè)叫做at least once的玩意坑麻了》 因為有些朋友看完之后再評論區給出了自己的思考,也有朋友和我私聊,分享了自己的看法,我覺(jué)得有些想法很好,所以我決定一魚(yú)兩吃,再聊聊這個(gè)問(wèn)題。 假設,我們是一場(chǎng)面試,面試官給你拋出了這樣一個(gè)問(wèn)題:

你好呀,我是歪歪。

我想再討論一下上次的這篇文章 《哎,被這個(gè)叫做at least once的玩意坑麻了》

因為有些朋友看完之后再評論區給出了自己的思考,也有朋友和我私聊,分享了自己的看法,我覺(jué)得有些想法很好,所以我決定一魚(yú)兩吃,再聊聊這個(gè)問(wèn)題。

假設,我們是一場(chǎng)面試,面試官給你拋出了這樣一個(gè)問(wèn)題:

如果一個(gè)消費隊列由于某些原因,對于某個(gè)消息發(fā)起了兩次。導致一樣的數據落庫兩條,請問(wèn)你會(huì )怎么處理這個(gè)問(wèn)題?

這題你一拿到手上,應該就立馬能分析出是在問(wèn)如何實(shí)現一個(gè)冪等機制。

想著(zhù)這玩意我熟啊,張口就能給出方案:

業(yè)務(wù)消息?=?select(業(yè)務(wù)唯一流水號);
if(業(yè)務(wù)消息?==?null){
????save(業(yè)務(wù)消息);
}

面試官一聽(tīng),提示道:你這個(gè)方案在多線(xiàn)程的情況下會(huì )不會(huì )有什么問(wèn)題呢?

于是你的小腦瓜子立刻開(kāi)始轉了起來(lái):先查詢(xún),再判斷,最后保存。

如果兩個(gè)線(xiàn)程同時(shí)過(guò)來(lái),都查不到數據,那么就能都走到保存的邏輯里面去,確實(shí)攔不住。

于是你扣了一下腦殼,想起了你上家公司針對這個(gè)問(wèn)題,就是在數據庫的表結構里面,對業(yè)務(wù)唯一流水號做了唯一索引,所以不會(huì )出現重復插入的情況。

然后你給出了“加唯一索引”的方案,準備絕殺這個(gè)問(wèn)題。

沒(méi)想到面試官非常不懂事,還在繼續追問(wèn):我想盡量不要讓程序拋出異常,還有沒(méi)有其他的方案呢?

Redis

你抱著(zhù)自己的左手,邊啃指甲邊思考:唯一索引是數據庫幫我們保證的邏輯,現在面試官這個(gè)老登不想讓我用數據庫來(lái)做這件事情。那就必須要控制在并發(fā)的場(chǎng)景下,只有一個(gè)請求能抵達數據庫。

這就叫“面試造火箭,工作擰螺絲!”

鎖!這不就是鎖干的事兒?jiǎn)幔?

于是你飛快的又想到了一個(gè)方法:

flag?=?redis(業(yè)務(wù)唯一流水號,過(guò)期時(shí)間);
if(flag){
????save(業(yè)務(wù)消息);????
}

可以利用業(yè)務(wù)唯一流水號結合 Redis 來(lái)做一個(gè)鎖,加鎖成功的請求才能走到 save 邏輯中。

這樣就能解決并發(fā)場(chǎng)景下,多個(gè)請求穿透到 save 邏輯這一步的問(wèn)題。

面試官聽(tīng)到你這個(gè)方案之后,立馬就啟動(dòng)了追問(wèn)技能:如果放 Redis 成功了,但是還沒(méi)來(lái)得及 save,服務(wù)重啟了。

這個(gè)請求理論上是應該能再次發(fā)起的,但是由于 Redis 鎖的存在,導致不會(huì )走到 save 的邏輯去,怎么辦呢?

于是你又扣了一下腦殼,想起你在上家公司的時(shí)候,好像也遇到過(guò)這個(gè)情況。

當時(shí)的解決方案就是人工介入,分析了一波數據,確認了這個(gè)消息確實(shí)應該被繼續處理,于是你找 DBA 幫忙刪除了 Redis 對應的 key,流程就通了。

然而這個(gè)回答面試官并不滿(mǎn)意:人工就顯得不優(yōu)雅了,要不再想想?

你又抱著(zhù)自己的右手,邊啃指甲邊思考:這個(gè)老登考慮的確實(shí)挺多的,感覺(jué)應該在一個(gè)很厲害的團隊,我得加把勁兒,再想想。

現在要人工介入的原因,是因為我們把第二次的請求攔截住并丟棄了。

如果不丟棄,那么理論上在“過(guò)期時(shí)間”到了,鎖被釋放后,第二次的請求拿到鎖,就能接著(zhù)往下走。

所以,這里需要在 Redis 這里加一個(gè)加鎖失敗則等待的邏輯:

flag?=?redis(業(yè)務(wù)唯一流水號,過(guò)期時(shí)間,獲取不到則等待);
if(flag){
????save(業(yè)務(wù)消息);????
}

但是你一看這個(gè)邏輯又不對了:由于有鎖等待的邏輯,那么如果兩個(gè)請求過(guò)來(lái),還是有可能會(huì )都放入到 Redis 里面,flag 都會(huì )為 true,那么 save 方法還是會(huì )走兩遍。

所以,還得在獲取鎖成功之后加上一個(gè)查詢(xún)數據庫的邏輯:

flag?=?redis(業(yè)務(wù)唯一流水號,過(guò)期時(shí)間,獲取不到則等待);
if(flag){
????業(yè)務(wù)消息?=?select(業(yè)務(wù)唯一流水號);
????if(業(yè)務(wù)消息?==?null){
????????save(業(yè)務(wù)消息);????
????}
}else{
????//等待結束后還是未獲取到鎖,發(fā)送預警
????monitor(預警信息);
}

第一層的 Redis 相當于讓請求排隊,確保只有一個(gè)請求進(jìn)來(lái)。

第二層的 select 才是真正的防止重復的業(yè)務(wù)邏輯。

同時(shí),如果等待結束后還是未獲取到鎖,出現這種低概率情況,就預警出來(lái),人工兜底嘛,一旦人工介入,那就是能解決任何問(wèn)題。

你心想這波應該是穩了,應該是可以換題了。

然而面試官并不打算在這個(gè)回合上輕易放過(guò)你:這個(gè)方案確實(shí)是可以解決這個(gè)問(wèn)題,但是在技術(shù)實(shí)現上引入了 Redis 框架,如果我不使用 Redis,單純的靠 MySQL 呢?

回到 MySQL

聽(tīng)到這個(gè)問(wèn)題的時(shí)候你覺(jué)得不對啊,最開(kāi)始的時(shí)候不就是說(shuō)了“加唯一索引”就可以解決這個(gè)問(wèn)題嗎?

于是面試官補充了一下描述:

最開(kāi)始的加唯一索引是基于業(yè)務(wù)表來(lái)做的,如果出現問(wèn)題就讓其拋出主鍵沖突異常,這個(gè)方案確實(shí)是可以實(shí)現需求。但是我現在想讓你給我設計一個(gè)通用的技術(shù)組件,不需要基于某個(gè)具體的業(yè)務(wù)場(chǎng)景去設計。我想聽(tīng)聽(tīng)你的思路。

拿到新的題目,你開(kāi)始覺(jué)得這是***難,看著(zhù)面試官求知的眼神,你又開(kāi)始懷疑:這個(gè)老登不會(huì )是來(lái)套方案的吧?

看著(zhù)自己已經(jīng)被咬禿了的左右大拇指指甲,感覺(jué)自己的靈感和指甲一樣都光禿禿的。

開(kāi)始后悔前面幾個(gè)回合咬得太快了,原以為可以秒殺這個(gè)面試,沒(méi)想到面試官還在纏斗。你動(dòng)了使用必殺技來(lái)結束戰斗的念想。

于是從帽子的縫隙中插進(jìn)入一根指甲已經(jīng)禿了的手指,在差不多禿了的頭頂,用指腹畫(huà)圈,給自己頭皮按摩,醫生說(shuō)這樣的有助于毛囊發(fā)育,你想著(zhù)頭發(fā)還會(huì )長(cháng)出來(lái),就思如泉涌,這就是必殺技。

這就叫“面試造火箭,工作擰螺絲!”

你陷入了思考,Redis 在前面的方案中是為了防止有多條數據穿透到 save 方法中去,如果不讓用 Redis。MySQL 怎么實(shí)現類(lèi)似的效果呢?

也加鎖嗎?for update?

業(yè)務(wù)消息?=?select(業(yè)務(wù)唯一流水號);//select?***?for?update
if(業(yè)務(wù)消息?==?null){
????save(業(yè)務(wù)消息);????
}

這玩意一看上去就是性能就拉胯了,為了解決這個(gè)偶發(fā)的問(wèn)題,犧牲了接口的性能,這個(gè)路線(xiàn)就走的有點(diǎn)遠了。

而且這個(gè)上鎖的邏輯隱藏的有點(diǎn)深,容易留下后患,面試官肯定不會(huì )滿(mǎn)意的。

那還有什么辦法,能把 MySQL 當作鎖來(lái)用,確保并發(fā)情況下只有一個(gè)請求能穿過(guò)這個(gè)鎖呢?

那還是得靠唯一索引的約束才行。

但是這個(gè)唯一索引面試官不讓用業(yè)務(wù)表的,那就只能直接搞個(gè)“消息消費記錄表”,里面有個(gè)“消息唯一標識”的字段,這個(gè)字段是唯一索引。

這張表面試官問(wèn)起來(lái),我就說(shuō)這張表是完全獨立于業(yè)務(wù)的存在,只是為了解決消息冪等這個(gè)存粹的技術(shù)問(wèn)題而出現的,基于它,我們就可以設計出一個(gè)通用的技術(shù)組件,這樣應該說(shuō)的過(guò)去。

表有了,技術(shù)方法大概的雛形就有了。

然而你還不能開(kāi)始答題,現在思路還不是特別清晰,你要把方案捋清楚了再張口。

在不知不覺(jué)間,你的指腹已經(jīng)摩擦的有點(diǎn)麻木了,于是你換了一個(gè)手,穿過(guò)帽子,接著(zhù)按摩著(zhù)自己的頭頂。

這個(gè)表我怎么用呢?

if(保存數據到消息消費記錄表){//出現主鍵沖突就返回false
????save(業(yè)務(wù)消息);????
}

先校驗,再保存,非原子性,這樣肯定不行啊,

我們想想一個(gè)場(chǎng)景,如果保存數據到消息消費記錄表成功,還沒(méi)來(lái)得及 save(業(yè)務(wù)消息) ,服務(wù)重啟了,怎么辦?

所以為了保證原子性,我們可以加入事務(wù),把這兩步綁定到一起:

開(kāi)啟事務(wù);
if(保存數據到消息消費記錄表){//出現主鍵沖突就返回false
????save(業(yè)務(wù)消息);????
}
提交事務(wù);

這樣,如果保存數據到消息消費記錄表成功,還沒(méi)來(lái)得及 save(扣款信息) ,服務(wù)重啟,事務(wù)回滾,消息消費記錄表就不會(huì )真的插入成功。

而 MQ 沒(méi)有收到這個(gè)消息的回執,也會(huì )再次進(jìn)行投遞。

由于消息消費記錄表里沒(méi)有這個(gè)數據,所以會(huì )再次進(jìn)行消費。

現在你覺(jué)得似乎沒(méi)啥問(wèn)題了,剛想給面試官說(shuō)你這個(gè)思路,但是立馬又想到了另外一個(gè)問(wèn)題:通過(guò)引入事務(wù)來(lái)解決了“非原子性”的問(wèn)題,但是事務(wù)這玩意,一般來(lái)說(shuō),大家都是能不使用事務(wù)的地方就盡量不使用事務(wù),通過(guò)最終一致性來(lái)保證數據的完整性。

這個(gè)老登肯定會(huì )在這個(gè)地方繼續窮追猛打的,我先預判了他,想想這個(gè)問(wèn)題怎么解決。

我們可以在消息消費記錄表里面再引入一個(gè)“狀態(tài)”字段,這個(gè)字段有兩個(gè)取值:消費中、消費完成。

同時(shí)把唯一索引改成“消息唯一標識+狀態(tài)”。

首先,MQ 發(fā)起請求,數據往消息消費記錄表插的時(shí)候,狀態(tài)直接就是“消費中”。

如果插入成功,則說(shuō)明是第一次消費,進(jìn)入到業(yè)務(wù)邏輯中去。

  • 如果業(yè)務(wù)邏輯執行成功,則更新消息消費記錄表對應數據為“消費完成”。
  • 如果業(yè)務(wù)邏輯執行失敗,則刪除消息消費記錄表對應數據,把消息仍回 MQ,等待下次重試。

如果插入失敗,則說(shuō)明是重復消費,直接扔掉。

畫(huà)成流程圖上大概是這樣的:

這就叫“面試造火箭,工作擰螺絲!”

順便提一嘴,上面這個(gè)流程圖我是用這個(gè)網(wǎng)站直接生成的,我覺(jué)得這個(gè)網(wǎng)站畫(huà)圖還挺舒服的:

https://excalidraw.com/

這就叫“面試造火箭,工作擰螺絲!”
這就叫“面試造火箭,工作擰螺絲!”

你感覺(jué)這波應該穩了,于是給面試官說(shuō)出了自己的方案,并在白字上畫(huà)了流程圖。

面試官拿著(zhù)你的流程圖,看了一眼,立馬就看出了一個(gè)問(wèn)題:如果一個(gè)消息插入失敗,你的邏輯是扔掉。那假設這條消息的狀態(tài)是消費中,業(yè)務(wù)邏輯執行失敗,是不是應該重新消費才對呢?

于是你立馬反映過(guò)來(lái),如果插入失敗,則說(shuō)明是重復消費,還需要判斷數據的狀態(tài)。

  • 如果狀態(tài)是“消費成功”,則說(shuō)明重復請求,直接返回成功
  • 如果狀態(tài)是“消費中”,則說(shuō)明還未處理完成,為了確保成功,需要把請求再次仍回到 MQ。

修改了流程圖:

這就叫“面試造火箭,工作擰螺絲!”

面試官拿著(zhù)這個(gè)流程圖,微微一笑:

倘若我業(yè)務(wù)執行完之后,狀態(tài)更新之前,服務(wù)掛了,閣下又該如何應對?

巧了,這個(gè)問(wèn)題上一篇文章的評論區也提到了:

這就叫“面試造火箭,工作擰螺絲!”

所以,還需要針對長(cháng)時(shí)間在“消費中”的數據進(jìn)行一個(gè)監控,人工兜底一下。

此外,為了防止“消費完成”的數據量過(guò)多,還應該對于這個(gè)狀態(tài)的數據做一個(gè)定時(shí)清理的任務(wù)。

終于,你看到了面試官臉上那一閃而過(guò)的滿(mǎn)意表情,在你覺(jué)得面試官應該會(huì )放過(guò)你了的時(shí)候,他又提出了另外的問(wèn)題:

你這個(gè)通用組件理論上確實(shí)是可行的。

但是,這張表放在哪個(gè)庫的哪個(gè)表里呢?

是統一放在一個(gè)庫里呢還是就放在業(yè)務(wù)服務(wù)的庫里呢?

統一放一個(gè)庫的話(huà)太大了怎么辦呢是不是要按日期分表?

萬(wàn)一跟業(yè)務(wù)庫用的數據庫不是一個(gè)數據庫產(chǎn)品那事務(wù)不生效咋辦呢?

放在業(yè)務(wù)庫里的話(huà)萬(wàn)一業(yè)務(wù)服務(wù)連好幾個(gè)庫那我具體放哪一個(gè)呢?

是不是所有業(yè)務(wù)庫我都得加這么一張表強制綁架他們的數據庫?

...

這就叫“面試造火箭,工作擰螺絲!”

這一部分問(wèn)題,也來(lái)自上一篇文章評論區。

這就叫“面試造火箭,工作擰螺絲!”

聽(tīng)到這些問(wèn)題,你開(kāi)始覺(jué)得這個(gè)面試官是在胡攪蠻纏,一氣之下,準備拿回簡(jiǎn)歷,結束面試。

但是手上動(dòng)作稍微大了一點(diǎn),一不小心掀起了自己的帽子,漏出了“資深的發(fā)型”。

面試官也愣住了,看著(zhù)你“資深的發(fā)型”,當即就握住了你的手:你就是我要找的人才。不面了,就你了,明天來(lái)報道!

這就叫“面試造火箭,工作擰螺絲!”

入職

入職之后你第一件事情就是看看這個(gè)公司的代碼。

當你看第一個(gè)接口的時(shí)候,發(fā)現根本沒(méi)有做冪等。

當你看第二個(gè)接口的時(shí)候,發(fā)現就是靠業(yè)務(wù)表的唯一索引做的冪等。

當你看第三個(gè)接口的時(shí)候,Redis 的方案躍然紙上。

突然一個(gè)哥們氣喘吁吁的跑過(guò)來(lái)找昨天面試你的老登,說(shuō):快,又出問(wèn)題了,幫忙刪除一個(gè) Redis key。

于是,你抽過(guò)去準備看一下怎么操作。

不經(jīng)意間看到了老登正在寫(xiě)一個(gè)文檔,題目叫做《一種分布式系統中數據唯一性的消息冪等保障策略》。

老登看到你過(guò)來(lái)了,說(shuō):正好,你來(lái)寫(xiě)這個(gè)文檔,我已經(jīng)把名字給你想好了,你就按照這個(gè)寫(xiě),把你昨天的思路寫(xiě)清楚,到時(shí)候我去匯報。

你興奮的問(wèn):匯報過(guò)了之后我們要按照這個(gè)方案落地嗎?

老登說(shuō):不不不,落地干啥啊,多麻煩啊,方案匯報嘛,體現一下我們在技術(shù)方面的時(shí)刻,在領(lǐng)導面前去刷個(gè)臉,所以你要多用一些高大上的詞,越晦澀難懂越好。哦,對了,我順便教教你怎么“刪除 Redis key”,以后就讓他們找你了。這幫老登,大半夜的,老是給我打電話(huà)。

這就叫“面試造火箭,工作擰螺絲!”
小編推薦閱讀

好特網(wǎng)發(fā)布此文僅為傳遞信息,不代表好特網(wǎng)認同期限觀(guān)點(diǎn)或證實(shí)其描述。

相關(guān)視頻攻略

更多

掃二維碼進(jìn)入好特網(wǎng)手機版本!

掃二維碼進(jìn)入好特網(wǎng)微信公眾號!

本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權,請發(fā)郵件admin@haote.com

湘ICP備2022002427號-10 湘公網(wǎng)安備:43070202000427號© 2013~2024 haote.com 好特網(wǎng)