更新時間:2020-02-07 14:58:22 來源:動力節(jié)點 瀏覽2740次
為Java多線程應(yīng)用程序優(yōu)化數(shù)據(jù)存儲庫
數(shù)據(jù)存儲庫通常是超高要求系統(tǒng)的瓶頸。在這些系統(tǒng)中,正在執(zhí)行的查詢數(shù)量非常大。DelayedBatchExecutor是一個用于減少所需查詢數(shù)量的組件,通過在Java多線程應(yīng)用程序中對所需查詢進(jìn)行批處理。
1個參數(shù)的n個查詢Vs.n個參數(shù)的1個查詢
假設(shè)有一個對關(guān)系數(shù)據(jù)庫執(zhí)行查詢的Java應(yīng)用程序,以便在給定其唯一標(biāo)識符(id)的情況下檢索Product實體(row)。
查詢?nèi)缦滤荆?/p>
SELECT*FROMPRODUCTWHEREID=<productId>
現(xiàn)在,檢索n個Products,有如下兩種方法:
執(zhí)行1個參數(shù)的n個獨立查詢:
SELECT*FROMPRODUCTWHEREID=<productId1>
SELECT*FROMPRODUCTWHEREID=<productId2>
...
SELECT*FROMPRODUCTWHEREID=<productIdn>
使用IN運算符或ORs的串聯(lián),對n個參數(shù)執(zhí)行1個查詢以便同時檢索n個Products
--ExampleusingINOPERATOR
SELECT*FROMPRODUCTWHEREIDIN(<productId1>,<productId2>,...,<productIdn>)
后者在網(wǎng)絡(luò)流量和數(shù)據(jù)庫服務(wù)器資源(CPU和磁盤)方面更為有效,因為:
往返數(shù)據(jù)庫的次數(shù)為1,而不是n。
數(shù)據(jù)庫引擎優(yōu)化了n個參數(shù)的數(shù)據(jù)遍歷過程,即每個表格可能只需要掃描1次,而不是n次。
這不僅適用于SELECT操作,而且適用于其他操作,例如INSERTs,UPDATEs和DELETEs。實際上,JDBCAPI包括上述操作的批量處理操作。
同樣的情況也適用于NoSQL存儲庫,其中大多都明確提供BULK操作。
DelayedBatchExecutor
需要從數(shù)據(jù)庫中檢索數(shù)據(jù)的Java應(yīng)用程序,如REST微服務(wù)或異步消息處理器,通常以多線程應(yīng)用程序(*1)實現(xiàn),其中:
每個線程在其執(zhí)行的某個時刻執(zhí)行相同的查詢(每個查詢具有不同的參數(shù))。
并發(fā)線程數(shù)很高(每秒數(shù)十或數(shù)百)。
在這種場景下,數(shù)據(jù)庫很可能在較短的時間間隔內(nèi)多次執(zhí)行相同的查詢。
如前所述,如果將1個參數(shù)的n個查詢替換為具有n個參數(shù)的單個等效查詢,那么則應(yīng)用程序?qū)⑹褂幂^少的數(shù)據(jù)庫服務(wù)器和網(wǎng)絡(luò)資源。
好消息是它可以通過timewindows(時間窗口)的機制來實現(xiàn),如下所示:
第一個嘗試執(zhí)行查詢的線程會打開一個時間窗口,因此其參數(shù)被存儲在一個列表中,同時該線程被暫停。在時間窗口內(nèi)執(zhí)行相同查詢的其余線程會將其參數(shù)添加到列表中,并且也會被暫停。此時,數(shù)據(jù)庫上未執(zhí)行任何查詢。
時間窗口結(jié)束或列表已滿(先前已定義最大容量限制)后,將使用列表中存儲的所有參數(shù)執(zhí)行單個查詢。最后,一旦數(shù)據(jù)庫提供了該查詢的結(jié)果,每個線程將接收相應(yīng)的結(jié)果,同時所有線程將自動恢復(fù)。
筆者構(gòu)建了一個簡單而輕量級的應(yīng)用機制(DelayedBatchExecutor),很容易在新的或現(xiàn)有的應(yīng)用程序中使用。它基于Reactor庫,并且為參數(shù)列表使用超時的Flux緩沖發(fā)布器。
運用DelayedBatchExecutor的吞吐量和延遲分析
假設(shè)針對Products的REST微服務(wù)公開了一個端點,用于檢索數(shù)據(jù)庫中給定的productId的Product數(shù)據(jù)。在沒有DelayedBatchExecutor的情況下,如果每秒對端點命中200次,則數(shù)據(jù)庫每秒執(zhí)行200個查詢。如果端點使用的DelayedBatchExecutor配置了50毫秒的時間窗口且最大容量=10個參數(shù),數(shù)據(jù)庫每秒鐘將只執(zhí)行10個參數(shù)的20個查詢,代價是每執(zhí)行一個線程,最多在50毫秒內(nèi)增加延時(*2)。
換句話說,為了將延時增加50毫秒(*2),數(shù)據(jù)庫每秒接收的查詢減少了10倍,然而保持了系統(tǒng)的整體吞吐量。還不錯!!
其他有趣的配置:
窗口時間=100毫秒,最大容量=20個參數(shù)→20個參數(shù)的10個查詢(查詢減少20倍)
窗口時間=500毫秒,最大容量=100個參數(shù)→2個查詢100個參數(shù)(查詢減少100倍)
執(zhí)行中的DelayedBatchExecutor
深入研究Product微服務(wù)示例。假設(shè)對于每個傳入的HTTP請求,微服務(wù)的控制器都要求檢索已有id的Product(JavaBean),因此將調(diào)用以下方法:
DAO組件(ProductDAO)的publicProductgetProductById(IntegerproductId).
以下分別是有和沒有DelayedBatchExecutor的DAO執(zhí)行。
沒有DelayedBatchExecutor
publicclassProductDAO{
publicProductgetProductById(Integerid){
Productproduct=...//executethequerySELECT*FROMPRODUCTWHEREID=<id>
//usingyourfavouriteAPI:JDBC,JPA,Hibernate...
returnproduct;
}
...
}
有DelayedBatchExecutor
//Singleton
publicclassProductDAO{
DelayedBatchExecutor2<Product,Integer>delayedBatchExecutorProductById=
DelayedBatchExecutor.define(Duration.ofMillis(50),10,this::retrieveProductsByIds);
publicProductgetProductById(Integerid){
Productproduct=delayedBatchExecutorProductById.execute(id);
returnproduct;
}
privateList<Product>retrieveProductsByIds(List<Integer>idList){
List<Product>productList=...//executequery:SELECT*FROMPRODUCTWHEREIDIN(idList.get(0),...,idList.get(n));
//usingyourfavouriteAPI:JDBC,JPA,Hibernate...
//Thepositionsoftheelementsofthelisttoreturnmustmatchtheonesintheparameterslist.
//Forinstance,thefirstProductofthelisttobereturnedmustbetheonewith
//theIdinthefirstpositionofproductIdsListandsoon...
//NOTE:nullcouldbeusedasvalue,meaningthatnoProductexistforthegivenproductId
returnproductList;
}
...
}
首先,必須在DAO中創(chuàng)建一個DelayedBatchExecutor實例,在本例中為delayedBatchExecutorProductById。需要以下三個參數(shù):
時間窗口(在此示例中為50毫秒)
參數(shù)列表的最大容量(在此示例中為10個參數(shù))
將使用參數(shù)列表調(diào)用的方法(詳細(xì)信息見后文)。在此示例中,方法為retrieveProductsByIds
其次,已經(jīng)重構(gòu)了DAO方法publicProductgetProductById(IntegerproductId),以簡單調(diào)用delayedBatchExecutorProductById實例的execute方法。所有的“magic”都是由DelayedBatchExecutor完成的。
之所以delayedBatchExecutorProductById是DelayedBatchExecutor2<Product,Integer>的實例,是因為其execute方法返回一個Product實例并接收一個Integer實例作為其實際參數(shù)。因此,存在:DelayedBatchExecutor2<Product,Integer>。
如果execute方法需要接收兩個參數(shù)(例如,一個Integer和一個String)并返回Product實例,則定義為DelayedBatchExecutor3<Product,Integer,String>等。
最終,retrieveProductsByIds方法必須返回List<Product>并接收List<Integer>作為參數(shù)。
如果使用的是DelayedBatchExecutor3<Product,Integer,String>,則必須將retrieveProductsByIds設(shè)為List<Product>retrieveProductsByIds(List<Integer>productIdsList,List<String>stringList)
就是這樣。
一旦運行,執(zhí)行控制器邏輯的并發(fā)線程會在某時刻調(diào)用方法getProductById(Integerid),并且此方法將返回對應(yīng)的Product。并發(fā)線程不知自己已經(jīng)被DelayedBatchExecutor暫停并恢復(fù)了。
以上就是動力節(jié)點Java培訓(xùn)機構(gòu)小編介紹的“java數(shù)據(jù)庫多線程應(yīng)用程序優(yōu)化數(shù)據(jù)存儲庫”的內(nèi)容,希望對大家有幫助,如有疑問,請在線咨詢,有專業(yè)老師隨時為你服務(wù)。
相關(guān)內(nèi)容
java多線程的狀態(tài)轉(zhuǎn)換以及基本操作
相關(guān)閱讀