分享5個Java接口性能提升的通用技巧
前言
作為后端開發(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è)計需要考慮很多方面。開發(fā)語言層面只占一小部分。哪個部分設(shè)計不好就會成為性能瓶頸。影響API性能的因素有很多,總結(jié)如下:
- 數(shù)據(jù)庫慢查詢
- 復(fù)雜的業(yè)務(wù)邏輯
- 糟糕的代碼
- 資源不足
- ........
在這篇文章中,我總結(jié)了一些行之有效的API性能優(yōu)化技巧,希望能給有需要的朋友一些幫助。
1. 并發(fā)調(diào)用
假設(shè)我們現(xiàn)在有一個電子商務(wù)系統(tǒng)需要提交訂單。該功能需要調(diào)用庫存系統(tǒng)進行庫存查扣,還需要獲取用戶地址信息。最后調(diào)用風(fēng)控系統(tǒng)判斷本次交易無風(fēng)險。這個接口的設(shè)計大部分可能會把接口設(shè)計成一個順序執(zhí)行的接口。畢竟我們需要獲取到用戶地址信息,完成庫存扣減,才能進行下一步。偽代碼如下:
public Boolean submitOrder(orderInfo orderInfo) { //check stock stockService.check(); //invoke addressService addressService.getByUser(); //risk control riskControlSerivce.check(); return doSubmitOrder(orderInfo); }
如果我們仔細分析這個函數(shù),就會發(fā)現(xiàn)幾個方法調(diào)用之間并沒有很強的依賴關(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); }
2. 避免大事務(wù)
所謂大事務(wù),就是歷經(jīng)時間很長的事務(wù)。如果使用Spring @Transaction
管理事務(wù),需要注意是否不小心啟動了大事務(wù)。因為Spring的事務(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; }
相信在很多人寫的業(yè)務(wù)中都出現(xiàn)過這種代碼,遠程調(diào)用操作,一個非DB操作,混合在持久層代碼中,這種代碼絕對是一個大事務(wù)。它不僅需要查詢用戶地址和扣除庫存,還需要插入訂單數(shù)據(jù)和訂單明細。這一系列操作需要合并到同一個事務(wù)中。如果RPC響應(yīng)慢,當(dāng)前線程會一直占用數(shù)據(jù)庫連接,導(dǎo)致并發(fā)場景下數(shù)據(jù)庫連接耗盡。不僅如此,如果事務(wù)需要回滾,你的API響應(yīng)也會因為回滾慢而變慢。
這個時候就需要考慮減小事務(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; } }
或者,您可以使用 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; }
3. 添加合適的索引
我們的服務(wù)在運行初期,系統(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`;
ALTER TABLE
通過命令添加索引。
ALTER TABLE `your_table_name` ADD INDEX index_name(username);
有時候,即使加了一些索引,數(shù)據(jù)查詢還是很慢。這時候你可以使用explain
命令查看執(zhí)行計劃來判斷你的SQL語句是否命中了索引。例如:
explain select * from product_info where type=0;
你會得到一個分析結(jié)果:
一般來說,索引失效有幾種情況:
- 不滿足最左前綴原則。例如,您創(chuàng)建一個組合索引
idx(a,b,c)
。但是你的SQL語句是這樣寫的select * from tb1 where b='xxx' and c='xxxx';
。 - 索引列使用算術(shù)運算。
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 in
或not 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ù)量很大時,查詢會越來越慢。這是因為使用的原理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);
5. 使用緩存
緩存是一種以空間換時間的解決方案。一些用戶經(jīng)常訪問的數(shù)據(jù)直接緩存在內(nèi)存中。因為內(nèi)存的讀取速度遠快于磁盤IO,所以我們也可以通過適當(dāng)?shù)木彺鎭硖岣逜PI的性能。簡單的,我們可以使用Java的HashMap
、ConcurrentHashMap
,或者caffeine
等本地緩存,或者Memcached
、Redis
等分布式緩存中間件。
到此這篇關(guān)于分享5個Java接口性能提升的通用技巧的文章就介紹到這了,更多相關(guān)Java接口性能提升技巧內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何巧用HashMap一行代碼統(tǒng)計單詞出現(xiàn)次數(shù)詳解
這篇文章主要給大家介紹了關(guān)于如何巧用HashMap一行代碼統(tǒng)計單詞出現(xiàn)次數(shù)的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07Spring boot隨機端口你都不會還怎么動態(tài)擴容
這篇文章主要介紹了Spring boot隨機端口你都不會還怎么動態(tài)擴容,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05