搞了個(gè)線上故障,被老板罵了....
大家好,我是Tom哥。
前幾天跟一位小伙伴聊天,心情特別沮喪,剛被老板罵完.....
差點(diǎn)丟了飯碗,還好老板沒(méi)說(shuō) “滾”。
就今年這就業(yè)行情,滿眼都是淚哇。
小伙伴在一家初創(chuàng)公司,團(tuán)隊(duì)規(guī)模很小,老板為了節(jié)省成本,也沒(méi)配置什么豪華陣容。
他的工作時(shí)間也不長(zhǎng),負(fù)責(zé)交易訂單,前幾天接到用戶投訴,「我的訂單列表」有多條一模一樣的訂單。
雖沒(méi)造成什么資損,但嚴(yán)重影響用戶體驗(yàn)。
看到這里,有經(jīng)驗(yàn)的同學(xué)可能猜到,應(yīng)該是接口沒(méi)做防重控制。
日常開(kāi)發(fā)中,重復(fù)提交也是蠻常見(jiàn)問(wèn)題。
比如:用戶提交一個(gè)表單,鼠標(biāo)點(diǎn)的太快,正好前端又是個(gè)新兵蛋子,沒(méi)做任何控制,瞬間就會(huì)有多個(gè)請(qǐng)求發(fā)到后端系統(tǒng)。
如果后端同學(xué)也沒(méi)做兜底方案的話,悲劇就發(fā)生了。
常見(jiàn)的解決方案是借助數(shù)據(jù)庫(kù)自身的「唯一索引約束」,來(lái)保證數(shù)據(jù)的準(zhǔn)確性,這種方案一般在插入場(chǎng)景用的多些。
變種方案可以考慮單獨(dú)創(chuàng)建一個(gè)防重表。
本文的案例有點(diǎn)特殊,訂單號(hào)是后端系統(tǒng)生成的,前后兩次請(qǐng)求無(wú)法區(qū)分重復(fù)狀態(tài),所以系統(tǒng)會(huì)創(chuàng)建兩條不同訂單 ID 記錄,繞過(guò)了「唯一索引約束」這個(gè)限制,這.....
另外,MySQL 性能也單薄了點(diǎn),單機(jī) QPS 在「千」維度,如果是面對(duì)一個(gè)高并發(fā)接口,性能也有點(diǎn)吃緊。
接下來(lái),我們就來(lái)講下,借助 Redis 來(lái)實(shí)現(xiàn)接口防重復(fù)提交。
技術(shù)方案
首先,我們來(lái)看下整理的流程,如下圖所示:
大致步驟:
1、客戶端發(fā)送請(qǐng)求到服務(wù)端。
2、服務(wù)端接收請(qǐng)求,然后從請(qǐng)求參數(shù)中提取唯一標(biāo)識(shí)。這個(gè)標(biāo)識(shí)可以沒(méi)有什么特殊業(yè)務(wù)含義,client 端隨機(jī)生成即可。
3、服務(wù)端系統(tǒng)將唯一標(biāo)識(shí)先嘗試寫(xiě)入 Redis 緩存中,可以認(rèn)為是加鎖操作。
4、加鎖失敗,說(shuō)明請(qǐng)求還在處理,此次是重復(fù)請(qǐng)求,可以丟棄。
5、加鎖成功,繼續(xù)后面正常業(yè)務(wù)邏輯處理。
6、業(yè)務(wù)邏輯處理完成后,刪除加鎖的標(biāo)記。
7、最后,將處理成功的結(jié)果返回給客戶端。
注意事項(xiàng):
- 重復(fù)提交場(chǎng)景一般都是在極短時(shí)間內(nèi),同時(shí)發(fā)送了多次請(qǐng)求(比如:頁(yè)面表單重復(fù)提交),我們只認(rèn)第一次請(qǐng)求為有效請(qǐng)求。
- 鎖用完后,要記得手動(dòng)刪除。為了防止鎖沒(méi)有正常釋放,我們可以為鎖設(shè)置一個(gè)極短的過(guò)期時(shí)間(比如 10 秒)。
項(xiàng)目實(shí)戰(zhàn)
1、引入 redis 組件
實(shí)戰(zhàn)的項(xiàng)目采用 Spring Boot 搭建,這里需要引入 Redis 相關(guān)依賴。
2、redis 變量配置
application.properties 配置文件中,添加redis相關(guān)服務(wù)配置。
3、定義注解類
定義一個(gè)注解,配置在需要防重復(fù)的接口方法上,提高開(kāi)發(fā)效率,同時(shí)降低代碼的耦合度。
4、接口攔截器
上面定義了IdempotentRule?注解,需要通過(guò)攔截器對(duì)正常的業(yè)務(wù)方法做攔截,增加一些特殊邏輯處理。
這里,比較特殊的是提取請(qǐng)求的唯一標(biāo)識(shí),由于不同的業(yè)務(wù)請(qǐng)求唯一標(biāo)識(shí)不一樣。
所以,這里采用 SPEL 表達(dá)式,將規(guī)則設(shè)置能力開(kāi)放出去,由業(yè)務(wù)方自己定義,比如:
@IdempotentRule(key = "#userParam.cardNumber", prefix = "repeat_")。
攔截器根據(jù) SPEL 表達(dá)式( 如 "#userParam.cardNumber")以及請(qǐng)求參數(shù)對(duì)象,計(jì)算當(dāng)前請(qǐng)求唯一標(biāo)識(shí)的值,
然后將值寫(xiě)入 Redis 中,并設(shè)置過(guò)時(shí)間。
如果設(shè)置成功,說(shuō)明是第一次請(qǐng)求,繼續(xù)下面的業(yè)務(wù)邏輯處理;否則,判定為重復(fù)請(qǐng)求,直接丟棄。
5、上層業(yè)務(wù)接口
測(cè)試結(jié)果
1、構(gòu)造客戶端請(qǐng)求,第一次處理成功。
2、 Redis 緩存中,能查到請(qǐng)求設(shè)置的鎖標(biāo)記。
3、模擬重復(fù),連續(xù)多次快速提交請(qǐng)求,請(qǐng)求會(huì)被攔截,并拋出異常。