5個介面效能提升的通用技巧

JAVA旭陽發表於2023-01-02

前言

作為後端開發人員,我們總是在編寫各種API,無論是為前端web提供資料支援的HTTP REST API ,還是提供內部使用的RPC API。這些API在服務初期可能表現不錯,但隨著使用者數量的增長,一開始響應很快的API越來越慢,直到使用者抱怨:“你的系統太糟糕了。” 我只是瀏覽網頁。為什麼這麼慢?”。這時候你就需要考慮如何最佳化你的API效能了。

要想提高你的API的效能,我們首先要知道哪些問題會導致介面響應慢。API設計需要考慮很多方面。開發語言層面只佔一小部分。哪個部分設計不好就會成為效能瓶頸。影響API效能的因素有很多,總結如下:

  • 資料庫慢查詢
  • 複雜的業務邏輯
  • 糟糕的程式碼
  • 資源不足
  • ........

在這篇文章中,我總結了一些行之有效的API效能最佳化技巧,希望能給有需要的朋友一些幫助。

歡迎關注個人公眾號『JAVA旭陽』交流溝通

1. 併發呼叫

假設我們現在有一個電子商務系統需要提交訂單。該功能需要呼叫庫存系統進行庫存查扣,還需要獲取使用者地址資訊。最後呼叫風控系統判斷本次交易無風險。這個介面的設計大部分可能會把介面設計成一個順序執行的介面。畢竟我們需要獲取到使用者地址資訊,完成庫存扣減,才能進行下一步。虛擬碼如下:

public Boolean submitOrder(orderInfo orderInfo) {

	//check stock
	stockService.check();
	//invoke addressService
	addressService.getByUser();
	//risk control
	riskControlSerivce.check();
	
	return doSubmitOrder(orderInfo);
}

如果我們仔細分析這個函式,就會發現幾個方法呼叫之間並沒有很強的依賴關係。而且這三個系統的呼叫都比較耗時。假設這些系統的呼叫耗時分佈如下

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

如果順序呼叫此API,則整個API的執行時間為650ms(150ms+200ms+300ms)。如果能轉化為並行呼叫,API的執行時間為300ms,效能直接提升50%。使用並行呼叫,大致程式碼如下:

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. 避免大事務

所謂大事務,就是歷經時間很長的事務。如果使用Spring @Transaction管理事務,需要注意是否不小心啟動了大事務。因為Spring的事務管理原理是將多個事務合併到一個執行中,如果一個API裡面有多個資料庫讀寫,而且這個API的併發訪問量比較高,很可能大事務會導致太大大量資料鎖在資料庫中,造成大量阻塞,資料庫連線池連線耗盡。

@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;
}

相信在很多人寫的業務中都出現過這種程式碼,遠端呼叫操作,一個非DB操作,混合在持久層程式碼中,這種程式碼絕對是一個大事務。它不僅需要查詢使用者地址和扣除庫存,還需要插入訂單資料和訂單明細。這一系列操作需要合併到同一個事務中。如果RPC響應慢,當前執行緒會一直佔用資料庫連線,導致併發場景下資料庫連線耗盡。不僅如此,如果事務需要回滾,你的API響應也會因為回滾慢而變慢。

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

@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 的程式設計事務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. 新增合適的索引

我們的服務在執行初期,系統需要儲存的資料量很小,可能是資料庫沒有加索引來快速儲存和訪問資料。但是隨著業務的增長,單表資料量不斷增加,資料庫的查詢效能變差。這時候我們應該給你的資料庫表新增適當的索引。可以透過命令檢視錶的索引(這裡以MySQL為例)。

show index from `your_table_name`;

ALTER TABLE透過命令新增索引。

ALTER TABLE `your_table_name` ADD INDEX index_name(username);

有時候,即使加了一些索引,資料查詢還是很慢。這時候你可以使用explain命令檢視執行計劃來判斷你的SQL語句是否命中了索引。例如:

explain select * from product_info where type=0;

你會得到一個分析結果:

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

  • 不滿足最左字首原則。例如,您建立一個組合索引idx(a,b,c)。但是你的SQL語句是這樣寫的select * from tb1 where b='xxx' and c='xxxx';
  • 索引列使用算術運算。select * from tb1 where a%10=0;
  • 索引列使用函式。select * from tb1 where date_format(a,'%m-%d-%Y')='2023-01-02';
  • like使用關鍵字的模糊查詢。select * from tb1 where a like '%aaa';
  • 使用not innot exist關鍵字。
  • 等等

4. 返回更少的資料

如果我們查詢大量符合條件的資料,我們不需要返回所有資料。我們可以透過分頁的方式增量提供資料。這樣,我們需要透過網路傳輸的資料更少,編碼和解碼資料的時間更短,API 響應更快。

但是,傳統的limit offset方法用於 paging( select * from product limit 10000,20)。當頁面數量很大時,查詢會越來越慢。這是因為使用的原理limit offset是找出10000條資料,然後丟棄前面的9980條資料。我們可以使用延遲關聯來最佳化此 SQL。

select * from product where id in (select id from product limit 10000,20);

5. 使用快取

快取是一種以空間換時間的解決方案。一些使用者經常訪問的資料直接快取在記憶體中。因為記憶體的讀取速度遠快於磁碟IO,所以我們也可以透過適當的快取來提高API的效能。簡單的,我們可以使用Java的HashMapConcurrentHashMap,或者caffeine等本地快取,或者MemcachedRedis等分散式快取中介軟體。

總結

我在這裡列出了五個通用的 API 效能最佳化技巧,這些技巧只有在系統有一定的併發壓力時才有效。如果本文對你有幫助的話,請留下一個贊吧。

歡迎關注個人公眾號『JAVA旭陽』交流溝通

相關文章