目錄

前言

一、一些緣由

1、性能分析

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

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

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

3、日志的配置

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

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

2、執(zhí)行情況

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

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

2、提升后的效果

五、總結(jié)


前言

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

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

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

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

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

一、一些緣由

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

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

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

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

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

1、性能分析

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

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

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

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

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

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

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

????????上一節(jié)中對(duì)Mp的批量插入的方法進(jìn)行了調(diào)用,這里我們依然對(duì)saveBatch方法進(jìn)行簡(jiǎn)單的介紹,好讓大家對(duì)saveBatch有一個(gè)直觀的印象。我們可以打開(kāi)ServiceImpl的實(shí)現(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對(duì)象,然后調(diào)用批量執(zhí)行的方法。被調(diào)用的方法如下:

/*** 執(zhí)行批量操作** @param entityClass 實(shí)體類* @param log         日志對(duì)象* @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.

????????來(lái)看一下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)然大家在使用這個(gè)類的時(shí)候還是非常方便的,只要調(diào)用相應(yīng)的方法即可實(shí)現(xiàn)分批導(dǎo)入。

3、日志的配置

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

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

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

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

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

@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對(duì)象WKTWriter wktWriter = new WKTWriter();// 將Geometry對(duì)象轉(zhuǎn)換為WKT格式的字符串String wkt = wktWriter.write(geometry);String geom = "SRID=" + epsgCode +";" + wkt;//拼接srid,實(shí)現(xiàn)動(dòng)態(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("程序運(yùn)行耗時(shí):"+ time + "毫秒");Long s3 = System.currentTimeMillis();if(dataList.size() >0) {placeService.saveBatch(dataList, 600);}Long e3 = System.currentTimeMillis();System.out.println("空間入庫(kù)耗時(shí)::"+ (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日志的情況,我們來(lái)看一下它的相關(guān)性能指標(biāo)。

2、執(zhí)行情況

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

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

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

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

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

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

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

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

????????在系統(tǒng)中,我們采用logback來(lái)進(jìn)行日志的配置,因此我們首先需要在logback中進(jìn)行相應(yīng)的設(shè)置。將日志的級(jí)別從debug提升到error,只有在發(fā)生錯(cuò)誤的時(shí)候才進(jìn)行輸出。設(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.

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

2、提升后的效果

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

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

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

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

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

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

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

五、總結(jié)

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