目錄

前言

一、一些緣由

1、性能分析

二、插入方式調(diào)整

1、批量插入的實現(xiàn)

2、MP的批量插入實現(xiàn)

3、日志的配置

三、默認(rèn)處理方式

1、基礎(chǔ)程序代碼

2、執(zhí)行情況

四、提升調(diào)試日志等級

1、在logback中進行設(shè)置

2、提升后的效果

五、總結(jié)


前言

????????在現(xiàn)代軟件開發(fā)中,性能優(yōu)化是一個永恒的話題,尤其是在處理大規(guī)模數(shù)據(jù)時,如何提升數(shù)據(jù)庫操作的效率成為了一個關(guān)鍵問題。在數(shù)據(jù)庫操作中,批量插入空間矢量數(shù)據(jù)是一個常見的需求,尤其是在地理信息系統(tǒng)(GIS)和空間數(shù)據(jù)分析領(lǐng)域。調(diào)試日志是軟件開發(fā)中不可或缺的工具,它幫助開發(fā)者追蹤程序的運行狀態(tài),定位問題和異常。然而,日志記錄本身是一個資源密集型的操作,尤其是在生產(chǎn)環(huán)境中,過多的日志記錄可能會對性能產(chǎn)生負(fù)面影響。對于空間矢量數(shù)據(jù)的批量插入操作,這種影響尤為明顯,因為這類操作通常涉及大量的I/O操作和數(shù)據(jù)庫交互。

生產(chǎn)慎用之調(diào)試日志對空間矢量數(shù)據(jù)批量插入的性能影響-以MybatisPlus為例_Java日志調(diào)試

????????盡管調(diào)試日志對于開發(fā)和問題排查至關(guān)重要,但在生產(chǎn)環(huán)境中,它們可能會成為性能瓶頸。每次日志記錄都會涉及到I/O操作,這會占用CPU時間和磁盤I/O資源。在批量插入空間矢量數(shù)據(jù)時,如果日志記錄過于頻繁,可能會導(dǎo)致以下問題:

  1. 降低I/O效率:日志記錄會占用磁盤I/O資源,這可能會與數(shù)據(jù)庫操作競爭資源,導(dǎo)致整體性能下降。
  2. 增加延遲:日志記錄可能會引入額外的延遲,尤其是在高并發(fā)情況下,這會直接影響到批量插入操作的響應(yīng)時間。
  3. 資源競爭:日志記錄和數(shù)據(jù)庫操作可能會競爭有限的系統(tǒng)資源,如CPU和內(nèi)存,這可能會導(dǎo)致性能瓶頸。

????????調(diào)試日志是一把雙刃劍,它在幫助開發(fā)者解決問題的同時,也可能對生產(chǎn)環(huán)境的性能產(chǎn)生影響。在處理空間矢量數(shù)據(jù)的批量插入時,合理控制和優(yōu)化日志記錄是提升性能的關(guān)鍵。本文通過在MybatisPlus中調(diào)整插入SQL的輸出對比前后的耗時與內(nèi)存的占用,最大限度地減少對性能的負(fù)面影響。本文將深入探討這些策略的具體實現(xiàn)和最佳實踐,以期為Java開發(fā)者提供實用的指導(dǎo)和建議。

一、一些緣由

????????其實在日常工作當(dāng)中,空間矢量數(shù)據(jù)的數(shù)據(jù)量都是非常大。不僅是范圍大,屬性數(shù)據(jù)也尤其多,不僅屬性列多,而且數(shù)據(jù)行數(shù)也可能非常多。那么我們在使用ORM框架在操作這些數(shù)據(jù)的時候,在進行空間數(shù)據(jù)入庫的時候尤其需要注意性能的影響。有一些程序需要追求高性能,尤其是一些需要快速計算的場景,用戶需要盡快的將數(shù)據(jù)入庫,好開展后續(xù)的業(yè)務(wù)。

????????之前有一個朋友給發(fā)了私信,說他們在處理線上的生產(chǎn)數(shù)據(jù)時,數(shù)據(jù)的規(guī)模大約是幾十W的規(guī)模。在使用GeoTools讀取Shapefile后,然后調(diào)用Mybatis-Plus來進行數(shù)據(jù)入庫。它的整體性能不高,耗時比較久,然后就找到博主聊了一下。原始聊天截圖就不放出來了。分享其中遇到的一些問題:

????????1、這位朋友在進行批量數(shù)據(jù)入庫的時候,使用循環(huán)來進行調(diào)用,沒有使用批量操作。

????????2、系統(tǒng)的日志級別開的比較低,為了方便監(jiān)控程序,系統(tǒng)的日志級別在生產(chǎn)環(huán)境上也是Debug。

????????3、服務(wù)器在空間數(shù)據(jù)庫入庫時,內(nèi)存占用較高。

