5個接口性能提升的通用技巧

前言

作為后端開發(fā)人員捅膘,我們總是在編寫各種API溯警,無論是為前端web提供數(shù)據(jù)支持的HTTP REST API ,還是提供內(nèi)部使用的RPC API晾匠。這些API在服務(wù)初期可能表現(xiàn)不錯具壮,但隨著用戶數(shù)量的增長准颓,一開始響應(yīng)很快的API越來越慢,直到用戶抱怨:“你的系統(tǒng)太糟糕了棺妓∪烈眩” 我只是瀏覽網(wǎng)頁。為什么這么慢怜跑?”样勃。這時候你就需要考慮如何優(yōu)化你的API性能了。

要想提高你的API的性能,我們首先要知道哪些問題會導(dǎo)致接口響應(yīng)慢峡眶。API設(shè)計(jì)需要考慮很多方面剧防。開發(fā)語言層面只占一小部分。哪個部分設(shè)計(jì)不好就會成為性能瓶頸辫樱。影響API性能的因素有很多诵姜,總結(jié)如下:

  • 數(shù)據(jù)庫慢查詢
  • 復(fù)雜的業(yè)務(wù)邏輯
  • 糟糕的代碼
  • 資源不足
  • ........

在這篇文章中,我總結(jié)了一些行之有效的API性能優(yōu)化技巧搏熄,希望能給有需要的朋友一些幫助。

歡迎關(guān)注個人公眾號『JAVA旭陽』交流溝通

1. 并發(fā)調(diào)用

假設(shè)我們現(xiàn)在有一個電子商務(wù)系統(tǒng)需要提交訂單暇赤。該功能需要調(diào)用庫存系統(tǒng)進(jìn)行庫存查扣心例,還需要獲取用戶地址信息。最后調(diào)用風(fēng)控系統(tǒng)判斷本次交易無風(fēng)險(xiǎn)鞋囊。這個接口的設(shè)計(jì)大部分可能會把接口設(shè)計(jì)成一個順序執(zhí)行的接口止后。畢竟我們需要獲取到用戶地址信息,完成庫存扣減溜腐,才能進(jìn)行下一步译株。偽代碼如下:

public Boolean submitOrder(orderInfo orderInfo) {

    //check stock
    stockService.check();
    //invoke addressService
    addressService.getByUser();
    //risk control
    riskControlSerivce.check();

    return doSubmitOrder(orderInfo);
}
復(fù)制代碼

如果我們仔細(xì)分析這個函數(shù),就會發(fā)現(xiàn)幾個方法調(diào)用之間并沒有很強(qiáng)的依賴關(guān)系挺益。而且這三個系統(tǒng)的調(diào)用都比較耗時歉糜。假設(shè)這些系統(tǒng)的調(diào)用耗時分布如下

  • stockService.check()需要 150 毫秒。
  • addressService.getByUser()需要 200 毫秒望众。
  • riskControlSerivce.check()需要 300 毫秒匪补。

如果順序調(diào)用此API,則整個API的執(zhí)行時間為650ms(150ms+200ms+300ms)烂翰。如果能轉(zhuǎn)化為并行調(diào)用夯缺,API的執(zhí)行時間為300ms,性能直接提升50%甘耿。使用并行調(diào)用踊兜,大致代碼如下:

public Boolean submitOrder(orderInfo orderInfo) {

    //check stock
    CompletableFuture<Void> stockFuture = CompletableFuture.supplyAsync(() -> {
        return stockService.check(); 
    }, executor);
    //invoke addressService
    CompletableFuture<Address> addressFuture = CompletableFuture.supplyAsync(() -> {
        return addressService.getByUser();
    }, executor);
    //risk control
    CompletableFuture<Void> riskFuture = CompletableFuture.supplyAsync(() -> {
        return  riskControlSerivce.check();
    }, executor);

    CompletableFuture.allOf(stockFuture, addressFuture, riskFuture);
    stockFuture.get();
    addressFuture.get();
    riskFuture.get();
    return doSubmitOrder(orderInfo);
}
復(fù)制代碼

2. 避免大事務(wù)

