成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

100000行級別數據的 Excel 導入優化之路

數據庫 其他數據庫
在優化的過程中,我還發現了一個特別影響性能的東西:info 日志,還是使用 41w行、25列、45.5m 數據,在 開始-數據讀取完畢 之間每 1000 行打印一條 info 日志,緩存校驗數據-校驗完畢 之間每行打印 3+ 條 info 日志,日志框架使用 Slf4j 。打印并持久化到磁盤。

今天分享一篇大數據量Excel導入如何優化的文章,非常不錯。

需求說明

項目中有一個 Excel 導入的需求:繳費記錄導入。

由實施 / 用戶 將別的系統的數據填入我們系統中的 Excel 模板,應用將文件內容讀取、校對、轉換之后產生欠費數據、票據、票據詳情并存儲到數據庫中。

在接手之前可能由于之前導入的數據量并不多沒有對效率有過高的追求。但是到了 4.0 版本,預估導入時Excel 行數會是 10w+ 級別,而往數據庫插入的數據量是大于 3n 的,也就是說 10w 行的 Excel,則至少向數據庫插入 30w 行數據。因此優化原來的導入代碼是勢在必行的。逐步分析和優化了導入的代碼,使之在百秒內完成(最終性能瓶頸在數據庫的處理速度上,測試服務器 4g 內存不僅放了數據庫,還放了很多微服務應用。處理能力不太行)。具體的過程如下,每一步都有列出影響性能的問題和解決的辦法。

導入 Excel 的需求在系統中還是很常見的,優化辦法可能不是最優的,歡迎讀者在評論區留言交流提供更優的思路。

一些細節

  • 數據導入:導入使用的模板由系統提供,格式是 xlsx (支持 65535+行數據) ,用戶按照表頭在對應列寫入相應的數據。
  • 數據校驗:數據校驗有兩種:

字段長度、字段正則表達式校驗等,內存內校驗不存在外部數據交互。對性能影響較小。

數據重復性校驗,如票據號是否和系統已存在的票據號重復(需要查詢數據庫,十分影響性能)。

  • 數據插入:測試環境數據庫使用 MySQL 5.7,未分庫分表,連接池使用 Druid。

迭代記錄

1. 第一版:POI + 逐行查詢校對 + 逐行插入

這個版本是最古老的版本,采用原生 POI,手動將 Excel 中的行映射成 ArrayList 對象,然后存儲到 List,代碼執行的步驟如下:

  • 手動讀取 Excel 成 List
  • 循環遍歷,在循環中進行以下步驟

檢驗字段長度。

一些查詢數據庫的校驗,比如校驗當前行欠費對應的房屋是否在系統中存在,需要查詢房屋表。

寫入當前行數據

  • 返回執行結果,如果出錯 / 校驗不合格。則返回提示信息并回滾數據
  • 顯而易見的,這樣實現一定是趕工趕出來的,后續可能用的少也沒有察覺到性能問題,但是它最多適用于個位數/十位數級別的數據。存在以下明顯的問題:
  • 查詢數據庫的校驗對每一行數據都要查詢一次數據庫,應用訪問數據庫來回的網絡IO次數被放大了 n 倍,時間也就放大了 n 倍。
  • 寫入數據也是逐行寫入的,問題和上面的一樣。

數據讀取使用原生 POI,代碼十分冗余,可維護性差。

2. 第二版:EasyPOI + 緩存數據庫查詢操作 + 批量插入

針對第一版分析的三個問題,分別采用以下三個方法優化。

緩存數據,以空間換時間

逐行查詢數據庫校驗的時間成本主要在來回的網絡IO中,優化方法也很簡單。將參加校驗的數據全部緩存到 HashMap 中。直接到 HashMap 去命中。

例如:校驗行中的房屋是否存在,原本是要用 區域 + 樓宇 + 單元 + 房號 去查詢房屋表匹配房屋ID,查到則校驗通過,生成的欠單中存儲房屋ID,校驗不通過則返回錯誤信息給用戶。而房屋信息在導入欠費的時候是不會更新的。并且一個小區的房屋信息也不會很多(5000以內)因此我采用一條SQL,將該小區下所有的房屋以 區域/樓宇/單元/房號 作為 key,以 房屋ID 作為 value,存儲到 HashMap 中,后續校驗只需要在 HashMap 中命中。

自定義 SessionMapper

Mybatis 原生是不支持將查詢到的結果直接寫人一個 HashMap 中的,需要自定義 SessionMapper。

SessionMapper 中指定使用 MapResultHandler 處理 SQL 查詢的結果集。

@Repository
public class SessionMapper extends SqlSessionDaoSupport {

@Resource
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
super.setSqlSessionFactory(sqlSessionFactory);
}