1、性能分析

????????在了解了一些程序的執(zhí)行細(xì)節(jié)之后,我也做了一個對照實驗。實驗的主要目的是對比應(yīng)用程序中調(diào)試日志的輸出對性能影響 ,主要方法就是在程序執(zhí)行時打開和關(guān)閉系統(tǒng)日志,通過觀察打開前后的應(yīng)用程序執(zhí)行消耗時間和使用Java VisualVM監(jiān)控的CPU和內(nèi)存消耗情況來對比。

二、插入方式調(diào)整

????????為了首先將應(yīng)用程序的插入調(diào)整到一個比較好的執(zhí)行狀態(tài),我們先把原來的循環(huán)插入的方式進行了修改,改成批量插入的形式。因此這里有必要對批量插入的具體實現(xiàn)進行一個簡單的介紹。

1、批量插入的實現(xiàn)

????????在我們的代碼中,使用的ORM框架是Mybatis-Plus,熟悉這個框架的小伙伴們一定知道。在MP中除了有單個插入的方法,還提供了一個批量插入的實現(xiàn)。因此,如果您是使用了MP這種的增強框架,那么改造起來還是比較快的,否則就需要大家自己去實現(xiàn)批量插入的方法。在MP中需要調(diào)用service提供的saveBatch(List,Size)即可。在在我的示例代碼中,實現(xiàn)批量插入的關(guān)鍵代碼如下所示:

Long s3 = System.currentTimeMillis();
if(dataList.size() >0) {placeService.saveBatch(dataList, 600);
}
Long e3 = System.currentTimeMillis();
System.out.println("空間入庫耗時::"+ (e3 - s3) + "毫秒");
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

2、MP的批量插入實現(xiàn)

????????上一節(jié)中對Mp的批量插入的方法進行了調(diào)用,這里我們依然對saveBatch方法進行簡單的介紹,好讓大家對saveBatch有一個直觀的印象。我們可以打開ServiceImpl的實現(xiàn)類中的以下代碼:

/*** 批量插入* @param entityList ignore* @param batchSize  ignore* @return ignore*/@Transactional(rollbackFor = Exception.class)@Overridepublic boolean saveBatch(Collection<T> entityList, int batchSize) {String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

????????這里的方法表示首先獲取sql的Statement對象,然后調(diào)用批量執(zhí)行的方法。被調(diào)用的方法如下:

/*** 執(zhí)行批量操作** @param entityClass 實體類* @param log         日志對象* @param list        數(shù)據(jù)集合* @param batchSize   批次大小* @param consumer    consumer* @param <E>         T* @return 操作結(jié)果* @since 3.4.0*/
public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {Assert.isFalse(batchSize < 1, "batchSize must not be less than one");return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, sqlSession -> {int size = list.size();int idxLimit = Math.min(batchSize, size);int i = 1;for (E element : list) {consumer.accept(sqlSession, element);if (i == idxLimit) {sqlSession.flushStatements();idxLimit = Math.min(idxLimit + batchSize, size);}i++;}});
}
  • 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.
  • 27.
  • 28.

????????來看一下insert方法的處理邏輯,最終的執(zhí)行update的方法如下:

@Override
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {final Configuration configuration = ms.getConfiguration();final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);final BoundSql boundSql = handler.getBoundSql();final String sql = boundSql.getSql();final Statement stmt;if (sql.equals(currentSql) && ms.equals(currentStatement)) {int last = statementList.size() - 1;stmt = statementList.get(last);applyTransactionTimeout(stmt);handler.parameterize(stmt);// fix Issues 322BatchResult batchResult = batchResultList.get(last);batchResult.addParameterObject(parameterObject);} else {Connection connection = getConnection(ms.getStatementLog());stmt = handler.prepare(connection, transaction.getTimeout());handler.parameterize(stmt);    // fix Issues 322currentSql = sql;currentStatement = ms;statementList.add(stmt);batchResultList.add(new BatchResult(ms, sql, parameterObject));}handler.batch(stmt);return BATCH_UPDATE_RETURN_VALUE;}
  • 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.

????????當(dāng)然大家在使用這個類的時候還是非常方便的,只要調(diào)用相應(yīng)的方法即可實現(xiàn)分批導(dǎo)入。

3、日志的配置

????????在實例的應(yīng)用開發(fā)過程中,日志的輸出與管理,我們使用Logback組件。在最開始的時候,在對比實驗中,首先我們采用默認(rèn)的方式,即對應(yīng)的ORM處理組件中的日志級別使用默認(rèn)方法。具體如何在Logback中進行日志的設(shè)置,請大家結(jié)合互聯(lián)網(wǎng)相關(guān)資料進行查詢,這些都是比較成熟的。

三、默認(rèn)處理方式

????????對比實驗的第一種實現(xiàn)方法就是采用默認(rèn)的方法,即使用默認(rèn)的日志級別。但是在最開始時,我們還是給出測試的代碼的全部。如果您也感興趣,可以替換相應(yīng)的文件來進行驗證這個過程。測試結(jié)果可能隨著數(shù)據(jù)量的不同,數(shù)據(jù)屬性字段的不同而有所不同。

1、基礎(chǔ)程序代碼

????????為了還原網(wǎng)友提出的問題,也能盡快的找到原因。我們這里就以之前的全球主要城市為例,重點講解如何進行數(shù)據(jù)的處理和融合,以及最終如何進入到數(shù)據(jù)庫中。實例代碼如下:

@Test
/*** * @throws Exception*/
public void shp2PostGIS() throws Exception {Long startTime = System.currentTimeMillis();File file = new File(SHP_FILE);if (!file.exists()) {System.out.println("文件不存在");}ShapefileDataStore store = new ShapefileDataStore(file.toURI().toURL());store.setCharset(Charset.defaultCharset());// 設(shè)置中文字符編碼// 獲取特征類型SimpleFeatureType featureType = store.getSchema(store.getTypeNames()[0]);CoordinateReferenceSystem crs = featureType.getGeometryDescriptor().getCoordinateReferenceSystem();Integer epsgCode = CRS.lookupEpsgCode(crs, true);List<HashMap<String, Object>> mapList = new ArrayList<HashMap<String,Object>>();ModelMapper modelMapper = new ModelMapper();//設(shè)置忽略字段PropertyMap<HashMap<String,Object>, Ne10mPopulatedPlaces> propertyMap = new PropertyMap<HashMap<String,Object>, Ne10mPopulatedPlaces>() {protected void configure() {skip(destination.getPkId());}};modelMapper.addMappings(propertyMap);//忽略大小寫modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);// 設(shè)置命名約定,將下劃線轉(zhuǎn)換為駝峰modelMapper.getConfiguration().setSourceNameTokenizer(NameTokenizers.UNDERSCORE).setDestinationNameTokenizer(NameTokenizers.CAMEL_CASE);//設(shè)置忽略模式modelMapper.getConfiguration().setSkipNullEnabled(true);Long s1 = System.currentTimeMillis();List<Ne10mPopulatedPlaces> dataList = new ArrayList<Ne10mPopulatedPlaces>();SimpleFeatureSource featureSource = store.getFeatureSource();// 執(zhí)行查詢SimpleFeatureCollection simpleFeatureCollection = featureSource.getFeatures();SimpleFeatureIterator itertor = simpleFeatureCollection.features();// 遍歷featurecollectionwhile (itertor.hasNext()) {HashMap<String, Object> map = new HashMap<String, Object>();SimpleFeature feature = itertor.next();Collection<Property> p = feature.getProperties();Iterator<Property> it = p.iterator();// 遍歷feature的propertieswhile (it.hasNext()) {Property pro = it.next();if (null != pro && null != pro.getValue()) {String field = pro.getName().toString();String value = pro.getValue().toString();map.put(field, value);}}// 獲取空間字段org.locationtech.jts.geom.Geometry geometry = (org.locationtech.jts.geom.Geometry) feature.getDefaultGeometry();// 創(chuàng)建WKTWriter對象WKTWriter wktWriter = new WKTWriter();// 將Geometry對象轉(zhuǎn)換為WKT格式的字符串String wkt = wktWriter.write(geometry);String geom = "SRID=" + epsgCode +";" + wkt;//拼接srid,實現(xiàn)動態(tài)寫入map.put("geom", geom);mapList.add(map);}Long e1 = System.currentTimeMillis();System.out.println("解析shp:"+ (e1 - s1) + "毫秒");Long s2 = System.currentTimeMillis();for(HashMap<String, Object> map : mapList) {Ne10mPopulatedPlaces places = modelMapper.map(map, Ne10mPopulatedPlaces.class);dataList.add(places);}Long e2 = System.currentTimeMillis();System.out.println("轉(zhuǎn)化shp:"+ (e2 - s2) + "毫秒");store.dispose();System.out.println(dataList.size());Long endTime = System.currentTimeMillis();Long time = endTime - startTime;System.out.println("程序運行耗時:"+ time + "毫秒");Long s3 = System.currentTimeMillis();if(dataList.size() >0) {placeService.saveBatch(dataList, 600);}Long e3 = System.currentTimeMillis();System.out.println("空間入庫耗時::"+ (e3 - s3) + "毫秒");
}
  • 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.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.

????????在不關(guān)閉執(zhí)行SQL日志的情況,我們來看一下它的相關(guān)性能指標(biāo)。

2、執(zhí)行情況

????????在默認(rèn)情況下,這段程序在執(zhí)行過程中會輸出大量的調(diào)試日志,如下圖所示:

生產(chǎn)慎用之調(diào)試日志對空間矢量數(shù)據(jù)批量插入的性能影響-以MybatisPlus為例_批量插入_02

????????在我們的控制臺中有許多的插入日志,同時可以看到,整個空間數(shù)據(jù)入庫的時間為29512毫秒,即將近30秒。除了時間的消耗,我們再來看一內(nèi)存方面的消耗。

生產(chǎn)慎用之調(diào)試日志對空間矢量數(shù)據(jù)批量插入的性能影響-以MybatisPlus為例_批量插入_03

????????可以很直觀的看到,在進行大量的日志輸出時,內(nèi)存的使用量還是比較大,同時根據(jù)不同的批次呈現(xiàn)一個比較有規(guī)律的上升和下降,而最大的內(nèi)存使用接近1000MB左右。 接下來,我們再來看一下關(guān)閉日志輸出后的效果。

四、提升調(diào)試日志等級

????????為了實現(xiàn)在運行時將這個插入SQL的日式調(diào)試等級,我們在Logback中進行相應(yīng)的配置。以此來驗證在提升SQL調(diào)試日志的等級后,這個批量插入的方法是不是有一個性能的提升。

1、在logback中進行設(shè)置

????????在系統(tǒng)中,我們采用logback來進行日志的配置,因此我們首先需要在logback中進行相應(yīng)的設(shè)置。將日志的級別從debug提升到error,只有在發(fā)生錯誤的時候才進行輸出。設(shè)置的關(guān)鍵代碼如下所示:

<!--  Ne10mPopulatedPlacesMapper 關(guān)閉調(diào)試日志 add by 夜郎king in 2024-11-26  -->
<logger name="com.yelang.project.extend.earthquake.mapper.Ne10mPopulatedPlacesMapper" level="error"/>
  • 1.
  • 2.

請注意,這里的com.yelang.project.extend.earthquake.mapper.Ne10mPopulatedPlacesMapper標(biāo)識我們需要關(guān)閉的ORM類的全名。我們將他的日志級別提升到了error。

2、提升后的效果

????????在將輸出日志關(guān)閉之后,在控制臺中首先就沒有了sql的調(diào)試日志,說明配置成功。

生產(chǎn)慎用之調(diào)試日志對空間矢量數(shù)據(jù)批量插入的性能影響-以MybatisPlus為例_批量插入_04

????????可以看到控制臺很干凈,調(diào)試的SQL日志已經(jīng)被清理掉。同時注意耗時情況,變成了7046,也就是7秒的時間就完成了處理。在來后臺看一下是不是真的處理成功。在數(shù)據(jù)庫進行相應(yīng)的數(shù)據(jù)查詢。

生產(chǎn)慎用之調(diào)試日志對空間矢量數(shù)據(jù)批量插入的性能影響-以MybatisPlus為例_批量插入_05

????????可以看到,數(shù)據(jù)的總條數(shù)也是7342條。因此可以判斷,關(guān)閉sql調(diào)試日志后,對時間的消耗降低了很多,從30秒優(yōu)化到了7秒,?大概提升76%;再來看一下內(nèi)存的占用情況。

生產(chǎn)慎用之調(diào)試日志對空間矢量數(shù)據(jù)批量插入的性能影響-以MybatisPlus為例_Java日志調(diào)試_06

????????相對于默認(rèn)的處理情況而言,提升了日志等級的處理方式,其內(nèi)存占用更加平穩(wěn),波動小。同時最大的內(nèi)存占用在700MB左右,更多是500MB以下。從側(cè)面也說明了優(yōu)化的效果。

五、總結(jié)

?????????以上就是本文的主要內(nèi)容,本文通過在MybatisPlus中調(diào)整插入SQL的輸出對比前后的耗時與內(nèi)存的占用,最大限度地減少對性能的負(fù)面影響。文章通過對照實驗,對比了開啟調(diào)試日志和關(guān)閉調(diào)試日志后的數(shù)據(jù)插入性能,從對比實驗結(jié)果可以看到。關(guān)閉調(diào)試日志后,我們的應(yīng)用程序耗時更短,同時內(nèi)存的占用也更低。如果在生產(chǎn)環(huán)境中進行使用,尤其是新手同志,為了觀察參數(shù)就留下了很多調(diào)試信息,這樣反而加大了系統(tǒng)的負(fù)擔(dān)。所以要請大家一定綜合理性的評估,關(guān)閉不必要的調(diào)試日志,讓應(yīng)用程序的性能最大。行文倉庫,定有許多不足之處,如有不足,在此懇請各位專家在評論區(qū)批評指出,不勝感激。