所謂大事務(wù),就是歷經(jīng)時間很長的事務(wù)佳恬。如果使用Spring @Transaction管理事務(wù)捏境,需要注意是否不小心啟動了大事務(wù)。因?yàn)镾pring的事務(wù)管理原理是將多個事務(wù)合并到一個執(zhí)行中毁葱,如果一個API里面有多個數(shù)據(jù)庫讀寫典蝌,而且這個API的并發(fā)訪問量比較高,很可能大事務(wù)會導(dǎo)致太大大量數(shù)據(jù)鎖在數(shù)據(jù)庫中头谜,造成大量阻塞骏掀,數(shù)據(jù)庫連接池連接耗盡。

@Transactional(rollbackFor=Exception.class)
public Boolean submitOrder(orderInfo orderInfo) {

    //check stock
    stockService.check();
    //invoke addressService
    addressService.getByUser();
    //risk control
    riskControlRpcApi.check();

    orderService.insertOrder(orderInfo);
    orderDetailService.insertOrderDetail(orderInfo);

    return true;
}
復(fù)制代碼

相信在很多人寫的業(yè)務(wù)中都出現(xiàn)過這種代碼,遠(yuǎn)程調(diào)用操作截驮,一個非DB操作笑陈,混合在持久層代碼中,這種代碼絕對是一個大事務(wù)葵袭。它不僅需要查詢用戶地址和扣除庫存涵妥,還需要插入訂單數(shù)據(jù)和訂單明細(xì)。這一系列操作需要合并到同一個事務(wù)中坡锡。如果RPC響應(yīng)慢蓬网,當(dāng)前線程會一直占用數(shù)據(jù)庫連接,導(dǎo)致并發(fā)場景下數(shù)據(jù)庫連接耗盡鹉勒。不僅如此帆锋,如果事務(wù)需要回滾,你的API響應(yīng)也會因?yàn)榛貪L慢而變慢禽额。

這個時候就需要考慮減小事務(wù)了锯厢,我們可以把非事務(wù)操作和事務(wù)操作分開,像這樣:

@Autowired
private OrderDaoService orderDaoService;

public Boolean submitOrder(OrderInfo orderInfo) {

    //invoke addressService
    addressService.getByUser();
    //risk control
    riskControlRpcApi.check();
    return orderDaoService.doSubmitOrder(orderInfo);
}

@Service
public class OrderDaoService{

    @Transactional(rollbackFor=Exception.class)
    public Boolean doSubmitOrder(OrderInfo orderInfo) {
        //check stock
        stockService.check();
        orderService.insertOrder(orderInfo);
        orderDetailService.insertOrderDetail(orderInfo);
        return true;
    }
}
復(fù)制代碼

或者脯倒,您可以使用 spring 的編程事務(wù)TransactionTemplate实辑。

@Autowired
private TransactionTemplate transactionTemplate;

public void submitOrder(OrderInfo orderInfo) {

    //invoke addressService
    addressService.getByUser();
    //risk control
    riskControlRpcApi.check();
    return transactionTemplate.execute(()->{
        return doSubmitOrder(orderInfo);
    })
}

public Boolean doSubmitOrder(OrderInfo orderInfo) {
        //check stock
        stockService.check();
        orderService.insertOrder(orderInfo);
        orderDetailService.insertOrderDetail(orderInfo);
        return true;
    }
復(fù)制代碼

3. 添加合適的索引

我們的服務(wù)在運(yùn)行初期,系統(tǒng)需要存儲的數(shù)據(jù)量很小藻丢,可能是數(shù)據(jù)庫沒有加索引來快速存儲和訪問數(shù)據(jù)剪撬。但是隨著業(yè)務(wù)的增長,單表數(shù)據(jù)量不斷增加悠反,數(shù)據(jù)庫的查詢性能變差婿奔。這時候我們應(yīng)該給你的數(shù)據(jù)庫表添加適當(dāng)?shù)乃饕问慎?梢酝ㄟ^命令查看表的索引(這里以MySQL為例)萍摊。

show index from `your_table_name`;
復(fù)制代碼

ALTER TABLE通過命令添加索引。

ALTER TABLE `your_table_name` ADD INDEX index_name(username);
復(fù)制代碼

有時候如叼,即使加了一些索引冰木,數(shù)據(jù)查詢還是很慢。這時候你可以使用explain命令查看執(zhí)行計(jì)劃來判斷你的SQL語句是否命中了索引笼恰。例如:

explain select * from product_info where type=0;
復(fù)制代碼

你會得到一個分析結(jié)果:

image

