基於Apache-Commons-Pool2實現Grpc客戶端連接池
在項目運行過程中,有些操作對系統資源消耗較大,比如建立數據庫連接、建立Redis連接等操作,我們希望一次性創建多個連接對象,並在以後需要使用時能直接使用已創建好的連接,達到提高性能的目的。池技術通過提前將一些占用較多資源的對象初始化,並將初始化後的對象保存到池中備用,達到提高應用服務性能的目的,數據庫的JDBC連接池和Jedis連接池等都使用了池技術。
Apache-Commons-Pool2提供了一套池技術的規範接口和實現的通用邏輯,我們只需要實現其抽象出來的方法就可以了。這篇博文主要分享基於Apache-Commons-Pool2來實現Grpc連接池的應用。
關於Grpc相關的內容,大家如想了解基本的實現方法,可以參考我的另一篇博客(傳送門):http://blog.51cto.com/andrewli/2058908
核心組件
我們先來了解一下Apache-Commons-Pool2規範接口中涉及到的幾個核心組件,包括:
- ObjectPool
對象池,用於存儲對象,並管理對象的入池和出池。對象池的實現類是 GenericObjectPool<T>; - PoolConfig
池屬性,用於設置連接池的一些配置信息,比如最大池容量、超過池容量後的處理邏輯等。池屬性的實現類是:GenericObjectPoolConfig; - ObjectFactory
對象工廠,在需要的時候生成新的對象實例,並放入池中。對象工廠的接口是:interface PooledObjectFactory<T>; - ClientObject
核心組件依賴關系及其工作流程
接口與類之間的依賴關系
在梳理連接池相關的核心組件工作流程之前,我們先來了解一下核心組件涉及到的類和接口之間的繼承和實現關系。
-
對象池類的繼承關系
對象池的最頂層接口是ObjectPool<T>,裏面定義了對象池的基本方法,包括對象的添加、取出、校驗、返還,以及獲取處於Idle休眠狀態的對象數量、獲取處於Active狀態的對象數量、清空池、關閉池。
抽象類BaseGenericObjectPool<T>,定義了對象池的初始配置,並實現了對象池的基本接口方法。
由於GenericObjectPool<T>類支持範型,我們要做的,就是指定GenericObjectPool<T>池類返回的池對象類型<T>,並設置對象工廠類、配置類等池屬性;或者繼承GenericObjectPool類以添加更多的自定義池特性。
-
池屬性類的繼承關系
池屬性的最上層接口是interface Cloneable,抽象類BaseObjectPoolConfig實現了這個接口,並定義了默認的池配置屬性。
GenericObjectPoolConfig類繼承了BaseObjectPoolConfig,同樣定義了默認的池配置屬性值。
我們可以直接使用GenericObjectPoolConfig類,或者繼承GenericObjectPoolConfig類,根據自己的需求設置自定義池配置屬性。 -
池內對象類的繼承關系
池內對象類實現了上層的PooledObject<T>接口,這個接口裏面定義了一個池對象需要實現的各種方法。
另外,池內對象類還需要定義類本身需要具備的成員屬性和需要實現的業務方法。 - 對象工廠類的繼承關系
對象工廠類實現了最上層的PooledObjectFactory<T>接口,該接口定義了對象工廠的核心功能方法,包括:創建對象、銷毀對象、校驗對象、激活對象、鈍化對象。
工作流程
根據上述對核心組件的類繼承關系分析,我們可以梳理出一個流程,逐步實現各個組件,並組合成一套適用於我們業務的連接池架構。我們來看看這個流程該如何定義。
(1)定義我們的池內對象類 ClientObject,並結合我們的實際業務來實現上層接口的方法。
(2)定義對象工廠類ClientFactory,並結合我們的實際業務來實現上層接口的方法。
(3)定義池屬性類ClientPoolConfig,結合我們的實際需求來設置屬性值。
(4)使用對象池GenericObjectPool,指定泛型類型GenericObjectPool<ClientObject>。
連接池內部的核心業務邏輯:
池內對象的創建和返回邏輯是池技術裏的關鍵,可以查看池對象的borrowObject方法去了解這部分細節內容。
應用實踐
代碼實現
根據上述對Apache-commons-pool2的特點和實現流程的分析,我們基於Grpc客戶端連接池的應用場景,來進行代碼實踐,主要包括實現池內對象類 ClientObject和實現對象工廠類ClientFactory。
具體代碼可以進入我的百度網盤下載,鏈接如下:
https://pan.baidu.com/s/1eaGpz6XN2a3ssw0eYsNLww
代碼測試
為了驗證我們的Grpc連接池的作用,我編寫了一個測試方法,模擬以下場景,即開啟10個線程,每個線程循環10次使用Grpc連接發送消息給grpc服務端,然後查看線程池中累計創建的連接對象個數、線程池中每個連接對象的被使用次數等信息。
通過測試輸出的信息,我得到的結論是:不使用連接池時,總共需要進行100次Grpc連接並發送消息;使用連接池後,總共僅需要建立2次Grpc連接來發送100次消息,每個連接被調用了50次。
測試代碼如下。
package com.cmcc.littlec.grpc.poolclient;
import com.cmcc.littlec.grpc.util.Constants;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
public class test {
@SuppressWarnings("unchecked")
public static GenericObjectPool<ClientObject> getClientPool(){
ClientPoolFactory factory = new ClientPoolFactory(Constants.grpcHost, Constants.grpcPort);
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
config.setMaxIdle(8);
config.setMinIdle(3);
config.setMaxTotal(18);
config.setTestOnBorrow(true);
config.setTestOnReturn(true);
GenericObjectPool<ClientObject> clientPool = new GenericObjectPool<ClientObject>(factory, config);
return clientPool;
}
public static void main(String[] args ){
final GenericObjectPool<ClientObject> clientPool = getClientPool();
for(int i=0; i<10; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
ClientObject client = clientPool.borrowObject();
String str = "hello, grpc client_" + i;//參數
try {
client.sayHello(str);
}catch(Exception e){
client.invalidate();
}
System.out.println("Thread : " + Thread.currentThread().getName() + "; clientPool size : " + clientPool.getCreatedCount());
System.out.println("clientObj : "+client.toString());
clientPool.returnObject(client);
}
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
});
t.start();
try {
if(i%2==0) {
Thread.sleep(5000L);//每隔兩個線程創建後停頓5S
}
}catch(Exception e){ }
}
}
}
基於Apache-Commons-Pool2實現Grpc客戶端連接池