你好呀,我是歪歪。 我想再討論一下上次的這篇文章《哎,被這個叫做at least once的玩意坑麻了》 因為有些朋友看完之后再評論區(qū)給出了自己的思考,也有朋友和我私聊,分享了自己的看法,我覺得有些想法很好,所以我決定一魚兩吃,再聊聊這個問題。 假設(shè),我們是一場面試,面試官給你拋出了這樣一個問題:
你好呀,我是歪歪。
我想再討論一下上次的這篇文章 《哎,被這個叫做at least once的玩意坑麻了》
因為有些朋友看完之后再評論區(qū)給出了自己的思考,也有朋友和我私聊,分享了自己的看法,我覺得有些想法很好,所以我決定一魚兩吃,再聊聊這個問題。
假設(shè),我們是一場面試,面試官給你拋出了這樣一個問題:
如果一個消費隊列由于某些原因,對于某個消息發(fā)起了兩次。導(dǎo)致一樣的數(shù)據(jù)落庫兩條,請問你會怎么處理這個問題?
這題你一拿到手上,應(yīng)該就立馬能分析出是在問如何實現(xiàn)一個冪等機(jī)制。
想著這玩意我熟啊,張口就能給出方案:
業(yè)務(wù)消息?=?select(業(yè)務(wù)唯一流水號);
if(業(yè)務(wù)消息?==?null){
????save(業(yè)務(wù)消息);
}
面試官一聽,提示道:你這個方案在多線程的情況下會不會有什么問題呢?
于是你的小腦瓜子立刻開始轉(zhuǎn)了起來:先查詢,再判斷,最后保存。
如果兩個線程同時過來,都查不到數(shù)據(jù),那么就能都走到保存的邏輯里面去,確實攔不住。
于是你扣了一下腦殼,想起了你上家公司針對這個問題,就是在數(shù)據(jù)庫的表結(jié)構(gòu)里面,對業(yè)務(wù)唯一流水號做了唯一索引,所以不會出現(xiàn)重復(fù)插入的情況。
然后你給出了“加唯一索引”的方案,準(zhǔn)備絕殺這個問題。
沒想到面試官非常不懂事,還在繼續(xù)追問:我想盡量不要讓程序拋出異常,還有沒有其他的方案呢?
你抱著自己的左手,邊啃指甲邊思考:唯一索引是數(shù)據(jù)庫幫我們保證的邏輯,現(xiàn)在面試官這個老登不想讓我用數(shù)據(jù)庫來做這件事情。那就必須要控制在并發(fā)的場景下,只有一個請求能抵達(dá)數(shù)據(jù)庫。
鎖!這不就是鎖干的事兒嗎?
于是你飛快的又想到了一個方法:
flag?=?redis(業(yè)務(wù)唯一流水號,過期時間);
if(flag){
????save(業(yè)務(wù)消息);????
}
可以利用業(yè)務(wù)唯一流水號結(jié)合 Redis 來做一個鎖,加鎖成功的請求才能走到 save 邏輯中。
這樣就能解決并發(fā)場景下,多個請求穿透到 save 邏輯這一步的問題。
面試官聽到你這個方案之后,立馬就啟動了追問技能:如果放 Redis 成功了,但是還沒來得及 save,服務(wù)重啟了。
這個請求理論上是應(yīng)該能再次發(fā)起的,但是由于 Redis 鎖的存在,導(dǎo)致不會走到 save 的邏輯去,怎么辦呢?
于是你又扣了一下腦殼,想起你在上家公司的時候,好像也遇到過這個情況。
當(dāng)時的解決方案就是人工介入,分析了一波數(shù)據(jù),確認(rèn)了這個消息確實應(yīng)該被繼續(xù)處理,于是你找 DBA 幫忙刪除了 Redis 對應(yīng)的 key,流程就通了。
然而這個回答面試官并不滿意:人工就顯得不優(yōu)雅了,要不再想想?
你又抱著自己的右手,邊啃指甲邊思考:這個老登考慮的確實挺多的,感覺應(yīng)該在一個很厲害的團(tuán)隊,我得加把勁兒,再想想。
現(xiàn)在要人工介入的原因,是因為我們把第二次的請求攔截住并丟棄了。
如果不丟棄,那么理論上在“過期時間”到了,鎖被釋放后,第二次的請求拿到鎖,就能接著往下走。
所以,這里需要在 Redis 這里加一個加鎖失敗則等待的邏輯:
flag?=?redis(業(yè)務(wù)唯一流水號,過期時間,獲取不到則等待);
if(flag){
????save(業(yè)務(wù)消息);????
}
但是你一看這個邏輯又不對了:由于有鎖等待的邏輯,那么如果兩個請求過來,還是有可能會都放入到 Redis 里面,flag 都會為 true,那么 save 方法還是會走兩遍。
所以,還得在獲取鎖成功之后加上一個查詢數(shù)據(jù)庫的邏輯:
flag?=?redis(業(yè)務(wù)唯一流水號,過期時間,獲取不到則等待);
if(flag){
????業(yè)務(wù)消息?=?select(業(yè)務(wù)唯一流水號);
????if(業(yè)務(wù)消息?==?null){
????????save(業(yè)務(wù)消息);????
????}
}else{
????//等待結(jié)束后還是未獲取到鎖,發(fā)送預(yù)警
????monitor(預(yù)警信息);
}
第一層的 Redis 相當(dāng)于讓請求排隊,確保只有一個請求進(jìn)來。
第二層的 select 才是真正的防止重復(fù)的業(yè)務(wù)邏輯。
同時,如果等待結(jié)束后還是未獲取到鎖,出現(xiàn)這種低概率情況,就預(yù)警出來,人工兜底嘛,一旦人工介入,那就是能解決任何問題。
你心想這波應(yīng)該是穩(wěn)了,應(yīng)該是可以換題了。
然而面試官并不打算在這個回合上輕易放過你:這個方案確實是可以解決這個問題,但是在技術(shù)實現(xiàn)上引入了 Redis 框架,如果我不使用 Redis,單純的靠 MySQL 呢?
聽到這個問題的時候你覺得不對啊,最開始的時候不就是說了“加唯一索引”就可以解決這個問題嗎?
于是面試官補(bǔ)充了一下描述:
最開始的加唯一索引是基于業(yè)務(wù)表來做的,如果出現(xiàn)問題就讓其拋出主鍵沖突異常,這個方案確實是可以實現(xiàn)需求。但是我現(xiàn)在想讓你給我設(shè)計一個通用的技術(shù)組件,不需要基于某個具體的業(yè)務(wù)場景去設(shè)計。我想聽聽你的思路。
拿到新的題目,你開始覺得這是***難,看著面試官求知的眼神,你又開始懷疑:這個老登不會是來套方案的吧?
看著自己已經(jīng)被咬禿了的左右大拇指指甲,感覺自己的靈感和指甲一樣都光禿禿的。
開始后悔前面幾個回合咬得太快了,原以為可以秒殺這個面試,沒想到面試官還在纏斗。你動了使用必殺技來結(jié)束戰(zhàn)斗的念想。
于是從帽子的縫隙中插進(jìn)入一根指甲已經(jīng)禿了的手指,在差不多禿了的頭頂,用指腹畫圈,給自己頭皮按摩,醫(yī)生說這樣的有助于毛囊發(fā)育,你想著頭發(fā)還會長出來,就思如泉涌,這就是必殺技。
你陷入了思考,Redis 在前面的方案中是為了防止有多條數(shù)據(jù)穿透到 save 方法中去,如果不讓用 Redis。MySQL 怎么實現(xiàn)類似的效果呢?
也加鎖嗎?for update?
業(yè)務(wù)消息?=?select(業(yè)務(wù)唯一流水號);//select?***?for?update
if(業(yè)務(wù)消息?==?null){
????save(業(yè)務(wù)消息);????
}
這玩意一看上去就是性能就拉胯了,為了解決這個偶發(fā)的問題,犧牲了接口的性能,這個路線就走的有點遠(yuǎn)了。
而且這個上鎖的邏輯隱藏的有點深,容易留下后患,面試官肯定不會滿意的。
那還有什么辦法,能把 MySQL 當(dāng)作鎖來用,確保并發(fā)情況下只有一個請求能穿過這個鎖呢?
那還是得靠唯一索引的約束才行。
但是這個唯一索引面試官不讓用業(yè)務(wù)表的,那就只能直接搞個“消息消費記錄表”,里面有個“消息唯一標(biāo)識”的字段,這個字段是唯一索引。
這張表面試官問起來,我就說這張表是完全獨立于業(yè)務(wù)的存在,只是為了解決消息冪等這個存粹的技術(shù)問題而出現(xiàn)的,基于它,我們就可以設(shè)計出一個通用的技術(shù)組件,這樣應(yīng)該說的過去。
表有了,技術(shù)方法大概的雛形就有了。
然而你還不能開始答題,現(xiàn)在思路還不是特別清晰,你要把方案捋清楚了再張口。
在不知不覺間,你的指腹已經(jīng)摩擦的有點麻木了,于是你換了一個手,穿過帽子,接著按摩著自己的頭頂。
這個表我怎么用呢?
if(保存數(shù)據(jù)到消息消費記錄表){//出現(xiàn)主鍵沖突就返回false
????save(業(yè)務(wù)消息);????
}
先校驗,再保存,非原子性,這樣肯定不行啊,
我們想想一個場景,如果保存數(shù)據(jù)到消息消費記錄表成功,還沒來得及 save(業(yè)務(wù)消息) ,服務(wù)重啟了,怎么辦?
所以為了保證原子性,我們可以加入事務(wù),把這兩步綁定到一起:
開啟事務(wù);
if(保存數(shù)據(jù)到消息消費記錄表){//出現(xiàn)主鍵沖突就返回false
????save(業(yè)務(wù)消息);????
}
提交事務(wù);
這樣,如果保存數(shù)據(jù)到消息消費記錄表成功,還沒來得及 save(扣款信息) ,服務(wù)重啟,事務(wù)回滾,消息消費記錄表就不會真的插入成功。
而 MQ 沒有收到這個消息的回執(zhí),也會再次進(jìn)行投遞。
由于消息消費記錄表里沒有這個數(shù)據(jù),所以會再次進(jìn)行消費。
現(xiàn)在你覺得似乎沒啥問題了,剛想給面試官說你這個思路,但是立馬又想到了另外一個問題:通過引入事務(wù)來解決了“非原子性”的問題,但是事務(wù)這玩意,一般來說,大家都是能不使用事務(wù)的地方就盡量不使用事務(wù),通過最終一致性來保證數(shù)據(jù)的完整性。
這個老登肯定會在這個地方繼續(xù)窮追猛打的,我先預(yù)判了他,想想這個問題怎么解決。
我們可以在消息消費記錄表里面再引入一個“狀態(tài)”字段,這個字段有兩個取值:消費中、消費完成。
同時把唯一索引改成“消息唯一標(biāo)識+狀態(tài)”。
首先,MQ 發(fā)起請求,數(shù)據(jù)往消息消費記錄表插的時候,狀態(tài)直接就是“消費中”。
如果插入成功,則說明是第一次消費,進(jìn)入到業(yè)務(wù)邏輯中去。
如果插入失敗,則說明是重復(fù)消費,直接扔掉。
畫成流程圖上大概是這樣的:
順便提一嘴,上面這個流程圖我是用這個網(wǎng)站直接生成的,我覺得這個網(wǎng)站畫圖還挺舒服的:
你感覺這波應(yīng)該穩(wěn)了,于是給面試官說出了自己的方案,并在白字上畫了流程圖。
面試官拿著你的流程圖,看了一眼,立馬就看出了一個問題:如果一個消息插入失敗,你的邏輯是扔掉。那假設(shè)這條消息的狀態(tài)是消費中,業(yè)務(wù)邏輯執(zhí)行失敗,是不是應(yīng)該重新消費才對呢?
于是你立馬反映過來,如果插入失敗,則說明是重復(fù)消費,還需要判斷數(shù)據(jù)的狀態(tài)。
修改了流程圖:
面試官拿著這個流程圖,微微一笑:
倘若我業(yè)務(wù)執(zhí)行完之后,狀態(tài)更新之前,服務(wù)掛了,閣下又該如何應(yīng)對?
巧了,這個問題上一篇文章的評論區(qū)也提到了:
所以,還需要針對長時間在“消費中”的數(shù)據(jù)進(jìn)行一個監(jiān)控,人工兜底一下。
此外,為了防止“消費完成”的數(shù)據(jù)量過多,還應(yīng)該對于這個狀態(tài)的數(shù)據(jù)做一個定時清理的任務(wù)。
終于,你看到了面試官臉上那一閃而過的滿意表情,在你覺得面試官應(yīng)該會放過你了的時候,他又提出了另外的問題:
你這個通用組件理論上確實是可行的。
但是,這張表放在哪個庫的哪個表里呢?
是統(tǒng)一放在一個庫里呢還是就放在業(yè)務(wù)服務(wù)的庫里呢?
統(tǒng)一放一個庫的話太大了怎么辦呢是不是要按日期分表?
萬一跟業(yè)務(wù)庫用的數(shù)據(jù)庫不是一個數(shù)據(jù)庫產(chǎn)品那事務(wù)不生效咋辦呢?
放在業(yè)務(wù)庫里的話萬一業(yè)務(wù)服務(wù)連好幾個庫那我具體放哪一個呢?
是不是所有業(yè)務(wù)庫我都得加這么一張表強(qiáng)制綁架他們的數(shù)據(jù)庫?
...
這一部分問題,也來自上一篇文章評論區(qū)。
聽到這些問題,你開始覺得這個面試官是在胡攪蠻纏,一氣之下,準(zhǔn)備拿回簡歷,結(jié)束面試。
但是手上動作稍微大了一點,一不小心掀起了自己的帽子,漏出了“資深的發(fā)型”。
面試官也愣住了,看著你“資深的發(fā)型”,當(dāng)即就握住了你的手:你就是我要找的人才。不面了,就你了,明天來報道!
入職之后你第一件事情就是看看這個公司的代碼。
當(dāng)你看第一個接口的時候,發(fā)現(xiàn)根本沒有做冪等。
當(dāng)你看第二個接口的時候,發(fā)現(xiàn)就是靠業(yè)務(wù)表的唯一索引做的冪等。
當(dāng)你看第三個接口的時候,Redis 的方案躍然紙上。
突然一個哥們氣喘吁吁的跑過來找昨天面試你的老登,說:快,又出問題了,幫忙刪除一個 Redis key。
于是,你抽過去準(zhǔn)備看一下怎么操作。
不經(jīng)意間看到了老登正在寫一個文檔,題目叫做《一種分布式系統(tǒng)中數(shù)據(jù)唯一性的消息冪等保障策略》。
老登看到你過來了,說:正好,你來寫這個文檔,我已經(jīng)把名字給你想好了,你就按照這個寫,把你昨天的思路寫清楚,到時候我去匯報。
你興奮的問:匯報過了之后我們要按照這個方案落地嗎?
老登說:不不不,落地干啥啊,多麻煩啊,方案匯報嘛,體現(xiàn)一下我們在技術(shù)方面的時刻,在領(lǐng)導(dǎo)面前去刷個臉,所以你要多用一些高大上的詞,越晦澀難懂越好。哦,對了,我順便教教你怎么“刪除 Redis key”,以后就讓他們找你了。這幫老登,大半夜的,老是給我打電話。
機(jī)器學(xué)習(xí):神經(jīng)網(wǎng)絡(luò)構(gòu)建(下)
閱讀華為Mate品牌盛典:HarmonyOS NEXT加持下游戲性能得到充分釋放
閱讀實現(xiàn)對象集合與DataTable的相互轉(zhuǎn)換
閱讀算法與數(shù)據(jù)結(jié)構(gòu) 1 - 模擬
閱讀5. Spring Cloud OpenFeign 聲明式 WebService 客戶端的超詳細(xì)使用
閱讀Java代理模式:靜態(tài)代理和動態(tài)代理的對比分析
閱讀Win11筆記本“自動管理應(yīng)用的顏色”顯示規(guī)則
閱讀本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權(quán),請發(fā)郵件[email protected]
湘ICP備2022002427號-10 湘公網(wǎng)安備:43070202000427號© 2013~2025 haote.com 好特網(wǎng)