一般來說踊沸,索引失效有幾種情況:

  • 不滿足最左前綴原則。例如社证,您創(chuàng)建一個組合索引idx(a,b,c)逼龟。但是你的SQL語句是這樣寫的select * from tb1 where b='xxx' and c='xxxx';
  • 索引列使用算術(shù)運(yùn)算追葡。select * from tb1 where a%10=0;
  • 索引列使用函數(shù)腺律。select * from tb1 where date_format(a,'%m-%d-%Y')='2023-01-02';
  • like使用關(guān)鍵字的模糊查詢奕短。select * from tb1 where a like '%aaa';
  • 使用not innot exist關(guān)鍵字。
  • 等等

4. 返回更少的數(shù)據(jù)

如果我們查詢大量符合條件的數(shù)據(jù)匀钧,我們不需要返回所有數(shù)據(jù)翎碑。我們可以通過分頁的方式增量提供數(shù)據(jù)。這樣之斯,我們需要通過網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)更少日杈,編碼和解碼數(shù)據(jù)的時間更短,API 響應(yīng)更快佑刷。

但是莉擒,傳統(tǒng)的limit offset方法用于 paging( select * from product limit 10000,20)。當(dāng)頁面數(shù)量很大時瘫絮,查詢會越來越慢涨冀。這是因?yàn)槭褂玫脑?code>limit offset是找出10000條數(shù)據(jù),然后丟棄前面的9980條數(shù)據(jù)檀何。我們可以使用延遲關(guān)聯(lián)來優(yōu)化此 SQL。

select * from product where id in (select id from product limit 10000,20);
復(fù)制代碼

5. 使用緩存

緩存是一種以空間換時間的解決方案廷支。一些用戶經(jīng)常訪問的數(shù)據(jù)直接緩存在內(nèi)存中频鉴。因?yàn)閮?nèi)存的讀取速度遠(yuǎn)快于磁盤IO,所以我們也可以通過適當(dāng)?shù)木彺鎭硖岣逜PI的性能恋拍。簡單的垛孔,我們可以使用Java的HashMapConcurrentHashMap施敢,或者caffeine等本地緩存周荐,或者MemcachedRedis等分布式緩存中間件僵娃。

總結(jié)

我在這里列出了五個通用的 API 性能優(yōu)化技巧概作,這些技巧只有在系統(tǒng)有一定的并發(fā)壓力時才有效。如果本文對你有幫助的話默怨,請留下一個贊吧讯榕。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市匙睹,隨后出現(xiàn)的幾起案子愚屁,更是在濱河造成了極大的恐慌,老刑警劉巖痕檬,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件霎槐,死亡現(xiàn)場離奇詭異,居然都是意外死亡梦谜,警方通過查閱死者的電腦和手機(jī)丘跌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門袭景,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人碍岔,你說我怎么就攤上這事浴讯。” “怎么了蔼啦?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵榆纽,是天一觀的道長。 經(jīng)常有香客問我捏肢,道長奈籽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任鸵赫,我火速辦了婚禮衣屏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘辩棒。我一直安慰自己狼忱,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布一睁。 她就那樣靜靜地躺著钻弄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪者吁。 梳的紋絲不亂的頭發(fā)上窘俺,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天,我揣著相機(jī)與錄音复凳,去河邊找鬼瘤泪。 笑死,一個胖子當(dāng)著我的面吹牛育八,可吹牛的內(nèi)容都是我干的对途。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼髓棋,長吁一口氣:“原來是場噩夢啊……” “哼掀宋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起仲锄,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤劲妙,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后儒喊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體镣奋,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年怀愧,在試婚紗的時候發(fā)現(xiàn)自己被綠了侨颈。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片余赢。...
    茶點(diǎn)故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖哈垢,靈堂內(nèi)的尸體忽然破棺而出妻柒,到底是詐尸還是另有隱情,我是刑警寧澤耘分,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布举塔,位于F島的核電站,受9級特大地震影響求泰,放射性物質(zhì)發(fā)生泄漏央渣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一渴频、第九天 我趴在偏房一處隱蔽的房頂上張望芽丹。 院中可真熱鬧,春花似錦卜朗、人聲如沸拔第。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蚊俺。三九已至,卻和暖如春惹悄,著一層夾襖步出監(jiān)牢的瞬間春叫,已是汗流浹背肩钠。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工泣港, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人价匠。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓当纱,卻偏偏與公主長得像,于是被迫代替她去往敵國和親踩窖。 傳聞我的和親對象是個殘疾皇子坡氯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評論 2 345