// 區域樓宇單元房號 - 房屋ID
@SuppressWarnings("unchecked")
public Map<String, Long> getHouseMapByAreaId(Long areaId) {
MapResultHandler handler = new MapResultHandler();

this.getSqlSession().select(BaseUnitMapper.class.getName()+".getHouseMapByAreaId", areaId, handler);
Map<String, Long> map = handler.getMappedResults();
return map;
}
}

MapResultHandler 處理程序,將結果集放入 HashMap。

public class MapResultHandler implements ResultHandler {
private final Map mappedResults = new HashMap();

@Override
public void handleResult(ResultContext context) {
@SuppressWarnings("rawtypes")
Map map = (Map)context.getResultObject();
mappedResults.put(map.get("key"), map.get("value"));
}

public Map getMappedResults() {
return mappedResults;
}
}

示例 Mapper。

@Mapper
@Repository
public interface BaseUnitMapper {
// 收費標準綁定 區域樓宇單元房號 - 房屋ID
Map<String, Long> getHouseMapByAreaId(@Param("areaId") Long areaId);
}

示例 Mapper.xml

<select id="getHouseMapByAreaId" resultMap="mapResultLong">
SELECT
CONCAT( h.bulid_area_name, h.build_name, h.unit_name, h.house_num ) k,
h.house_id v
FROM
base_house h
WHERE
h.area_id = ##{areaId}
GROUP BY
h.house_id
</select>

<resultMap id="mapResultLong" type="java.util.HashMap">
<result property="key" column="k" javaType="string" jdbcType="VARCHAR"/>
<result property="value" column="v" javaType="long" jdbcType="INTEGER"/>
</resultMap>

之后在代碼中調用 SessionMapper 類對應的方法即可。

使用 values 批量插入

MySQL insert 語句支持使用 values (),(),() 的方式一次插入多行數據,通過 mybatis foreach 結合 java 集合可以實現批量插入,代碼寫法如下:

<insert id="insertList">
insert into table(colom1, colom2)
values
<foreach collection="list" item="item" index="index" separator=",">
( ##{item.colom1}, ##{item.colom2})
</foreach>
</insert>

使用 EasyPOI 讀寫 Excel

http://doc.wupaas.com/docs/easypoi/easypoi-1c0u4mo8p4ro8 采用基于注解的導入導出,修改注解就可以修改Excel,非常方便,代碼維護起來也容易。

3. 第三版:EasyExcel + 緩存數據庫查詢操作 + 批量插入

第二版采用 EasyPOI 之后,對于幾千、幾萬的 Excel 數據已經可以輕松導入了,不過耗時有點久(5W 數據 10分鐘左右寫入到數據庫)不過由于后來導入的操作基本都是開發在一邊看日志一邊導入,也就沒有進一步優化。但是好景不長,有新小區需要遷入,票據 Excel 有 41w 行,這個時候使用 EasyPOI 在開發環境跑直接就 OOM 了,增大 JVM 內存參數之后,雖然不 OOM 了,但是 CPU 占用 100% 20 分鐘仍然未能成功讀取全部數據。故在讀取大 Excel 時需要再優化速度。莫非要我這個渣渣去深入 POI 優化了嗎?別慌,先上 GITHUB 找找別的開源項目。這時阿里 EasyExcel 映入眼簾:

圖片

emmm,這不是為我量身定制的嗎!趕緊拿來試試。EasyExcel 采用和 EasyPOI 類似的注解方式讀寫 Excel,因此從 EasyPOI 切換過來很方便,分分鐘就搞定了。也確實如阿里大神描述的:41w行、25列、45.5m 數據讀取平均耗時 50s,因此對于大 Excel 建議使用 EasyExcel 讀取。

4. 第四版:優化數據插入速度

在第二版插入的時候,我使用了 values 批量插入代替逐行插入。每 30000 行拼接一個長 SQL、順序插入。整個導入方法這塊耗時最多,非常拉跨。后來我將每次拼接的行數減少到 10000、5000、3000、1000、500 發現執行最快的是 1000。結合網上一些對 innodb_buffer_pool_size 描述我猜是因為過長的 SQL 在寫操作的時候由于超過內存閾值,發生了磁盤交換。關注公號:碼猿技術專欄,回復關鍵詞:1111 獲取阿里內部Java調優手冊;限制了速度,另外測試服務器的數據庫性能也不怎么樣,過多的插入他也處理不過來。所以最終采用每次 1000 條插入。

每次 1000 條插入后,為了榨干數據庫的 CPU,那么網絡IO的等待時間就需要利用起來,這個需要多線程來解決,而最簡單的多線程可以使用 并行流 來實現,接著我將代碼用并行流來測試了一下:

10w行的 excel、42w 欠單、42w記錄詳情、2w記錄、16 線程并行插入數據庫、每次 1000 行。插入時間 72s,導入總時間 95 s。

圖片

并行插入工具類

并行插入的代碼我封裝了一個函數式編程的工具類,也提供給大家

/**
* 功能:利用并行流快速插入數據
*
* @author Keats
* @date 2020/7/1 9:25
*/
public class InsertConsumer {
/**
* 每個長 SQL 插入的行數,可以根據數據庫性能調整
*/
private final static int SIZE = 1000;

/**
* 如果需要調整并發數目,修改下面方法的第二個參數即可
*/
static {
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "4");
}

/**
* 插入方法
*
* @param list 插入數據集合
* @param consumer 消費型方法,直接使用 mapper::method 方法引用的方式
* @param <T> 插入的數據類型
*/
public static <T> void insertData(List<T> list, Consumer<List<T>> consumer) {
if (list == null || list.size() < 1) {
return;
}

List<List<T>> streamList = new ArrayList<>();

for (int i = 0; i < list.size(); i += SIZE) {
int j = Math.min((i + SIZE), list.size());
List<T> subList = list.subList(i, j);
streamList.add(subList);
}
// 并行流使用的并發數是 CPU 核心數,不能局部更改。全局更改影響較大,斟酌
streamList.parallelStream().forEach(consumer);
}
}

