概述
本篇博客是記錄使用spring batch做數(shù)據(jù)遷移時(shí)時(shí)遇到的一個(gè)關(guān)鍵問(wèn)題:數(shù)據(jù)遷移量大時(shí)如何保證內(nèi)存。當(dāng)我們?cè)谑褂胹pring batch時(shí),我們必須配置三個(gè)東西: reader,processor,和writer。其中,reader用于從數(shù)據(jù)庫(kù)中讀數(shù)據(jù),當(dāng)數(shù)據(jù)量較小時(shí),reader的邏輯不會(huì)對(duì)內(nèi)存帶來(lái)太多壓力,但是當(dāng)我們要去讀的數(shù)據(jù)量非常大的時(shí)候,我們就不得不考慮內(nèi)存等方面的問(wèn)題,因?yàn)槿魯?shù)據(jù)量非常大,內(nèi)存,執(zhí)行時(shí)間等等都會(huì)受到影響。關(guān)于spring batch的基礎(chǔ)知識(shí)和介紹請(qǐng)參考這篇博客: 批處理框架spring batch介紹及使用。
問(wèn)題是什么
在上面的內(nèi)容當(dāng)中我們已經(jīng)提到了,我們面臨的問(wèn)題是數(shù)據(jù)遷移量大時(shí)的內(nèi)存問(wèn)題。但是這樣的描述非常籠統(tǒng),因此博主決定將這一部分單獨(dú)拎出來(lái)說(shuō)。
在學(xué)習(xí)了spring batch的知識(shí)之后我們應(yīng)該很清楚的一點(diǎn)是,每一個(gè)spring batch的step都包含如下的部分:
即讀數(shù)據(jù),處理數(shù)據(jù),寫(xiě)數(shù)據(jù)。這三個(gè)步驟里面最可能會(huì)導(dǎo)致內(nèi)存變大問(wèn)題的無(wú)疑是讀數(shù)據(jù)環(huán)節(jié)。讀數(shù)據(jù)作為spring batch的數(shù)據(jù)輸入,是整個(gè)spring batch job的開(kāi)頭邏輯。
若我們的數(shù)據(jù)量不大,如只有幾十萬(wàn)條,那我們無(wú)疑不會(huì)面臨內(nèi)存問(wèn)題,即便一次將所有數(shù)據(jù)加載到內(nèi)存當(dāng)中,占的內(nèi)存也不會(huì)非常多,且spring batch數(shù)據(jù)遷移的速度非常之快,幾十萬(wàn)條的數(shù)據(jù)往往是幾十秒的時(shí)間就可以遷移完成。但是當(dāng)數(shù)據(jù)量變大之后,問(wèn)題就不一樣了。當(dāng)我們的數(shù)據(jù)量達(dá)到數(shù)百萬(wàn)或上千萬(wàn)時(shí),若一次性將所有數(shù)據(jù)全部讀到內(nèi)存當(dāng)中,則會(huì)占據(jù)遠(yuǎn)遠(yuǎn)超出正常范圍的非常大的內(nèi)存。該問(wèn)題示意圖如下所示:
我們寫(xiě)的任何程序都會(huì)有一個(gè)運(yùn)行內(nèi)存,假設(shè)這個(gè)內(nèi)存的總?cè)萘楷F(xiàn)在只有4g,而我們數(shù)據(jù)庫(kù)里需要操作的數(shù)據(jù)有8g,那么無(wú)疑,一次性的將數(shù)據(jù)讀出來(lái)就會(huì)出錯(cuò)。這便是需要考慮得問(wèn)題。
Spring提供的reader實(shí)現(xiàn)
spring提供了非常豐富的Reader實(shí)現(xiàn),其中比較常用的從數(shù)據(jù)庫(kù)讀數(shù)據(jù)的有JdbcCursorItemReader,JdbcPagingItemReader等。
JdbcCursorItemReader
使用JdbcCursorItemReader的示例代碼如下:
1
2
3
4
5
6
7
8
9
10
|
@Bean public JdbcCursorItemReader<CustomerCredit> itemReader() { return new JdbcCursorItemReaderBuilder<CustomerCredit>() .dataSource( this .dataSource) .name( "creditReader" ) .sql( "select ID, NAME, CREDIT from CUSTOMER" ) .rowMapper( new CustomerCreditRowMapper()) .build(); } |
JdbcCursorItemReader的好處在于使用簡(jiǎn)單,但是我們從它的sql就能發(fā)現(xiàn),JdbcCursorItemReader會(huì)一次把所有的數(shù)據(jù)全部拿回來(lái),當(dāng)數(shù)據(jù)量過(guò)大而服務(wù)器內(nèi)存不夠時(shí),就會(huì)遇到下面無(wú)法分配內(nèi)存的問(wèn)題:
報(bào)錯(cuò)信息為:Resource exhaustion event:The JVM was unable to allocate memory from the heap. 意思就是需要分配內(nèi)存的數(shù)據(jù)太多,但是無(wú)法找到足夠的內(nèi)存了。
反映在內(nèi)存里,堆內(nèi)存會(huì)呈現(xiàn)出如下的情況:
隨著每一次數(shù)據(jù)讀入,堆內(nèi)存都會(huì)增大,原因就在于JdbcCursorItemReader一次性讀回了所有的數(shù)據(jù),返回之后就會(huì)存在一個(gè)對(duì)象里面,而這個(gè)對(duì)象的尺寸過(guò)大,因此直接進(jìn)入了老年代。在數(shù)據(jù)遷移完成之前,這些數(shù)據(jù)都不會(huì)被回收。如下圖所示:
毫無(wú)疑問(wèn),當(dāng)我們的數(shù)據(jù)量大時(shí)不應(yīng)該使用這種類(lèi)型的reader來(lái)讀取數(shù)據(jù)。
JdbcPagingItemReader
JdbcPagingItemReader的作用和它的名字一樣,它可以分頁(yè)讀取數(shù)據(jù),但是使用起來(lái)相比于JdbcCursorItemReader更加復(fù)雜,示例代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
@Bean public JdbcPagingItemReader itemReader(DataSource dataSource, PagingQueryProvider queryProvider) { Map<String, Object> parameterValues = new HashMap<>(); parameterValues.put( "status" , "NEW" ); return new JdbcPagingItemReaderBuilder<CustomerCredit>() .name( "creditReader" ) .dataSource(dataSource) .queryProvider(queryProvider) .parameterValues(parameterValues) .rowMapper(customerCreditMapper()) .pageSize( 1000 ) .build(); } @Bean public SqlPagingQueryProviderFactoryBean queryProvider() { SqlPagingQueryProviderFactoryBean provider = new SqlPagingQueryProviderFactoryBean(); provider.setSelectClause( "select id, name, credit" ); provider.setFromClause( "from customer" ); provider.setWhereClause( "where status=:status" ); provider.setSortKey( "id" ); return provider; } |
可以看到我們能夠設(shè)置page的大小,JdbcPagingItemReader將根據(jù)這個(gè)頁(yè)的大小,每次讀取這么多的數(shù)據(jù),因此這些數(shù)據(jù)返回保存的對(duì)象,就只會(huì)是小對(duì)象,因此他們不會(huì)直接在老年代里分配,而是先分配在年輕代,隨著年輕代不斷變大,minor gc也不斷進(jìn)行,回收掉已經(jīng)處理完的數(shù)據(jù),老年代的內(nèi)存使用量不會(huì)有任何增大,類(lèi)似下圖:
老年代內(nèi)存不會(huì)有任何變化,年輕帶會(huì)隨著服務(wù)器數(shù)據(jù)遷移進(jìn)行而增大同時(shí)被回收。
在使用JdbcPagingItemReader時(shí),有一個(gè)必須注意的地方就是排序關(guān)鍵字是必須指定的,原因在于排序是分頁(yè)實(shí)現(xiàn)原理的技術(shù)基礎(chǔ)。sortKey和我們指定的其他字句一起構(gòu)建出SQL語(yǔ)句出來(lái)。在sortKey上必須使用unique key constraint約束,因?yàn)橹挥羞@樣才能得以確保執(zhí)行之間不會(huì)丟失任何數(shù)據(jù)。這也可以說(shuō)是JdbcCursorItemReader相對(duì)便利的一點(diǎn)優(yōu)勢(shì)。
總結(jié)
數(shù)據(jù)量小時(shí)選擇的方案差別不會(huì)很大,當(dāng)數(shù)據(jù)量大時(shí),為了有好的內(nèi)存表現(xiàn)則使用分頁(yè)的reader是必要的。但同時(shí),因?yàn)橐獙?shí)現(xiàn)分頁(yè),也會(huì)帶來(lái)一些不可避免的限制。
到此這篇關(guān)于spring batch使用reader讀數(shù)據(jù)的內(nèi)存容量問(wèn)題詳解的文章就介紹到這了,更多相關(guān)spring batch使用reader讀數(shù)據(jù)內(nèi)容請(qǐng)搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
原文鏈接:https://blog.csdn.net/topdeveloperr/article/details/88843186