<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          后端思想篇:設(shè)計好接口的36個錦囊!

          共 8720字,需瀏覽 18分鐘

           ·

          2022-05-12 22:31


          源?/?? ? ? ??文/?


          前言

          大家好,我是撿田螺的小男孩。作為后端開發(fā),不管是什么語言,JavaGo還是C++,其背后的后端思想都是類似的。后面打算出一個后端思想的技術(shù)專欄,主要包括后端的一些設(shè)計、或者后端規(guī)范相關(guān)的,希望對大家日常工作有幫助哈。

          我們做后端開發(fā)工程師,主要工作就是:如何把一個接口設(shè)計好。所以,今天就給大家介紹,設(shè)計好接口的36個錦囊。本文就是后端思想專欄的第一篇哈。

          1. 接口參數(shù)校驗

          入?yún)⒊鰠⑿r炇敲總€程序員必備的基本素養(yǎng)。你設(shè)計的接口,必須先校驗參數(shù)。比如入?yún)⑹欠裨试S為空,入?yún)㈤L度是否符合你的預(yù)期長度。這個要養(yǎng)成習(xí)慣哈,日常開發(fā)中,很多低級bug都是不校驗參數(shù)導(dǎo)致的。

          比如你的數(shù)據(jù)庫表字段設(shè)置為varchar(16),對方傳了一個32位的字符串過來,如果你不校驗參數(shù),插入數(shù)據(jù)庫直接異常了

          出參也是,比如你定義的接口報文,參數(shù)是不為空的,但是你的接口返回參數(shù),沒有做校驗,因為程序某些原因,直返回別人一個null值。。。


          2. 修改老接口時,注意接口的兼容性

          很多bug都是因為修改了對外舊接口,但是卻不做兼容導(dǎo)致的。關(guān)鍵這個問題多數(shù)是比較嚴重的,可能直接導(dǎo)致系統(tǒng)發(fā)版失敗的。新手程序員很容易犯這個錯誤哦~

          所以,如果你的需求是在原來接口上修改,尤其這個接口是對外提供服務(wù)的話,一定要考慮接口兼容。舉個例子吧,比如dubbo接口,原本是只接收A,B參數(shù),現(xiàn)在你加了一個參數(shù)C,就可以考慮這樣處理:

          //老接口
          void?oldService(A,B){
          ??//兼容新接口,傳個null代替C
          ??newService(A,B,null);
          }

          //新接口,暫時不能刪掉老接口,需要做兼容。
          void?newService(A,B,C){
          ??...
          }

          3. ?設(shè)計接口時,充分考慮接口的可擴展性

          要根據(jù)實際業(yè)務(wù)場景設(shè)計接口,充分考慮接口的可擴展性。

          比如你接到一個需求:是用戶添加或者修改員工時,需要刷臉。那你是反手提供一個員工管理的提交刷臉信息接口?還是先思考:提交刷臉是不是通用流程呢?比如轉(zhuǎn)賬或者一鍵貼現(xiàn)需要接入刷臉的話,你是否需要重新實現(xiàn)一個接口呢?還是當(dāng)前按業(yè)務(wù)類型劃分模塊,復(fù)用這個接口就好,保留接口的可擴展性。

          如果按模塊劃分的話,未來如果其他場景比如一鍵貼現(xiàn)接入刷臉的話,不用再搞一套新的接口,只需要新增枚舉,然后復(fù)用刷臉通過流程接口,實現(xiàn)一鍵貼現(xiàn)刷臉的差異化即可。

          4.接口考慮是否需要防重處理

          如果前端重復(fù)請求,你的邏輯如何處理?是不是考慮接口去重處理。

          當(dāng)然,如果是查詢類的請求,其實不用防重。如果是更新修改類的話,尤其金融轉(zhuǎn)賬類的,就要過濾重復(fù)請求了。簡單點,你可以使用Redis防重復(fù)請求,同樣的請求方,一定時間間隔內(nèi)的相同請求,考慮是否過濾。當(dāng)然,轉(zhuǎn)賬類接口,并發(fā)不高的話,推薦使用數(shù)據(jù)庫防重表,以唯一流水號作為主鍵或者唯一索引。

          5. 重點接口,考慮線程池隔離。

          一些登陸、轉(zhuǎn)賬交易、下單等重要接口,考慮線程池隔離哈。如果你所有業(yè)務(wù)都共用一個線程池,有些業(yè)務(wù)出bug導(dǎo)致線程池阻塞打滿的話,那就杯具了,所有業(yè)務(wù)都影響了。因此進行線程池隔離,重要業(yè)務(wù)分配多一點的核心線程,就更好保護重要業(yè)務(wù)。

          6. 調(diào)用第三方接口要考慮異常和超時處理

          如果你調(diào)用第三方接口,或者分布式遠程服務(wù)的的話,需要考慮:

          • 異常處理

          比如,你調(diào)別人的接口,如果異常了,怎么處理,是重試還是當(dāng)做失敗還是告警處理。

          • 接口超時

          沒法預(yù)估對方接口一般多久返回,一般設(shè)置個超時斷開時間,以保護你的接口。之前見過一個生產(chǎn)問題,就是http調(diào)用不設(shè)置超時時間,最后響應(yīng)方進程假死,請求一直占著線程不釋放,拖垮線程池。

          • 重試次數(shù)

          你的接口調(diào)失敗,需不需要重試?重試幾次?需要站在業(yè)務(wù)上角度思考這個問題


          7. 接口實現(xiàn)考慮熔斷和降級

          當(dāng)前互聯(lián)網(wǎng)系統(tǒng)一般都是分布式部署的。而分布式系統(tǒng)中經(jīng)常會出現(xiàn)某個基礎(chǔ)服務(wù)不可用,最終導(dǎo)致整個系統(tǒng)不可用的情況, 這種現(xiàn)象被稱為服務(wù)雪崩效應(yīng)。

          比如分布式調(diào)用鏈路A->B->C....,下圖所示:

          如果服務(wù)C出現(xiàn)問題,比如是因為慢SQL導(dǎo)致調(diào)用緩慢,那將導(dǎo)致B也會延遲,從而A也會延遲。堵住的A請求會消耗占用系統(tǒng)的線程、IO等資源。當(dāng)請求A的服務(wù)越來越多,占用計算機的資源也越來越多,最終會導(dǎo)致系統(tǒng)瓶頸出現(xiàn),造成其他的請求同樣不可用,最后導(dǎo)致業(yè)務(wù)系統(tǒng)崩潰。

          為了應(yīng)對服務(wù)雪崩, 常見的做法是熔斷和降級。最簡單是加開關(guān)控制,當(dāng)下游系統(tǒng)出問題時,開關(guān)降級,不再調(diào)用下游系統(tǒng)。還可以選用開源組件Hystrix。

          8. 日志打印好,接口的關(guān)鍵代碼,要有日志保駕護航。

          關(guān)鍵業(yè)務(wù)代碼無論身處何地,都應(yīng)該有足夠的日志保駕護航。比如:你實現(xiàn)轉(zhuǎn)賬業(yè)務(wù),轉(zhuǎn)個幾百萬,然后轉(zhuǎn)失敗了,接著客戶投訴,然后你還沒有打印到日志,想想那種水深火熱的困境下,你卻毫無辦法。。。

          那么,你的轉(zhuǎn)賬業(yè)務(wù)都需要哪些日志信息呢?至少,方法調(diào)用前,入?yún)⑿枰蛴⌒枰?,接口調(diào)用后,需要捕獲一下異常吧,同時打印異常相關(guān)日志吧,如下:

          public?void?transfer(TransferDTO?transferDTO){
          ????log.info("invoke?tranfer?begin");
          ????//打印入?yún)?br>????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");
          ????}

          之前寫過一篇打印日志的15個建議,大家可以看看哈:工作總結(jié)!日志打印的15個建議

          9. 接口的功能定義要具備單一性

          單一性是指接口做的事情比較單一、專一。比如一個登陸接口,它做的事情就只是校驗賬戶名密碼,然后返回登陸成功以及userId即可。但是如果你為了減少接口交互,把一些注冊、一些配置查詢等全放到登陸接口,就不太妥。

          其實這也是微服務(wù)一些思想,接口的功能單一、明確。比如訂單服務(wù)、積分、商品信息相關(guān)的接口都是劃分開的。將來拆分微服務(wù)的話,是不是就比較簡便啦。

          10.接口有些場景,使用異步更合理

          舉個簡單的例子,比如你實現(xiàn)一個用戶注冊的接口。用戶注冊成功時,發(fā)個郵件或者短信去通知用戶。這個郵件或者發(fā)短信,就更適合異步處理。因為總不能一個通知類的失敗,導(dǎo)致注冊失敗吧。

          至于做異步的方式,簡單的就是用線程池。還可以使用消息隊列,就是用戶注冊成功后,生產(chǎn)者產(chǎn)生一個注冊成功的消息,消費者拉到注冊成功的消息,就發(fā)送通知。

          不是所有的接口都適合設(shè)計為同步接口。比如你要做一個轉(zhuǎn)賬的功能,如果你是單筆的轉(zhuǎn)賬,你是可以把接口設(shè)計同步。用戶發(fā)起轉(zhuǎn)賬時,客戶端在靜靜等待轉(zhuǎn)賬結(jié)果就好。如果你是批量轉(zhuǎn)賬,一個批次一千筆,甚至一萬筆的,你則可以把接口設(shè)計為異步。就是用戶發(fā)起批量轉(zhuǎn)賬時,持久化成功就先返回受理成功。然后用戶隔十分鐘或者十五分鐘等再來查轉(zhuǎn)賬結(jié)果就好。又或者,批量轉(zhuǎn)賬成功后,再回調(diào)上游系統(tǒng)。

          11. 優(yōu)化接口耗時,遠程串行考慮改并行調(diào)用

          假設(shè)我們設(shè)計一個APP首頁的接口,它需要查用戶信息、需要查banner信息、需要查彈窗信息等等。那你是一個一個接口串行調(diào),還是并行調(diào)用呢?

          如果是串行一個一個查,比如查用戶信息200ms,查banner信息100ms、查彈窗信息50ms,那一共就耗時350ms了,如果還查其他信息,那耗時就更大了。這種場景是可以改為并行調(diào)用的。也就是說查用戶信息、查banner信息、查彈窗信息,可以同時發(fā)起。

          在Java中有個異步編程利器:CompletableFuture,就可以很好實現(xiàn)這個功能。有興趣的小伙伴可以看我之前這個文章哈:CompletableFuture詳解

          12. 接口合并或者說考慮批量處理思想

          數(shù)據(jù)庫操作或或者是遠程調(diào)用時,能批量操作就不要for循環(huán)調(diào)用。

          一個簡單例子,我們平時一個列表明細數(shù)據(jù)插入數(shù)據(jù)庫時,不要在for循環(huán)一條一條插入,建議一個批次幾百條,進行批量插入。同理遠程調(diào)用也類似想法,比如你查詢營銷標簽是否命中,可以一個標簽一個標簽去查,也可以批量標簽去查,那批量進行,效率就更高嘛。

          //反例
          for(int?i=0;i??remoteSingleQuery(param)
          }

          //正例
          remoteBatchQuery(param);

          小伙伴們是否了解過kafka為什么這么快呢?其實其中一點原因,就是kafka使用批量消息提升服務(wù)端處理能力。

          13. 接口實現(xiàn)過程中,恰當(dāng)使用緩存

          哪些場景適合使用緩存?讀多寫少且數(shù)據(jù)時效要求越低的場景

          緩存用得好,可以承載更多的請求,提升查詢效率,減少數(shù)據(jù)庫的壓力。

          比如一些平時變動很小或者說幾乎不會變的商品信息,可以放到緩存,請求過來時,先查詢緩存,如果沒有再查數(shù)據(jù)庫,并且把數(shù)據(jù)庫的數(shù)據(jù)更新到緩存。但是,使用緩存增加了需要考慮這些點:緩存和數(shù)據(jù)庫一致性如何保證、集群、緩存擊穿、緩存雪崩、緩存穿透等問題。

          • 保證數(shù)據(jù)庫和緩存一致性:緩存延時雙刪、刪除緩存重試機制、讀取biglog異步刪除緩存
          • 緩存擊穿:設(shè)置數(shù)據(jù)永不過期
          • 緩存雪崩:Redis集群高可用、均勻設(shè)置過期時間
          • 緩存穿透:接口層校驗、查詢?yōu)榭赵O(shè)置個默認空值標記、布隆過濾器。

          一般用Redis分布式緩存,當(dāng)然有些時候也可以考慮使用本地緩存,如Guava Cache、Caffeine等。使用本地緩存有些缺點,就是無法進行大數(shù)據(jù)存儲,并且應(yīng)用進程的重啟,緩存會失效。

          14. 接口考慮熱點數(shù)據(jù)隔離性

          瞬時間的高并發(fā),可能會打垮你的系統(tǒng)??梢宰鲆恍狳c數(shù)據(jù)的隔離。比如業(yè)務(wù)隔離、系統(tǒng)隔離、用戶隔離、數(shù)據(jù)隔離等。

          • 業(yè)務(wù)隔離性,比如12306的分時段售票,將熱點數(shù)據(jù)分散處理,降低系統(tǒng)負載壓力。
          • 系統(tǒng)隔離:比如把系統(tǒng)分成了用戶、商品、社區(qū)三個板塊。這三個塊分別使用不同的域名、服務(wù)器和數(shù)據(jù)庫,做到從接入層到應(yīng)用層再到數(shù)據(jù)層三層完全隔離。
          • 用戶隔離:重點用戶請求到配置更好的機器。
          • 數(shù)據(jù)隔離:使用單獨的緩存集群或者數(shù)據(jù)庫服務(wù)熱點數(shù)據(jù)。

          15. 可變參數(shù)配置化,比如紅包皮膚切換等

          假如產(chǎn)品經(jīng)理提了個紅包需求,圣誕節(jié)的時候,紅包皮膚為圣誕節(jié)相關(guān)的,春節(jié)的時候,為春節(jié)紅包皮膚等。

          如果在代碼寫死控制,可有類似以下代碼:

          if(duringChristmas){
          ???img?=?redPacketChristmasSkin;
          }else?if(duringSpringFestival){
          ???img?=??redSpringFestivalSkin;
          }

          如果到了元宵節(jié)的時候,運營小姐姐突然又有想法,紅包皮膚換成燈籠相關(guān)的,這時候,是不是要去修改代碼了,重新發(fā)布了?

          從一開始接口設(shè)計時,可以實現(xiàn)一張紅包皮膚的配置表,將紅包皮膚做成配置化呢?更換紅包皮膚,只需修改一下表數(shù)據(jù)就好了。

          當(dāng)然,還有一些場景適合一些配置化的參數(shù):一個分頁多少數(shù)量控制、某個搶紅包多久時間過期這些,都可以搞到參數(shù)配置化表里面。這也是擴展性思想的一種體現(xiàn)。

          16.接口考慮冪等性

          接口是需要考慮冪等性的,尤其搶紅包、轉(zhuǎn)賬這些重要接口。最直觀的業(yè)務(wù)場景,就是用戶連著點擊兩次,你的接口有沒有hold住?;蛘呦㈥犃谐霈F(xiàn)重復(fù)消費的情況,你的業(yè)務(wù)邏輯怎么控制?

          回憶下,什么是冪等?

          計算機科學(xué)中,冪等表示一次和多次請求某一個資源應(yīng)該具有同樣的副作用,或者說,多次請求所產(chǎn)生的影響與一次請求執(zhí)行的影響效果相同。

          大家別搞混哈,防重和冪等設(shè)計其實是有區(qū)別的。防重主要為了避免產(chǎn)生重復(fù)數(shù)據(jù),把重復(fù)請求攔截下來即可。而冪等設(shè)計除了攔截已經(jīng)處理的請求,還要求每次相同的請求都返回一樣的效果。不過呢,很多時候,它們的處理流程、方案是類似的哈。


          接口冪等實現(xiàn)方案主要有8種:

          • select+insert+主鍵/唯一索引沖突
          • 直接insert + 主鍵/唯一索引沖突
          • 狀態(tài)機冪等
          • 抽取防重表
          • token令牌
          • 悲觀鎖
          • 樂觀鎖
          • 分布式鎖

          大家可以看我這篇文章哈:聊聊冪等設(shè)計

          17. 讀寫分離,優(yōu)先考慮讀從庫,注意主從延遲問題

          我們的數(shù)據(jù)庫都是集群部署的,有主庫也有從庫,當(dāng)前一般都是讀寫分離的。比如你寫入數(shù)據(jù),肯定是寫入主庫,但是對于讀取實時性要求不高的數(shù)據(jù),則優(yōu)先考慮讀從庫,因為可以分擔(dān)主庫的壓力。

          如果讀取從庫的話,需要考慮主從延遲的問題。

          18.接口注意返回的數(shù)據(jù)量,如果數(shù)據(jù)量大需要分頁

          一個接口返回報文,不應(yīng)該包含過多的數(shù)據(jù)量。過多的數(shù)據(jù)量不僅處理復(fù)雜,并且數(shù)據(jù)量傳輸?shù)膲毫σ卜浅4?。因此?shù)量實在是比較大,可以分頁返回,如果是功能不相關(guān)的報文,那應(yīng)該考慮接口拆分。

          19. 好的接口實現(xiàn),離不開SQL優(yōu)化

          我們做后端的,寫好一個接口,離不開SQL優(yōu)化。

          SQL優(yōu)化從這幾個維度思考:

          • explain 分析SQL查詢計劃(重點關(guān)注type、extra、filtered字段)
          • show profile分析,了解SQL執(zhí)行的線程的狀態(tài)以及消耗的時間
          • 索引優(yōu)化 (覆蓋索引、最左前綴原則、隱式轉(zhuǎn)換、order by以及group by的優(yōu)化、join優(yōu)化)
          • 大分頁問題優(yōu)化(延遲關(guān)聯(lián)、記錄上一頁最大ID)
          • 數(shù)據(jù)量太大(分庫分表、同步到es,用es查詢)

          20.代碼鎖的粒度控制好

          什么是加鎖粒度呢?

          其實就是就是你要鎖住的范圍是多大。比如你在家上衛(wèi)生間,你只要鎖住衛(wèi)生間就可以了吧,不需要將整個家都鎖起來不讓家人進門吧,衛(wèi)生間就是你的加鎖粒度。

          我們寫代碼時,如果不涉及到共享資源,就沒有必要鎖住的。這就好像你上衛(wèi)生間,不用把整個家都鎖住,鎖住衛(wèi)生間門就可以了。

          比如,在業(yè)務(wù)代碼中,有一個ArrayList因為涉及到多線程操作,所以需要加鎖操作,假設(shè)剛好又有一段比較耗時的操作(代碼中的slowNotShare方法)不涉及線程安全問題,你會如何加鎖呢?

          反例:

          //不涉及共享資源的慢方法
          private?void?slowNotShare()?{
          ????try?{
          ????????TimeUnit.MILLISECONDS.sleep(100);
          ????}?catch?(InterruptedException?e)?{
          ????}
          }

          //錯誤的加鎖方法
          public?int?wrong()?{
          ????long?beginTime?=?System.currentTimeMillis();
          ????IntStream.rangeClosed(1,?10000).parallel().forEach(i?->?{
          ????????//加鎖粒度太粗了,slowNotShare其實不涉及共享資源
          ????????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();//可以不加鎖
          ????????//只對List這部分加鎖
          ????????synchronized?(data)?{
          ????????????data.add(i);
          ????????}
          ????});
          ????log.info("cosume?time:{}",?System.currentTimeMillis()?-?beginTime);
          ????return?data.size();
          }

          21.接口狀態(tài)和錯誤需要統(tǒng)一明確

          提供必要的接口調(diào)用狀態(tài)信息。比如你的一個轉(zhuǎn)賬接口調(diào)用是成功、失敗、處理中還是受理成功等,需要明確告訴客戶端。如果接口失敗,那么具體失敗的原因是什么。這些必要的信息都必須要告訴給客戶端,因此需要定義明確的錯誤碼和對應(yīng)的描述。同時,盡量對報錯信息封裝一下,不要把后端的異常信息完全拋出到客戶端。

          22.接口要考慮異常處理

          實現(xiàn)一個好的接口,離不開優(yōu)雅的異常處理。對于異常處理,提十個小建議吧:

          • 盡量不要使用e.printStackTrace(),而是使用log打印。因為e.printStackTrace()語句可能會導(dǎo)致內(nèi)存占滿。
          • catch住異常時,建議打印出具體的exception,利于更好定位問題
          • 不要用一個Exception捕捉所有可能的異常
          • 記得使用finally關(guān)閉流資源或者直接使用try-with-resource
          • 捕獲異常與拋出異常必須是完全匹配,或者捕獲異常是拋異常的父類
          • 捕獲到的異常,不能忽略它,至少打點日志吧
          • 注意異常對你的代碼層次結(jié)構(gòu)的侵染
          • 自定義封裝異常,不要丟棄原始異常的信息Throwable cause
          • 運行時異常RuntimeException?,不應(yīng)該通過catch的方式來處理,而是先預(yù)檢查,比如:NullPointerException處理
          • 注意異常匹配的順序,優(yōu)先捕獲具體的異常

          小伙伴們有興趣可以看下我之前寫的這篇文章哈:Java 異常處理的十個建議

          23. 優(yōu)化程序邏輯

          優(yōu)化程序邏輯這塊還是挺重要的,也就是說,你實現(xiàn)的業(yè)務(wù)代碼,如果是比較復(fù)雜的話,建議把注釋寫清楚。還有,代碼邏輯盡量清晰,代碼盡量高效。

          比如,你要使用用戶信息的屬性,你根據(jù)session已經(jīng)獲取到userId了,然后就把用戶信息從數(shù)據(jù)庫查詢出來,使用完后,后面可能又要用到用戶信息的屬性,有些小伙伴沒想太多,反手就把userId再傳進去,再查一次數(shù)據(jù)庫。。。我在項目中,見過這種代碼。。。直接把用戶對象傳下來不好嘛。。

          反例偽代碼:

          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ù)庫
          ??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對象過來即可,不用再多查一次數(shù)據(jù)庫
          public?Response?do(UserInfo?user){
          ??......
          ??return?new?Response();?
          }

          當(dāng)然,這只是一些很小的一個例子,還有很多類似的例子,需要大家開發(fā)過程中,多點思考的哈。

          24. 接口實現(xiàn)過程中,注意大文件、大事務(wù)、大對象

          • 讀取大文件時,不要Files.readAllBytes直接讀取到內(nèi)存,這樣會OOM的,建議使用BufferedReader一行一行來。
          • 大事務(wù)可能導(dǎo)致死鎖、回滾時間長、主從延遲等問題,開發(fā)中盡量避免大事務(wù)。
          • 注意一些大對象的使用,因為大對象是直接進入老年代的,可能會觸發(fā)fullGC

          25. 你的接口,需要考慮限流

          如果你的系統(tǒng)每秒扛住的請求是1000,如果一秒鐘來了十萬請求呢?換個角度就是說,高并發(fā)的時候,流量洪峰來了,超過系統(tǒng)的承載能力,怎么辦呢?

          如果不采取措施,所有的請求打過來,系統(tǒng)CPU、內(nèi)存、Load負載飚的很高,最后請求處理不過來,所有的請求無法正常響應(yīng)。

          針對這種場景,我們可以采用限流方案。就是為了保護系統(tǒng),多余的請求,直接丟棄。

          限流定義:

          在計算機網(wǎng)絡(luò)中,限流就是控制網(wǎng)絡(luò)接口發(fā)送或接收請求的速率,它可防止DoS攻擊和限制Web爬蟲。限流,也稱流量控制。是指系統(tǒng)在面臨高并發(fā),或者大流量請求的情況下,限制新的請求對系統(tǒng)的訪問,從而保證系統(tǒng)的穩(wěn)定性。

          可以使用Guava的RateLimiter單機版限流,也可以使用Redis分布式限流,還可以使用阿里開源組件sentinel限流

          大家可以看下我之前這篇文章哈:4種經(jīng)典限流算法講解

          26.代碼實現(xiàn)時,注意運行時異常(比如空指針、下標越界等)

          日常開發(fā)中,我們需要采取措施規(guī)避數(shù)組邊界溢出,被零整除,空指針等運行時錯誤。類似代碼比較常見:

          String?name?=?list.get(1).getName();?//list可能越界,因為不一定有2個元素哈

          應(yīng)該采取措施,預(yù)防一下數(shù)組邊界溢出。正例如下:

          if(CollectionsUtil.isNotEmpty(list)&&?list.size()>1){
          ??String?name?=?list.get(1).getName();?
          }



          27.保證接口安全性

          如果你的API接口是對外提供的,需要保證接口的安全性。保證接口的安全性有token機制和接口簽名。

          token機制身份驗證方案還比較簡單的,就是

          1. 客戶端發(fā)起請求,申請獲取token。
          2. 服務(wù)端生成全局唯一的token,保存到redis中(一般會設(shè)置一個過期時間),然后返回給客戶端。
          3. 客戶端帶著token,發(fā)起請求。
          4. 服務(wù)端去redis確認token是否存在,一般用 redis.del(token)的方式,如果存在會刪除成功,即處理業(yè)務(wù)邏輯,如果刪除失敗不處理業(yè)務(wù)邏輯,直接返回結(jié)果。

          接口簽名的方式,就是把接口請求相關(guān)信息(請求報文,包括請求時間戳、版本號、appid等),客戶端私鑰加簽,然后服務(wù)端用公鑰驗簽,驗證通過才認為是合法的、沒有被篡改過的請求。

          有關(guān)于加簽驗簽的,大家可以看下我這篇文章哈:程序員必備基礎(chǔ):加簽驗簽

          除了加簽驗簽和token機制,接口報文一般是要加密的。當(dāng)然,用https協(xié)議是會對報文加密的。如果是我們服務(wù)層的話,如何加解密呢?

          可以參考HTTPS的原理,就是服務(wù)端把公鑰給客戶端,然后客戶端生成對稱密鑰,接著客戶端用服務(wù)端的公鑰加密對稱密鑰,再發(fā)到服務(wù)端,服務(wù)端用自己的私鑰解密,得到客戶端的對稱密鑰。這時候就可以愉快傳輸報文啦,客戶端用對稱密鑰加密請求報文,服務(wù)端用對應(yīng)的對稱密鑰解密報文

          有時候,接口的安全性,還包括手機號、身份證等信息的脫敏。就是說,用戶的隱私數(shù)據(jù),不能隨便暴露

          28.分布式事務(wù),如何保證

          分布式事務(wù):就是指事務(wù)的參與者、支持事務(wù)的服務(wù)器、資源服務(wù)器以及事務(wù)管理器分別位于不同的分布式系統(tǒng)的不同節(jié)點之上。簡單來說,分布式事務(wù)指的就是分布式系統(tǒng)中的事務(wù),它的存在就是為了保證不同數(shù)據(jù)庫節(jié)點的數(shù)據(jù)一致性。

          分布式事務(wù)的幾種解決方案:

          • 2PC(二階段提交)方案、3PC
          • TCC(Try、Confirm、Cancel)
          • 本地消息表
          • 最大努力通知
          • seata

          大家可以看下這篇文章哈:看一遍就理解:分布式事務(wù)詳解

          29. 事務(wù)失效的一些經(jīng)典場景

          我們的接口開發(fā)過程中,經(jīng)常需要使用到事務(wù)。所以需要避開事務(wù)失效的一些經(jīng)典場景。

          • 方法的訪問權(quán)限必須是public,其他private等權(quán)限,事務(wù)失效
          • 方法被定義成了final的,這樣會導(dǎo)致事務(wù)失效。
          • 在同一個類中的方法直接內(nèi)部調(diào)用,會導(dǎo)致事務(wù)失效。
          • 一個方法如果沒交給spring管理,就不會生成spring事務(wù)。
          • 多線程調(diào)用,兩個方法不在同一個線程中,獲取到的數(shù)據(jù)庫連接不一樣的。
          • 表的存儲引擎不支持事務(wù)
          • 如果自己try...catch誤吞了異常,事務(wù)失效。
          • 錯誤的傳播特性

          推薦大家看下這篇文章:聊聊spring事務(wù)失效的12種場景,太坑了

          30. 掌握常用的設(shè)計模式

          把代碼寫好,還是需要熟練常用的設(shè)計模式,比如策略模式、工廠模式、模板方法模式、觀察者模式等等。設(shè)計模式,是代碼設(shè)計經(jīng)驗的總結(jié)。使用設(shè)計模式可以可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性。

          我之前寫過一篇總結(jié)工作中常用設(shè)計模式的文章,寫得挺不錯的,大家可以看下:實戰(zhàn)!工作中常用到哪些設(shè)計模式

          31. 寫代碼時,考慮線性安全問題

          高并發(fā)情況下,HashMap可能會出現(xiàn)死循環(huán)。因為它是非線性安全的,可以考慮使用ConcurrentHashMap。所以這個也盡量養(yǎng)成習(xí)慣,不要上來反手就是一個new HashMap();

          • Hashmap、Arraylist、LinkedList、TreeMap等都是線性不安全的;
          • Vector、Hashtable、ConcurrentHashMap等都是線性安全的

          32.接口定義清晰易懂,命名規(guī)范。

          我們寫代碼,不僅僅是為了實現(xiàn)當(dāng)前的功能,也要有利于后面的維護。說到維護,代碼不僅僅是寫給自己看的,也是給別人看的。所以接口定義要清晰易懂,命名規(guī)范。

          33. 接口的版本控制

          接口要做好版本控制。就是說,請求基礎(chǔ)報文,應(yīng)該包含version接口版本號字段,方便未來做接口兼容。其實這個點也算接口擴展性的一個體現(xiàn)點吧。

          比如客戶端APP某個功能優(yōu)化了,新老版本會共存,這時候我們的version版本號就派上用場了,對version做升級,做好版本控制。

          34. 注意代碼規(guī)范問題

          注意一些常見的代碼壞味道:

          • 大量重復(fù)代碼(抽共用方法,設(shè)計模式)
          • 方法參數(shù)過多(可封裝成一個DTO對象)
          • 方法過長(抽小函數(shù))
          • 判斷條件太多(優(yōu)化if...else)
          • 不處理沒用的代碼
          • 不注重代碼格式
          • 避免過度設(shè)計

          代碼的壞味道,這里我都寫到啦:25種代碼壞味道總結(jié)+優(yōu)化示例

          35.保證接口正確性,其實就是保證更少的bug

          保證接口的正確性,換個角度講,就是保證更少的bug,甚至是沒有bug。所以接口開發(fā)完后,一般需要開發(fā)自測一下。然后的話,接口的正確還體現(xiàn)在,多線程并發(fā)的時候,保證數(shù)據(jù)的正確性,等等。比如你做一筆轉(zhuǎn)賬交易,扣減余額的時候,可以通過CAS樂觀鎖的方式保證余額扣減正確吧。

          如果你是實現(xiàn)秒殺接口,得防止超賣問題吧。你可以使用Redis分布式鎖防止超賣問題。使用Redis分布式鎖,有幾個注意要點,大家可以看下我之前這篇文章哈:七種方案!探討Redis分布式鎖的正確使用姿勢

          36.學(xué)會溝通,跟前端溝通,跟產(chǎn)品溝通

          我把這一點放到最后,學(xué)會溝通是非常非常重要的。比如你開發(fā)定義接口時,一定不能上來就自己埋頭把接口定義完了,需要跟客戶端先對齊接口。遇到一些難點時,跟技術(shù)leader對齊方案。實現(xiàn)需求的過程中,有什么問題,及時跟產(chǎn)品溝通。

          總之就是,開發(fā)接口過程中,一定要溝通好~




          end





          頂級程序員:topcoding

          做最好的程序員社區(qū):Java后端開發(fā)、Python、大數(shù)據(jù)、AI


          一鍵三連「分享」、「點贊」和「在看」



          瀏覽 28
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  操逼一区 | 珍藏3年极品人妻疯狂3p | 大香蕉在线电影 | 国产一级a毛一级a看免费 | 色妞干网 |