這里多數使用到很多 Java8 的API,不了解的朋友可以翻看我之前關于 Java 的博客。方法使用起來很簡單

InsertConsumer.insertData(feeList, arrearageMapper::insertList);

其他影響性能的內容

日志

避免在 for 循環中打印過多的 info 日志

在優化的過程中,我還發現了一個特別影響性能的東西:info 日志,還是使用 41w行、25列、45.5m 數據,在 開始-數據讀取完畢 之間每 1000 行打印一條 info 日志,緩存校驗數據-校驗完畢 之間每行打印 3+ 條 info 日志,日志框架使用 Slf4j 。打印并持久化到磁盤。下面是打印日志和不打印日志效率的差別:

打印日志

圖片

不打印日志

圖片

我以為是我選錯 Excel 文件了,又重新選了一次,結果依舊

圖片

緩存校驗數據-校驗完畢 不打印日志耗時僅僅是打印日志耗時的 1/10 !

總結

提升Excel導入速度的方法:

  • 使用更快的 Excel 讀取框架(推薦使用阿里 EasyExcel)。
  • 對于需要與數據庫交互的校驗、按照業務邏輯適當的使用緩存。用空間換時間。
  • 使用 values(),(),() 拼接長 SQL 一次插入多行數據。
  • 使用多線程插入數據,利用掉網絡IO等待時間(推薦使用并行流,簡單易用)。
  • 避免在循環中打印無用的日志。
責任編輯:武曉燕 來源: 碼猿技術專欄
相關推薦

2024-02-05 13:28:00

Excel優化服務器

2023-06-29 08:22:43

數據Excel模板

2012-04-25 09:24:17

Java

2020-08-06 08:00:51

數據分頁優化

2020-09-01 17:19:36

數據監控建模

2023-12-07 07:46:21

MySQL寫入點LSN

2020-12-18 10:40:00

ExcelJava代碼

2010-04-14 09:24:29

在Oracle數據庫

2024-11-08 09:34:54

2019-05-22 15:57:11

面試ES性能數據

2016-05-23 13:50:23

UberHadoopSpark

2022-05-11 09:02:27

Python數據庫Excel

2013-02-27 15:48:05

自動化備份FacebookPB級別數據庫

2012-08-06 14:02:09

萬國數據數據中心GDS

2011-03-10 10:50:01

excelsql數據庫

2010-08-23 16:55:53

SharePoint

2020-07-08 13:46:25

Python數據分析預處理

2011-07-12 13:01:11

ExcelOracleSql Server

2020-10-06 18:57:14

PostgreSQL數據庫數據導入

2010-04-22 11:58:00

Oracle數據庫
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 色综合欧美| 天天操天天干天天曰 | 国产精品自拍一区 | 在线久草| 亚洲精品一区二区三区中文字幕 | 秋霞电影院午夜伦 | 国产免费看 | 久久久久av| 日韩精品极品视频在线观看免费 | 免费看a | www.中文字幕 | 亚洲 欧美 另类 综合 偷拍 | 中文字幕精品一区 | 久久久国产一区二区三区 | 亚洲欧美日韩国产 | 国产免费黄网 | 国产精品日韩 | 在线看片网站 | 欧美一级α片 | 日本欧美视频 | 成人精品国产免费网站 | www.天天操 | 成人九区 | 麻豆av电影网 | 亚洲欧洲精品一区 | 日韩精品专区在线影院重磅 | 日本精品视频在线 | 日韩精品成人av | 色播视频在线观看 | 一区二区三区欧美在线观看 | 中文字幕av第一页 | 一片毛片| 国产精品久久久爽爽爽麻豆色哟哟 | 欧美精品一区二区三区四区五区 | 欧美三级视频在线观看 | 在线看成人av | 国产一区二区三区在线 | 亚洲视频一区二区三区 | 日韩午夜| 国产区在线免费观看 | 欧美性受xxxx |