原文鏈接:https://mp.weixin.qq.com/s/Zri7YMhZINV1Mu2DPUecPA
1. 接口參數(shù)校驗(yàn)
入?yún)⒊鰠⑿r?yàn)是每個(gè)程序員必備的基本素養(yǎng)。設(shè)計(jì)接口,必須先校驗(yàn)參數(shù)。
比如入?yún)⑹欠裨试S為空,入?yún)㈤L(zhǎng)度是否符合預(yù)期長(zhǎng)度。這個(gè)要養(yǎng)成習(xí)慣,日常開(kāi)發(fā)中,很多低級(jí) bug 都是不校驗(yàn)參數(shù)導(dǎo)致的。
比如你的數(shù)據(jù)庫(kù)表字段設(shè)置為
varchar(16)
,對(duì)方傳了一個(gè) 32 位的字符串過(guò)來(lái),如果你不校驗(yàn)參數(shù),插入數(shù)據(jù)庫(kù)就直接異常了。
出參也是,比如你定義的接口報(bào)文,參數(shù)是不為空的,但是你的接口返回參數(shù)沒(méi)有做校驗(yàn),因?yàn)槌绦蚰承┰颍祷貏e人一個(gè)null
值。
2. 修改老接口,注意接口的兼容性
很多 bug 都是因?yàn)樾薷牧藢?duì)外老接口但是卻不做兼容導(dǎo)致的。關(guān)鍵這個(gè)問(wèn)題多數(shù)是比較嚴(yán)重的,可能直接導(dǎo)致系統(tǒng)發(fā)版失敗。新手程序員很容易犯這個(gè)錯(cuò)誤。所以,如果你的需求是在原來(lái)接口上做修改,尤其這個(gè)接口是對(duì)外提供服務(wù)的話,一定要考慮接口兼容。
舉個(gè)例子吧,比如 dubbo 接口,原本是只接收 A、B 參數(shù),現(xiàn)在加了一個(gè)參數(shù) C,就可以考慮這樣處理(方法重載):
//老接口
void oldService(A,B){
//兼容新接口,傳個(gè)null代替C
newService(A,B,null);
}
//新接口,暫時(shí)不能刪掉老接口,需要做兼容。
void newService(A,B,C){
...
}
3. 設(shè)計(jì)接口時(shí),充分考慮接口的可擴(kuò)展性
要根據(jù)實(shí)際業(yè)務(wù)場(chǎng)景設(shè)計(jì)接口,充分考慮接口的可擴(kuò)展性。
比如你接到一個(gè)需求:用戶(hù)添加或者修改員工時(shí),需要刷臉。那你是反手提供一個(gè)員工管理的提交刷臉信息接口呢?還是先思考:提交刷臉是不是通用流程呢?
比如轉(zhuǎn)賬或者一鍵貼現(xiàn)需要接入刷臉的話,你是否需要重新實(shí)現(xiàn)一個(gè)接口呢?還是當(dāng)前按業(yè)務(wù)類(lèi)型劃分模塊,復(fù)用這個(gè)接口就好,保留接口的可擴(kuò)展性。
如果按模塊劃分的話,未來(lái)如果其他場(chǎng)景比如一鍵貼現(xiàn)接入刷臉的話,不用再搞一套新的接口,只需要新增枚舉,然后復(fù)用刷臉通過(guò)流程接口,實(shí)現(xiàn)一鍵貼現(xiàn)刷臉的差異化即可。
4. 接口考慮是否需要做防重處理
如果前端重復(fù)請(qǐng)求,你的邏輯如何處理?是不是考慮接口去重處理。當(dāng)然,如果是查詢(xún)類(lèi)的請(qǐng)求,其實(shí)不用防重。如果是更新修改類(lèi)的話,尤其金融轉(zhuǎn)賬類(lèi)的,就要過(guò)濾重復(fù)請(qǐng)求了。簡(jiǎn)單點(diǎn),你可以使用 Redis 防重復(fù)請(qǐng)求,同樣的請(qǐng)求方,一定時(shí)間間隔內(nèi)的相同請(qǐng)求,考慮是否過(guò)濾。當(dāng)然,轉(zhuǎn)賬類(lèi)接口,并發(fā)不高的話,推薦使用數(shù)據(jù)庫(kù)防重表,以唯一流水號(hào)作為主鍵或者唯一索引。
5. 重點(diǎn)接口,考慮線程池隔離
一些登錄、轉(zhuǎn)賬交易、下單等重要接口,考慮線程池隔離。
如果你所有業(yè)務(wù)都共用一個(gè)線程池,有些業(yè)務(wù)出 bug 導(dǎo)致線程池阻塞打滿(mǎn)的話,那就杯具了,所有業(yè)務(wù)都影響了。因此進(jìn)行線程池隔離,重要業(yè)務(wù)分配多一點(diǎn)的核心線程,就能更好保護(hù)重要業(yè)務(wù)。
6. 調(diào)用第三方接口要考慮異常和超時(shí)處理
如果你調(diào)用第三方接口,或者分布式遠(yuǎn)程服務(wù)的話,需要考慮:
-
異常處理
比如,你調(diào)別人的接口,如果異常了,怎么處理,是重試還是當(dāng)做失敗還是告警處理。
-
接口超時(shí)
沒(méi)法預(yù)估對(duì)方接口多久返回,一般設(shè)置個(gè)超時(shí)斷開(kāi)時(shí)間,以保護(hù)你的接口。之前見(jiàn)過(guò)一個(gè)生產(chǎn)問(wèn)題,就是 http 調(diào)用不設(shè)置超時(shí)時(shí)間,最后響應(yīng)方進(jìn)程假死,請(qǐng)求一直占著線程不釋放,拖垮線程池。
-
重試次數(shù)
你的接口調(diào)失敗,需不需要重試?重試幾次?需要站在業(yè)務(wù)角度思考這個(gè)問(wèn)題。
7. 接口實(shí)現(xiàn)考慮熔斷和降級(jí)
當(dāng)前互聯(lián)網(wǎng)系統(tǒng)一般都是分布式部署的。而分布式系統(tǒng)中經(jīng)常會(huì)出現(xiàn)某個(gè)基礎(chǔ)服務(wù)不可用,最終導(dǎo)致整個(gè)系統(tǒng)不可用的情況,這種現(xiàn)象被稱(chēng)為服務(wù)雪崩效應(yīng)。
比如分布式調(diào)用鏈路A->B->C....
,下圖所示:
如果服務(wù) C 出現(xiàn)問(wèn)題,比如是因?yàn)槁?SQL 導(dǎo)致調(diào)用緩慢,那將導(dǎo)致 B 也會(huì)延遲,從而 A 也會(huì)延遲。
堵住的 A 請(qǐng)求會(huì)消耗占用系統(tǒng)的線程、IO 等資源。當(dāng)請(qǐng)求 A 的服務(wù)越來(lái)越多,占用計(jì)算機(jī)的資源也越來(lái)越多,
最終會(huì)導(dǎo)致系統(tǒng)瓶頸出現(xiàn),造成其他的請(qǐng)求同樣不可用,最后導(dǎo)致業(yè)務(wù)系統(tǒng)崩潰。
為了應(yīng)對(duì)服務(wù)雪崩,常見(jiàn)的做法是熔斷和降級(jí)。最簡(jiǎn)單是加開(kāi)關(guān)控制,當(dāng)下游系統(tǒng)出問(wèn)題時(shí),開(kāi)關(guān)降級(jí),不再調(diào)用下游系統(tǒng)。還可以選用開(kāi)源組件Hystrix
。
8. 日志打印好,接口的關(guān)鍵代碼,要有日志保駕護(hù)航
關(guān)鍵業(yè)務(wù)代碼無(wú)論身處何地,都應(yīng)該有足夠的日志保駕護(hù)航。
比如:你實(shí)現(xiàn)轉(zhuǎn)賬業(yè)務(wù),轉(zhuǎn)個(gè)幾百萬(wàn),然后轉(zhuǎn)失敗了,接著客戶(hù)投訴,然后你還沒(méi)有打印到日志,想想那種水深火熱的困境下,你卻毫無(wú)辦法。。。
那么,你的轉(zhuǎn)賬業(yè)務(wù)都需要哪些日志信息呢?至少,方法調(diào)用前,入?yún)⑿枰蛴“?,接口調(diào)用后,需要捕獲一下異常吧,同時(shí)打印異常相關(guān)日志,如下:
public?void?transfer(TransferDTO?transferDTO){
????log.info("invoke?tranfer?begin");
????//打印入?yún)?????log.info("invoke?tranfer,paramters:{}",transferDTO);
????try?{
??????res=??transferService.transfer(transferDTO);
????}catch(Exception?e){
?????log.error("transfer fail,account:{}",
?????transferDTO.getAccount())
?????log.error("transfer?fail,exception:{}",e);
????}
????log.info("invoke?tranfer?end");
????}
9. 接口的功能定義要具備單一性
單一性是指接口做的事情比較單一、專(zhuān)一。比如一個(gè)登錄接口,它做的事情就只是校驗(yàn)賬戶(hù)名密碼,然后返回登錄成功以及userId
即可。
但是如果為了減少接口交互,把一些注冊(cè)、一些配置查詢(xún)等全放到登錄接口,就不太妥。
其實(shí)這也是微服務(wù)一些思想,接口的功能單一、明確。比如訂單服務(wù)、積分、商品信息相關(guān)的接口都是劃分開(kāi)的。將來(lái)拆分微服務(wù)的話,是不是就比較簡(jiǎn)便啦。
10. 接口有些場(chǎng)景,使用異步更合理
舉個(gè)簡(jiǎn)單的例子,比如實(shí)現(xiàn)一個(gè)用戶(hù)注冊(cè)的接口,用戶(hù)注冊(cè)成功時(shí),發(fā)個(gè)郵件或者短信去通知用戶(hù)。這個(gè)郵件或者發(fā)短信,就更適合異步處理。因?yàn)榭偛荒芤粋€(gè)通知類(lèi)的失敗,導(dǎo)致注冊(cè)失敗吧。至于做異步的方式,簡(jiǎn)單的就是用線程池。還可以使用消息隊(duì)列,就是用戶(hù)注冊(cè)成功后,生產(chǎn)者產(chǎn)生一個(gè)注冊(cè)成功的消息,消費(fèi)者拉到注冊(cè)成功的消息,就發(fā)送通知。
?
不是所有的接口都適合設(shè)計(jì)為同步接口。比如你要做一個(gè)轉(zhuǎn)賬的功能,如果是單筆的轉(zhuǎn)賬,你是可以把接口設(shè)計(jì)同步。
用戶(hù)發(fā)起轉(zhuǎn)賬時(shí),客戶(hù)端再靜靜等待轉(zhuǎn)賬結(jié)果就好。如果是批量轉(zhuǎn)賬,一個(gè)批次一千筆,甚至一萬(wàn)筆的,你則可以把接口設(shè)計(jì)為異步。就是用戶(hù)發(fā)起批量轉(zhuǎn)賬時(shí),持久化成功就先返回受理成功。然后用戶(hù)隔十分鐘或者十五分鐘再來(lái)查轉(zhuǎn)賬結(jié)果就好。又或者,批量轉(zhuǎn)賬成功后,再回調(diào)上游系統(tǒng)。
11. 優(yōu)化接口耗時(shí),遠(yuǎn)程串行考慮改并行調(diào)用
假設(shè)我們?cè)O(shè)計(jì)一個(gè) APP 首頁(yè)的接口,它需要查用戶(hù)信息、需要查 banner 信息、需要查彈窗信息等等。那是一個(gè)一個(gè)接口串行調(diào),還是并行調(diào)用呢?
如果是串行一個(gè)一個(gè)查,比如查用戶(hù)信息 200ms,查 banner 信息 100ms、查彈窗信息 50ms,那一共就耗時(shí)350ms
了,如果還查其他信息,那耗時(shí)就更大了。這種場(chǎng)景是可以改為并行調(diào)用的。也就是說(shuō)查用戶(hù)信息、查 banner 信息、查彈窗信息,可以同時(shí)發(fā)起。
12. 接口合并或者說(shuō)考慮批量處理思想
數(shù)據(jù)庫(kù)操作或者是遠(yuǎn)程調(diào)用時(shí),能批量操作就不要 for 循環(huán)調(diào)用。
一個(gè)簡(jiǎn)單例子,我們平時(shí)一個(gè)列表明細(xì)數(shù)據(jù)插入數(shù)據(jù)庫(kù)時(shí),不要在 for 循環(huán)一條一條插入,建議一個(gè)批次幾百條,進(jìn)行批量插入。
同理遠(yuǎn)程調(diào)用也類(lèi)似想法,比如你查詢(xún)營(yíng)銷(xiāo)標(biāo)簽是否命中,可以一個(gè)標(biāo)簽一個(gè)標(biāo)簽去查,也可以批量標(biāo)簽去查,那批量進(jìn)行,效率就更高嘛。
//反例
for(int?i=0;i<n;i++){
??remoteSingleQuery(param)
}
//正例
remoteBatchQuery(param);
小伙伴們是否了解過(guò)kafka
為什么這么快呢?其實(shí)其中一點(diǎn)原因,就是 kafka?使用批量消息提升服務(wù)端處理能力。
13. 接口實(shí)現(xiàn)過(guò)程中,恰當(dāng)使用緩存
哪些場(chǎng)景適合使用緩存?讀多寫(xiě)少且數(shù)據(jù)時(shí)效要求越低的場(chǎng)景。
緩存用得好,可以承載更多的請(qǐng)求,提升查詢(xún)效率,減少數(shù)據(jù)庫(kù)的壓力。
比如一些平時(shí)變動(dòng)很小或者說(shuō)幾乎不會(huì)變的商品信息,可以放到緩存,請(qǐng)求過(guò)來(lái)時(shí),先查詢(xún)緩存,如果沒(méi)有再查數(shù)據(jù)庫(kù),并且把數(shù)據(jù)庫(kù)的數(shù)據(jù)更新到緩存。但是,使用緩存增加了需要考慮這些點(diǎn):緩存和數(shù)據(jù)庫(kù)一致性如何保證、集群、緩存擊穿、緩存雪崩、緩存穿透等問(wèn)題。
-
保證數(shù)據(jù)庫(kù)和緩存一致性:緩存延時(shí)雙刪、刪除緩存重試機(jī)制、讀取 biglog 異步刪除緩存。 - 緩存擊穿:設(shè)置數(shù)據(jù)永不過(guò)期。
-
緩存雪崩:Redis 集群高可用、均勻設(shè)置過(guò)期時(shí)間。 -
緩存穿透:接口層校驗(yàn)、查詢(xún)?yōu)榭赵O(shè)置個(gè)默認(rèn)空值標(biāo)記、布隆過(guò)濾器。
一般用Redis
分布式緩存,當(dāng)然有些時(shí)候也可以考慮使用本地緩存,如Guava Cache、Caffeine
等。
使用本地緩存有些缺點(diǎn),就是無(wú)法進(jìn)行大數(shù)據(jù)存儲(chǔ),并且應(yīng)用進(jìn)程的重啟,緩存會(huì)失效。
14. 接口考慮熱點(diǎn)數(shù)據(jù)隔離性
瞬時(shí)間的高并發(fā),可能會(huì)打垮你的系統(tǒng)??梢宰鲆恍狳c(diǎn)數(shù)據(jù)的隔離。比如業(yè)務(wù)隔離、系統(tǒng)隔離、用戶(hù)隔離、數(shù)據(jù)隔離等。
-
業(yè)務(wù)隔離:比如 12306 的分時(shí)段售票,將熱點(diǎn)數(shù)據(jù)分散處理,降低系統(tǒng)負(fù)載壓力。 -
系統(tǒng)隔離:比如把系統(tǒng)分成了用戶(hù)、商品、社區(qū)三個(gè)板塊。這三個(gè)塊分別使用不同的域名、服務(wù)器和數(shù)據(jù)庫(kù),做到從接入層到應(yīng)用層再到數(shù)據(jù)層三層完全隔離。 -
用戶(hù)隔離:重點(diǎn)用戶(hù)請(qǐng)求到配置更好的機(jī)器。 -
數(shù)據(jù)隔離:使用單獨(dú)的緩存集群或者數(shù)據(jù)庫(kù)服務(wù)熱點(diǎn)數(shù)據(jù)。
15. 可變參數(shù)配置化,比如紅包皮膚切換等
假如產(chǎn)品經(jīng)理提了個(gè)紅包需求,圣誕節(jié)的時(shí)候,紅包皮膚為圣誕節(jié)相關(guān)的,春節(jié)的時(shí)候,為春節(jié)紅包皮膚等。
如果在代碼寫(xiě)死控制,可有類(lèi)似以下代碼:
if(duringChristmas){
???img?=?redPacketChristmasSkin;
}else?if(duringSpringFestival){
???img?=??redSpringFestivalSkin;
}
如果到了元宵節(jié)的時(shí)候,運(yùn)營(yíng)小姐姐突然又有想法,紅包皮膚換成燈籠相關(guān)的,這時(shí)候,是不是要去修改代碼了,重新發(fā)布了?
從一開(kāi)始接口設(shè)計(jì)時(shí),可以實(shí)現(xiàn)一張紅包皮膚的配置表,將紅包皮膚做成配置化。更換紅包皮膚,只需修改一下表數(shù)據(jù)就好了。
當(dāng)然,還有一些場(chǎng)景適合一些配置化的參數(shù):一個(gè)分頁(yè)多少數(shù)量控制、某個(gè)搶紅包多久時(shí)間過(guò)期這些,都可以搞到參數(shù)配置化表里面。這也是擴(kuò)展性思想的一種體現(xiàn)。
16. 接口考慮冪等性
接口是需要考慮冪等性的,尤其搶紅包、轉(zhuǎn)賬這些重要接口。最直觀的業(yè)務(wù)場(chǎng)景,就是用戶(hù)連著點(diǎn)擊兩次,你的接口有沒(méi)有?hold 住。或者消息隊(duì)列出現(xiàn)重復(fù)消費(fèi)的情況,你的業(yè)務(wù)邏輯怎么控制?
回憶下,什么是冪等?
計(jì)算機(jī)科學(xué)中,冪等表示一次和多次請(qǐng)求某一個(gè)資源應(yīng)該具有同樣的副作用,或者說(shuō),多次請(qǐng)求所產(chǎn)生的影響與一次請(qǐng)求執(zhí)行的影響效果相同。
大家別搞混哈,防重和冪等設(shè)計(jì)其實(shí)是有區(qū)別的。防重主要為了避免產(chǎn)生重復(fù)數(shù)據(jù),把重復(fù)請(qǐng)求攔截下來(lái)即可。
而冪等設(shè)計(jì)除了攔截已經(jīng)處理的請(qǐng)求,還要求每次相同的請(qǐng)求都返回一樣的效果。不過(guò)呢,很多時(shí)候,它們的處理流程、方案是類(lèi)似的。
接口冪等實(shí)現(xiàn)方案主要有 8 種:
-
select + insert + 主鍵/唯一索引沖突 -
直接 insert + 主鍵/唯一索引沖突 -
狀態(tài)機(jī)冪等 -
抽取防重表 -
token 令牌 -
悲觀鎖 -
樂(lè)觀鎖 -
分布式鎖
17. 讀寫(xiě)分離,優(yōu)先考慮讀從庫(kù),注意主從延遲問(wèn)題
我們的數(shù)據(jù)庫(kù)都是集群部署的,有主庫(kù)也有從庫(kù),當(dāng)前一般都是讀寫(xiě)分離的。
比如寫(xiě)入數(shù)據(jù),肯定是寫(xiě)入主庫(kù),但是對(duì)于讀取實(shí)時(shí)性要求不高的數(shù)據(jù),則優(yōu)先考慮讀從庫(kù),因?yàn)榭梢苑謸?dān)主庫(kù)的壓力。
如果讀取從庫(kù)的話,需要考慮主從延遲的問(wèn)題。
18. 接口注意返回的數(shù)據(jù)量,如果數(shù)據(jù)量大需要分頁(yè)
一個(gè)接口返回報(bào)文,不應(yīng)該包含過(guò)多的數(shù)據(jù)量。
過(guò)多的數(shù)據(jù)量不僅處理復(fù)雜,并且數(shù)據(jù)量傳輸?shù)膲毫σ卜浅4蟆?/p>
如果數(shù)量實(shí)在是比較大,可以分頁(yè)返回,如果是功能不相關(guān)的報(bào)文,那應(yīng)該考慮接口拆分。
19. 好的接口實(shí)現(xiàn),離不開(kāi) SQL 優(yōu)化
我們做后端的,寫(xiě)好一個(gè)接口,離不開(kāi) SQL 優(yōu)化。
SQL 優(yōu)化從以下這幾個(gè)維度思考:
-
explain 分析 SQL 查詢(xún)計(jì)劃(重點(diǎn)關(guān)注 type、extra、filtered 字段)。 -
show profile 分析,了解 SQL 執(zhí)行的線程的狀態(tài)以及消耗的時(shí)間。 -
索引優(yōu)化(覆蓋索引、最左前綴原則、隱式轉(zhuǎn)換、order by 以及 group by 的優(yōu)化、join 優(yōu)化) -
大分頁(yè)問(wèn)題優(yōu)化(延遲關(guān)聯(lián)、記錄上一頁(yè)最大 ID) -
數(shù)據(jù)量太大(分庫(kù)分表、同步到 es,用 es 查詢(xún))
20. 代碼鎖的粒度控制好
什么是加鎖粒度呢?
其實(shí)就是你要鎖住的范圍是多大。比如你在家上衛(wèi)生間,你只要鎖住衛(wèi)生間就可以了吧,不需要將整個(gè)家都鎖起來(lái)不讓家人進(jìn)門(mén)吧,衛(wèi)生間就是你的加鎖粒度。
我們寫(xiě)代碼時(shí),如果不涉及到共享資源,就沒(méi)有必要鎖住。這就好像你上衛(wèi)生間,不用把整個(gè)家都鎖住,鎖住衛(wèi)生間門(mén)就可以了。
比如,在業(yè)務(wù)代碼中,有一個(gè) ArrayList 因?yàn)樯婕暗蕉嗑€程操作,所以需要加鎖操作,假設(shè)剛好又有一段比較耗時(shí)的操作(代碼中的slowNotShare
方法)不涉及線程安全問(wèn)題,你會(huì)如何加鎖呢?
反例:
//不涉及共享資源的慢方法
private?void?slowNotShare()?{
????try?{
????????TimeUnit.MILLISECONDS.sleep(100);
????}?catch?(InterruptedException?e)?{
????}
}
//錯(cuò)誤的加鎖方法
public?int?wrong()?{
????long?beginTime?=?System.currentTimeMillis();
????IntStream.rangeClosed(1,?10000).parallel().forEach(i?->?{
????????//加鎖粒度太粗了,slowNotShare其實(shí)不涉及共享資源
????????synchronized?(this)?{
????????????slowNotShare();
????????????data.add(i);
????????}
????});
????log.info("cosume?time:{}",?System.currentTimeMillis()?-?beginTime);
????return?data.size();
}
正例:
public?int?right()?{
????long?beginTime?=?System.currentTimeMillis();
????IntStream.rangeClosed(1,?10000).parallel().forEach(i?->?{
????????slowNotShare();//可以不加鎖
????????//只對(duì)List這部分加鎖
????????synchronized?(data)?{
????????????data.add(i);
????????}
????});
????log.info("cosume?time:{}",?System.currentTimeMillis()?-?beginTime);
????return?data.size();
}
21. 接口狀態(tài)和錯(cuò)誤需要統(tǒng)一明確
提供必要的接口調(diào)用狀態(tài)信息。比如一個(gè)轉(zhuǎn)賬接口調(diào)用是成功、失敗、處理中還是受理成功等,需要明確告訴客戶(hù)端。
如果接口失敗,那么具體失敗的原因是什么。這些必要的信息都必須要告訴給客戶(hù)端,因此需要定義明確的錯(cuò)誤碼和對(duì)應(yīng)的描述。
同時(shí),盡量對(duì)報(bào)錯(cuò)信息封裝一下,不要把后端的異常信息完全拋出到客戶(hù)端。
22. 接口要考慮異常處理
實(shí)現(xiàn)一個(gè)好的接口,離不開(kāi)優(yōu)雅的異常處理。
對(duì)于異常處理,提十個(gè)小建議:
-
盡量不要使用 e.printStackTrace()
,而是使用log
打印。因?yàn)?code>e.printStackTrace()語(yǔ)句可能會(huì)導(dǎo)致內(nèi)存占滿(mǎn)。 -
catch
住異常時(shí),建議打印出具體的exception
,利于更好定位問(wèn)題。 -
不要用一個(gè) Exception
捕捉所有可能的異常。 -
記得使用 finally
關(guān)閉流資源或者直接使用try-with-resource
。 -
捕獲異常與拋出異常必須是完全匹配,或者捕獲異常是拋異常的父類(lèi)。 -
捕獲到的異常,不能忽略它,至少打點(diǎn)日志吧。 -
注意異常對(duì)你的代碼層次結(jié)構(gòu)的侵染。 -
自定義封裝異常,不要丟棄原始異常的信息 Throwable cause
。 -
運(yùn)行時(shí)異常 RuntimeException
?,不應(yīng)該通過(guò)catch
的方式來(lái)處理,而是先預(yù)檢查,比如:NullPointerException
處理。 -
注意異常匹配的順序,優(yōu)先捕獲具體的異常。
23. 優(yōu)化程序邏輯
優(yōu)化程序邏輯這塊還是挺重要的,也就是說(shuō),你實(shí)現(xiàn)的業(yè)務(wù)代碼,如果是比較復(fù)雜的話,建議把注釋寫(xiě)清楚。還有,代碼邏輯盡量清晰,代碼盡量高效。
比如,要使用用戶(hù)信息的屬性,根據(jù) session 已經(jīng)獲取到
userId
了,然后就把用戶(hù)信息從數(shù)據(jù)庫(kù)查詢(xún)出來(lái),使用完后,后面可能又要用到用戶(hù)信息的屬性,有些小伙伴沒(méi)想太多,反手就把userId
再傳進(jìn)去,再查一次數(shù)據(jù)庫(kù)。。。我在項(xiàng)目中,見(jiàn)過(guò)這種代碼。。。直接把用戶(hù)對(duì)象傳下來(lái)不好嘛。。。
反例:
public?Response?test(Session?session){
????UserInfo?user?=?UserDao.queryByUserId(session.getUserId());
????
????if(user==null){
???????reutrn?new?Response();
????}
????
????return?do(session.getUserId());
}
public?Response?do(String?UserId){
??//多查了一次數(shù)據(jù)庫(kù)
??UserInfo?user?=?UserDao.queryByUserId(session.getUserId());
??......
??return?new?Response();?
}
正例:
public?Response?test(Session?session){
????UserInfo?user?=?UserDao.queryByUserId(session.getUserId());
????
????if(user==null){
???????reutrn?new?Response();
????}
????
????return?do(session.getUserId());
}
//直接傳UserInfo對(duì)象過(guò)來(lái)即可,不用再多查一次數(shù)據(jù)庫(kù)
public?Response?do(UserInfo?user){
??......
??return?new?Response();?
}
當(dāng)然,這只是一些很小的一個(gè)例子,還有很多類(lèi)似的例子,需要大家開(kāi)發(fā)過(guò)程中,多點(diǎn)思考的哈。
24. 接口實(shí)現(xiàn)過(guò)程中,注意大文件、大事務(wù)、大對(duì)象
-
讀取大文件時(shí),不要 Files.readAllBytes
直接讀取到內(nèi)存,這樣會(huì) OOM 的,建議使用BufferedReader
一行一行來(lái)。 -
大事務(wù)可能導(dǎo)致死鎖、回滾時(shí)間長(zhǎng)、主從延遲等問(wèn)題,開(kāi)發(fā)中盡量避免大事務(wù)。 -
注意一些大對(duì)象的使用,因?yàn)榇髮?duì)象是直接進(jìn)入老年代的,可能會(huì)觸發(fā) fullGC。
25. 你的接口,需要考慮限流
如果你的系統(tǒng)每秒扛住的請(qǐng)求是 1000,如果一秒鐘來(lái)了十萬(wàn)請(qǐng)求呢?換個(gè)角度就是說(shuō),高并發(fā)的時(shí)候,流量洪峰來(lái)了,超過(guò)系統(tǒng)的承載能力,怎么辦呢?
如果不采取措施,所有的請(qǐng)求打過(guò)來(lái),系統(tǒng) CPU、內(nèi)存、Load 負(fù)載飚得很高,最后請(qǐng)求處理不過(guò)來(lái),所有的請(qǐng)求無(wú)法正常響應(yīng)。
針對(duì)這種場(chǎng)景,我們可以采用限流方案。就是為了保護(hù)系統(tǒng),多余的請(qǐng)求,直接丟棄。
限流定義:
在計(jì)算機(jī)網(wǎng)絡(luò)中,限流就是控制網(wǎng)絡(luò)接口發(fā)送或接收請(qǐng)求的速率,它可防止 DoS 攻擊和限制 Web 爬蟲(chóng)。
限流,也稱(chēng)流量控制。是指系統(tǒng)在面臨高并發(fā),或者大流量請(qǐng)求的情況下,限制新的請(qǐng)求對(duì)系統(tǒng)的訪問(wèn),從而保證系統(tǒng)的穩(wěn)定性。
可以使用 Guava 的RateLimiter
單機(jī)版限流,也可以使用Redis
分布式限流,還可以使用阿里開(kāi)源組件sentinel
限流。
26. 代碼實(shí)現(xiàn)時(shí),注意運(yùn)行時(shí)異常(比如空指針、下標(biāo)越界等)
日常開(kāi)發(fā)中,我們需要采取措施規(guī)避數(shù)組邊界溢出、被零整除、空指針等運(yùn)行時(shí)錯(cuò)誤。類(lèi)似代碼比較常見(jiàn):
String?name?=?list.get(1).getName();?//list可能越界,因?yàn)椴灰欢ㄓ?個(gè)元素哈
應(yīng)該采取措施,預(yù)防一下數(shù)組邊界溢出。正例如下:
if(CollectionsUtil.isNotEmpty(list)&&?list.size()>1){
??String?name?=?list.get(1).getName();?
}
27. 保證接口安全性
如果你的 API 接口是對(duì)外提供的,需要保證接口的安全性。保證接口的安全性有?token 機(jī)制和接口簽名。
token 機(jī)制身份驗(yàn)證方案還比較簡(jiǎn)單的,就是:
-
客戶(hù)端發(fā)起請(qǐng)求,申請(qǐng)獲取 token。 -
服務(wù)端生成全局唯一的 token,保存到 redis 中(一般會(huì)設(shè)置一個(gè)過(guò)期時(shí)間),然后返回給客戶(hù)端。 -
客戶(hù)端帶著 token,發(fā)起請(qǐng)求。 -
服務(wù)端去 redis 確認(rèn) token 是否存在,一般用 redis.del(token) 的方式,如果存在會(huì)刪除成功,即處理業(yè)務(wù)邏輯,如果刪除失敗不處理業(yè)務(wù)邏輯,直接返回結(jié)果。
接口簽名的方式,就是把接口請(qǐng)求相關(guān)信息(請(qǐng)求報(bào)文,包括請(qǐng)求時(shí)間戳、版本號(hào)、appid 等),客戶(hù)端私鑰加簽,然后服務(wù)端用公鑰驗(yàn)簽,驗(yàn)證通過(guò)才認(rèn)為是合法的、沒(méi)有被篡改過(guò)的請(qǐng)求。
除了加簽驗(yàn)簽和 token 機(jī)制,接口報(bào)文一般是要加密的。當(dāng)然,用 https 協(xié)議是會(huì)對(duì)報(bào)文加密的。如果是我們服務(wù)層的話,如何加解密呢?
可以參考 HTTPS 的原理,就是服務(wù)端把公鑰給客戶(hù)端,然后客戶(hù)端生成對(duì)稱(chēng)密鑰,
接著客戶(hù)端用服務(wù)端的公鑰加密對(duì)稱(chēng)密鑰,再發(fā)到服務(wù)端,服務(wù)端用自己的私鑰解密,得到客戶(hù)端的對(duì)稱(chēng)密鑰。
這時(shí)候就可以愉快傳輸報(bào)文啦,客戶(hù)端用對(duì)稱(chēng)密鑰加密請(qǐng)求報(bào)文,服務(wù)端用對(duì)應(yīng)的對(duì)稱(chēng)密鑰解密報(bào)文。
有時(shí)候,接口的安全性,還包括手機(jī)號(hào)、身份證等信息的脫敏。就是說(shuō),用戶(hù)的隱私數(shù)據(jù),不能隨便暴露。
28. 分布式事務(wù),如何保證
分布式事務(wù):就是指事務(wù)的參與者、支持事務(wù)的服務(wù)器、資源服務(wù)器以及事務(wù)管理器分別位于不同的分布式系統(tǒng)的不同節(jié)點(diǎn)之上。簡(jiǎn)單來(lái)說(shuō),分布式事務(wù)指的就是分布式系統(tǒng)中的事務(wù),它的存在就是為了保證不同數(shù)據(jù)庫(kù)節(jié)點(diǎn)的數(shù)據(jù)一致性。
分布式事務(wù)的幾種解決方案:
-
2PC(二階段提交)方案、3PC -
TCC(Try、Confirm、Cancel) -
本地消息表 -
最大努力通知 -
seata
29. 事務(wù)失效的一些經(jīng)典場(chǎng)景
我們的接口開(kāi)發(fā)過(guò)程中,經(jīng)常需要使用到事務(wù)。所以需要避開(kāi)事務(wù)失效的一些經(jīng)典場(chǎng)景。
-
方法的訪問(wèn)權(quán)限必須是 public,其他 private 等權(quán)限,事務(wù)失效。 -
方法被定義成了 final 的,這樣會(huì)導(dǎo)致事務(wù)失效。 -
在同一個(gè)類(lèi)中的方法直接內(nèi)部調(diào)用,會(huì)導(dǎo)致事務(wù)失效。 -
一個(gè)方法如果沒(méi)交給 spring 管理,就不會(huì)生成 spring 事務(wù)。 -
多線程調(diào)用,兩個(gè)方法不在同一個(gè)線程中,獲取到的數(shù)據(jù)庫(kù)連接不一樣的。 -
表的存儲(chǔ)引擎不支持事務(wù)。 -
如果自己 try...catch 誤吞了異常,事務(wù)失效。 -
錯(cuò)誤的傳播特性。
30. 掌握常用的設(shè)計(jì)模式
把代碼寫(xiě)好,還是需要熟練常用的設(shè)計(jì)模式,比如策略模式、工廠模式、模板方法模式、觀察者模式等等。
設(shè)計(jì)模式,是代碼設(shè)計(jì)經(jīng)驗(yàn)的總結(jié)。使用設(shè)計(jì)模式可以重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。
31. 寫(xiě)代碼時(shí),考慮線性安全問(wèn)題
在高并發(fā)情況下,HashMap
可能會(huì)出現(xiàn)死循環(huán)。因?yàn)樗欠蔷€性安全的,可以考慮使用ConcurrentHashMap
。所以這個(gè)也盡量養(yǎng)成習(xí)慣,不要上來(lái)反手就是一個(gè)new HashMap()
。
Hashmap、Arraylist、LinkedList、TreeMap 等都是線性不安全的。 Vector、Hashtable、ConcurrentHashMap 等都是線性安全的。
32. 接口定義清晰易懂,命名規(guī)范
我們寫(xiě)代碼,不僅僅是為了實(shí)現(xiàn)當(dāng)前的功能,也要有利于后面的維護(hù)。
說(shuō)到維護(hù),代碼不僅僅是寫(xiě)給自己看的,也是給別人看的。
所以接口定義要清晰易懂,命名規(guī)范。
33. 接口的版本控制
接口要做好版本控制。就是說(shuō),請(qǐng)求基礎(chǔ)報(bào)文,應(yīng)該包含version
接口版本號(hào)字段,方便未來(lái)做接口兼容。其實(shí)這個(gè)點(diǎn)也算接口擴(kuò)展性的一個(gè)體現(xiàn)點(diǎn)吧。
比如客戶(hù)端 APP 某個(gè)功能優(yōu)化了,新老版本會(huì)共存,這時(shí)候我們的version
版本號(hào)就派上用場(chǎng)了,對(duì)version
做升級(jí),做好版本控制。
34. 注意代碼規(guī)范問(wèn)題
注意一些常見(jiàn)的代碼壞味道:
-
大量重復(fù)代碼(抽共用方法,設(shè)計(jì)模式)。 -
方法參數(shù)過(guò)多(可封裝成一個(gè) DTO 對(duì)象)。 -
方法過(guò)長(zhǎng)(抽小函數(shù))。 -
判斷條件太多(優(yōu)化 if...else)。 -
不處理沒(méi)用的代碼。 -
不注重代碼格式。 -
避免過(guò)度設(shè)計(jì)。
35. 保證接口正確性,其實(shí)就是保證更少的 bug
保證接口的正確性,換個(gè)角度講,就是保證更少的bug,甚至是沒(méi)有bug。所以接口開(kāi)發(fā)完后,一般需要開(kāi)發(fā)自測(cè)一下。
然后的話,接口的正確性還體現(xiàn)在,多線程并發(fā)的時(shí)候,保證數(shù)據(jù)的正確性,等等。比如做一筆轉(zhuǎn)賬交易,扣減余額的時(shí)候,可以通過(guò) CAS 樂(lè)觀鎖的方式保證余額扣減正確。
如果你是實(shí)現(xiàn)秒殺接口,得防止超賣(mài)問(wèn)題吧??梢允褂?Redis 分布式鎖防止超賣(mài)問(wèn)題。
36. 學(xué)會(huì)溝通,跟前端溝通,跟產(chǎn)品溝通
我把這一點(diǎn)放到最后,學(xué)會(huì)溝通是非常非常重要的。
比如你開(kāi)發(fā)定義接口時(shí),一定不能上來(lái)就自己埋頭把接口定義完了,需要跟客戶(hù)端先對(duì)齊接口。遇到一些難點(diǎn)時(shí),跟技術(shù) leader 對(duì)齊方案。
實(shí)現(xiàn)需求的過(guò)程中,有什么問(wèn)題,及時(shí)跟產(chǎn)品溝通。
總之就是,開(kāi)發(fā)接口過(guò)程中,一定要溝通好~
?
本文摘自 :https://www.cnblogs.com/