2. 修改老接口時(shí),注意接口的兼容性
3. 設(shè)計(jì)接口時(shí),充分考慮接口的可擴(kuò)展性
4. 接口考慮是否需要防重處理
5. 重點(diǎn)接口,考慮線程池隔離
6. 調(diào)用第三方接口要考慮異常和超時(shí)處理
7. 接口實(shí)現(xiàn)考慮熔斷和降級(jí)
8. 日志打印好,接口的關(guān)鍵代碼,要有日志保駕護(hù)航
9. 接口的功能定義要具備單一性
10. 接口有些場(chǎng)景,使用異步更合理
11. 優(yōu)化接口耗時(shí),遠(yuǎn)程串行考慮改并行調(diào)用
12. 接口合并或者說(shuō)考慮批量處理思想
13. 接口實(shí)現(xiàn)過(guò)程中,恰當(dāng)使用緩存
14. 接口考慮熱點(diǎn)數(shù)據(jù)隔離性
15. 可變參數(shù)配置化,比如紅包皮膚切換等
16. 接口考慮冪等性
17. 讀寫(xiě)分離,優(yōu)先考慮讀從庫(kù),注意主從延遲問(wèn)題
18. 接口注意返回的數(shù)據(jù)量,如果數(shù)據(jù)量大需要分頁(yè)
19. 好的接口實(shí)現(xiàn),離不開(kāi) SQL 優(yōu)化
20. 代碼鎖的粒度控制好
21. 接口狀態(tài)和錯(cuò)誤需要統(tǒng)一明確
22. 接口要考慮異常處理
23. 優(yōu)化程序邏輯
24. 接口實(shí)現(xiàn)過(guò)程中,注意大文件、大事務(wù)、大對(duì)象
25. 你的接口,需要考慮限流
26. 代碼實(shí)現(xiàn)時(shí),注意運(yùn)行時(shí)異常(比如空指針、下標(biāo)越界等)
27. 保證接口安全性
28. 分布式事務(wù),如何保證
29. 事務(wù)失效的一些經(jīng)典場(chǎng)景
30. 掌握常用的設(shè)計(jì)模式
31. 寫(xiě)代碼時(shí),考慮線性安全問(wèn)題
32. 接口定義清晰易懂,命名規(guī)范
33. 接口的版本控制
34. 注意代碼規(guī)范問(wèn)題
35. 保證接口正確性,其實(shí)就是保證更少的 bug
36. 學(xué)會(huì)溝通,跟前端溝通,跟產(chǎn)品溝通
作為后端開(kāi)發(fā),不管是什么語(yǔ)言,Java、Go還是C++,其背后的后端思想都是類似的。
后端開(kāi)發(fā)工程師,主要工作就是:如何把一個(gè)接口設(shè)計(jì)好 。
今天就給大家介紹,設(shè)計(jì)好接口的 36 個(gè)錦囊。
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)槌绦蚰承┰?,返回別人一個(gè)null值。
2. 修改老接口時(shí),注意接口的兼容性
很多 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,就可以考慮這樣處理:
//老接口 voidoldService(A,B){ //兼容新接口,傳個(gè)null代替C newService(A,B,null); } //新接口,暫時(shí)不能刪掉老接口,需要做兼容。 voidnewService(A,B,C){ ... }
3. 設(shè)計(jì)接口時(shí),充分考慮接口的可擴(kuò)展性
要根據(jù)實(shí)際業(yè)務(wù)場(chǎng)景設(shè)計(jì)接口,充分考慮接口的可擴(kuò)展性。
比如你接到一個(gè)需求:用戶添加或者修改員工時(shí),需要刷臉。那你是反手提供一個(gè)員工管理的提交刷臉信息接口呢?還是先思考:提交刷臉是不是通用流程呢?比如轉(zhuǎn)賬或者一鍵貼現(xiàn)需要接入刷臉的話,你是否需要重新實(shí)現(xiàn)一個(gè)接口呢?還是當(dāng)前按業(yè)務(wù)類型劃分模塊,復(fù)用這個(gè)接口就好,保留接口的可擴(kuò)展性。
如果按模塊劃分的話,未來(lái)如果其他場(chǎng)景比如一鍵貼現(xiàn)接入刷臉的話,不用再搞一套新的接口,只需要新增枚舉,然后復(fù)用刷臉通過(guò)流程接口,實(shí)現(xiàn)一鍵貼現(xiàn)刷臉的差異化即可。
4. 接口考慮是否需要防重處理
如果前端重復(fù)請(qǐng)求,你的邏輯如何處理?是不是考慮接口去重處理。
當(dāng)然,如果是查詢類的請(qǐng)求,其實(shí)不用防重。如果是更新修改類的話,尤其金融轉(zhuǎn)賬類的,就要過(guò)濾重復(fù)請(qǐng)求了。
簡(jiǎn)單點(diǎn),你可以使用 Redis 防重復(fù)請(qǐng)求,同樣的請(qǐng)求方,一定時(shí)間間隔內(nèi)的相同請(qǐng)求,考慮是否過(guò)濾。當(dāng)然,轉(zhuǎn)賬類接口,并發(fā)不高的話,推薦使用數(shù)據(jù)庫(kù)防重 表,以唯一流 水號(hào)作為主鍵或者唯一索引 。
5. 重點(diǎn)接口,考慮線程池隔離
一些登錄、轉(zhuǎn)賬交易、下單等重要接口,考慮線程池隔離。
如果你所有業(yè)務(wù)都共用一個(gè)線程池,有些業(yè)務(wù)出 bug 導(dǎo)致線程池阻塞打滿的話,那就杯具了,所有業(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)象被稱為服務(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)失敗了,接著客戶投訴,然后你還沒(méi)有打印到日志,想想那種水深火熱的困境下,你卻毫無(wú)辦法。。。
那么,你的轉(zhuǎn)賬業(yè)務(wù)都需要哪些日志信息呢?至少,方法調(diào)用前,入?yún)⑿枰蛴“?,接口調(diào)用后,需要捕獲一下異常吧,同時(shí)打印異常相關(guān)日志,如下:
publicvoidtransfer(TransferDTOtransferDTO){ log.info("invoketranferbegin"); //打印入?yún)?log.info("invoketranfer,paramters:{}",transferDTO); try{ res=transferService.transfer(transferDTO); }catch(Exceptione){ log.error("transferfail,account:{}", transferDTO.getAccount()) log.error("transferfail,exception:{}",e); } log.info("invoketranferend"); }
9. 接口的功能定義要具備單一性
單一性是指接口做的事情比較單一、專一。比如一個(gè)登錄接口,它做的事情就只是校驗(yàn)賬戶名密碼,然后返回登錄成功以及userId即可。但是如果為了減少接口交互,把一些注冊(cè)、一些配置查詢等全放到登錄接口,就不太妥。
其實(shí)這也是微服務(wù)一些思想,接口的功能單一、明確。比如訂單服務(wù)、積分、商品信息相關(guān)的接口都是劃分開(kāi)的。將來(lái)拆分微服務(wù)的話,是不是就比較簡(jiǎn)便啦。
10. 接口有些場(chǎng)景,使用異步更合理
舉個(gè)簡(jiǎn)單的例子,比如實(shí)現(xiàn)一個(gè)用戶注冊(cè)的接口,用戶注冊(cè)成功時(shí),發(fā)個(gè)郵件或者短信去通知用戶。這個(gè)郵件或者發(fā)短信,就更適合異步處理。因?yàn)榭偛荒芤粋€(gè)通知類的失敗,導(dǎo)致注冊(cè)失敗吧。
至于做異步的方式,簡(jiǎn)單的就是用線程池 。還可以使用消息隊(duì)列,就是用戶注冊(cè)成功后,生產(chǎn)者產(chǎn)生一個(gè)注冊(cè)成功的消息,消費(fèi)者拉到注冊(cè)成功的消息,就發(fā)送通知。
不是所有的接口都適合設(shè)計(jì)為同步接口。比如你要做一個(gè)轉(zhuǎn)賬的功能,如果是單筆的轉(zhuǎn)賬,你是可以把接口設(shè)計(jì)同步。用戶發(fā)起轉(zhuǎn)賬時(shí),客戶端再靜靜等待轉(zhuǎn)賬結(jié)果就好。如果是批量轉(zhuǎn)賬,一個(gè)批次一千筆,甚至一萬(wàn)筆的,你則可以把接口設(shè)計(jì)為異步。就是用戶發(fā)起批量轉(zhuǎn)賬時(shí),持久化成功就先返回受理成功。然后用戶隔十分鐘或者十五分鐘再來(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è)的接口,它需要查用戶信息、需要查 banner 信息、需要查彈窗信息等等。那是一個(gè)一個(gè)接口串行調(diào),還是并行調(diào)用呢?
如果是串行一個(gè)一個(gè)查,比如查用戶信息 200ms,查 banner 信息 100ms、查彈窗信息 50ms,那一共就耗時(shí)350ms了,如果還查其他信息,那耗時(shí)就更大了。這種場(chǎng)景是可以改為并行調(diào)用的。也就是說(shuō)查用戶信息、查 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)用也類似想法,比如你查詢營(yíng)銷標(biāo)簽是否命中,可以一個(gè)標(biāo)簽一個(gè)標(biāo)簽去查,也可以批量標(biāo)簽去查,那批量進(jìn)行,效率就更高嘛。
//反例 for(inti=0;i
小伙伴們是否了解過(guò)kafka為什么這么快呢?其實(shí)其中一點(diǎn)原因,就是 kafka 使用批量消息 提升服務(wù)端處理能力。
13. 接口實(shí)現(xiàn)過(guò)程中,恰當(dāng)使用緩存
哪些場(chǎng)景適合使用緩存?讀多寫(xiě)少且數(shù)據(jù)時(shí)效要求越低的場(chǎng)景 。
緩存用得好,可以承載更多的請(qǐng)求,提升查詢效率,減少數(shù)據(jù)庫(kù)的壓力。
比如一些平時(shí)變動(dòng)很小或者說(shuō)幾乎不會(huì)變的商品信息,可以放到緩存,請(qǐng)求過(guò)來(lái)時(shí),先查詢緩存,如果沒(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)、查詢?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)隔離、用戶隔離、數(shù)據(jù)隔離 等。
業(yè)務(wù)隔離,比如 12306 的分時(shí)段售票,將熱點(diǎn)數(shù)據(jù)分散處理,降低系統(tǒng)負(fù)載壓力。
系統(tǒng)隔離:比如把系統(tǒng)分成了用戶、商品、社區(qū)三個(gè)板塊。這三個(gè)塊分別使用不同的域名、服務(wù)器和數(shù)據(jù)庫(kù),做到從接入層到應(yīng)用層再到數(shù)據(jù)層三層完全隔離。
用戶隔離:重點(diǎn)用戶請(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ě)死控制,可有類似以下代碼:
if(duringChristmas){ img=redPacketChristmasSkin; }elseif(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)景,就是用戶連著點(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í)候,它們的處理流程、方案是類似的。
接口冪等實(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 查詢計(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 查詢)
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ì)如何加鎖呢?
反例:
//不涉及共享資源的慢方法 privatevoidslowNotShare(){ try{ TimeUnit.MILLISECONDS.sleep(100); }catch(InterruptedExceptione){ } } //錯(cuò)誤的加鎖方法 publicintwrong(){ longbeginTime=System.currentTimeMillis(); IntStream.rangeClosed(1,10000).parallel().forEach(i->{ //加鎖粒度太粗了,slowNotShare其實(shí)不涉及共享資源 synchronized(this){ slowNotShare(); data.add(i); } }); log.info("cosumetime:{}",System.currentTimeMillis()-beginTime); returndata.size(); }
正例:
publicintright(){ longbeginTime=System.currentTimeMillis(); IntStream.rangeClosed(1,10000).parallel().forEach(i->{ slowNotShare();//可以不加鎖 //只對(duì)List這部分加鎖 synchronized(data){ data.add(i); } }); log.info("cosumetime:{}",System.currentTimeMillis()-beginTime); returndata.size(); }
21. 接口狀態(tài)和錯(cuò)誤需要統(tǒng)一明確
提供必要的接口調(diào)用狀態(tài)信息。比如一個(gè)轉(zhuǎn)賬接口調(diào)用是成功、失敗、處理中還是受理成功等,需要明確告訴客戶端。如果接口失敗,那么具體失敗的原因是什么。這些必要的信息都必須要告訴給客戶端,因此需要定義明確的錯(cuò)誤碼和對(duì)應(yīng)的描述。同時(shí),盡量對(duì)報(bào)錯(cuò)信息封裝一下,不要把后端的異常信息完全拋出到客戶端。
22. 接口要考慮異常處理
實(shí)現(xiàn)一個(gè)好的接口,離不開(kāi)優(yōu)雅的異常處理。
對(duì)于異常處理,提十個(gè)小建議:
盡量不要使用e.printStackTrace(),而是使用log打印。因?yàn)閑.printStackTrace()語(yǔ)句可能會(huì)導(dǎo)致內(nèi)存占滿。
catch住異常時(shí),建議打印出具體的exception,利于更好定位問(wèn)題。
不要用一個(gè)Exception捕捉所有可能的異常。
記得使用finally關(guān)閉流資源或者直接使用try-with-resource。
捕獲異常與拋出異常必須是完全匹配,或者捕獲異常是拋異常的父類。
捕獲到的異常,不能忽略它,至少打點(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ě)清楚 。還有,代碼邏輯盡量清晰,代碼盡量高效。
比如,要使用用戶信息的屬性,根據(jù) session 已經(jīng)獲取到userId了,然后就把用戶信息從數(shù)據(jù)庫(kù)查詢出來(lái),使用完后,后面可能又要用到用戶信息的屬性,有些小伙伴沒(méi)想太多,反手就把userId再傳進(jìn)去,再查一次數(shù)據(jù)庫(kù)。。。我在項(xiàng)目中,見(jiàn)過(guò)這種代碼。。。直接把用戶對(duì)象傳下來(lái)不好嘛。。。
反例:
publicResponsetest(Sessionsession){ UserInfouser=UserDao.queryByUserId(session.getUserId()); if(user==null){ reutrnnewResponse(); } returndo(session.getUserId()); } publicResponsedo(StringUserId){ //多查了一次數(shù)據(jù)庫(kù) UserInfouser=UserDao.queryByUserId(session.getUserId()); ...... returnnewResponse(); }
正例:
publicResponsetest(Sessionsession){ UserInfouser=UserDao.queryByUserId(session.getUserId()); if(user==null){ reutrnnewResponse(); } returndo(session.getUserId()); } //直接傳UserInfo對(duì)象過(guò)來(lái)即可,不用再多查一次數(shù)據(jù)庫(kù) publicResponsedo(UserInfouser){ ...... returnnewResponse(); }
當(dāng)然,這只是一些很小的一個(gè)例子,還有很多類似的例子,需要大家開(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)。限流,也稱流量控制。是指系統(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ò)誤。類似代碼比較常見(jiàn):
Stringname=list.get(1).getName();//list可能越界,因?yàn)椴灰欢ㄓ?個(gè)元素哈
應(yīng)該采取措施,預(yù)防一下數(shù)組邊界溢出。正例如下:
if(CollectionsUtil.isNotEmpty(list)&&list.size()>1){ Stringname=list.get(1).getName(); }
27. 保證接口安全性
如果你的 API 接口是對(duì)外提供的,需要保證接口的安全性。保證接口的安全性有 token 機(jī)制和接口簽名 。
token 機(jī)制身份驗(yàn)證 方案還比較簡(jiǎn)單的,就是:
客戶端發(fā)起請(qǐng)求,申請(qǐng)獲取 token。
服務(wù)端生成全局唯一的 token,保存到 redis 中(一般會(huì)設(shè)置一個(gè)過(guò)期時(shí)間),然后返回給客戶端。
客戶端帶著 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 等),客戶端私鑰加簽,然后服務(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ù)端把公鑰給客戶端,然后客戶端生成對(duì)稱密鑰,接著客戶端用服務(wù)端的公鑰加密對(duì)稱密鑰,再發(fā)到服務(wù)端,服務(wù)端用自己的私鑰解密,得到客戶端的對(duì)稱密鑰。這時(shí)候就可以愉快傳輸報(bào)文啦,客戶端用對(duì)稱密鑰加密請(qǐng)求報(bào)文 ,服務(wù)端用對(duì)應(yīng)的對(duì)稱密鑰解密報(bào)文 。
有時(shí)候,接口的安全性,還包括手機(jī)號(hào)、身份證等信息的脫敏 。就是說(shuō),用戶的隱私數(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è)類中的方法直接內(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)吧。
比如客戶端 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)就自己埋頭把接口定義完了 ,需要跟客戶端先對(duì)齊接口 。遇到一些難點(diǎn)時(shí),跟技術(shù) leader 對(duì)齊方案。實(shí)現(xiàn)需求的過(guò)程中,有什么問(wèn)題,及時(shí)跟產(chǎn)品溝通。
總之就是,開(kāi)發(fā)接口過(guò)程中,一定要溝通好~
來(lái)源:撿田螺的小男孩 在此特別鳴謝!
-
接口
+關(guān)注
關(guān)注
33文章
8615瀏覽量
151304 -
數(shù)據(jù)庫(kù)
+關(guān)注
關(guān)注
7文章
3816瀏覽量
64458 -
指針
+關(guān)注
關(guān)注
1文章
480瀏覽量
70576
原文標(biāo)題:為什么有公司規(guī)定所有接口都必須用Post?
文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論