4次迭代!10w行級別數(shù)據的Excel導入優(yōu)化記錄

鏈接 : https://www.cnblogs.com/keatsCoder/p/13217561.html
excel導入是個很常見的業(yè)務需求,不同的數(shù)據級別使用的方案效率差別也會很大,看看作者的4次優(yōu)化,有沒給你帶來一些啟發(fā)!
需求說明
一些細節(jié)
迭代記錄
第一版:POI + 逐行查詢校對 + 逐行插入
第二版:EasyPOI + 緩存數(shù)據庫查詢操作 + 批量插入
第三版:EasyExcel + 緩存數(shù)據庫查詢操作 + 批量插入
第四版:優(yōu)化數(shù)據插入速度
其他影響性能的內容
日志
總結
需求說明
項目中有一個 Excel 導入的需求:繳費記錄導入
由實施 / 用戶 將別的系統(tǒng)的數(shù)據填入我們系統(tǒng)中的 Excel 模板,應用將文件內容讀取、校對、轉換之后產生欠費數(shù)據、票據、票據詳情并存儲到數(shù)據庫中。
在我接手之前可能由于之前導入的數(shù)據量并不多沒有對效率有過高的追求。但是到了 4.0 版本,我預估導入時 Excel 行數(shù)會是 10w+ 級別,而往數(shù)據庫插入的數(shù)據量是大于 3n 的,也就是說 10w 行的 Excel,則至少向數(shù)據庫插入 30w 行數(shù)據。因此優(yōu)化原來的導入代碼是勢在必行的。我逐步分析和優(yōu)化了導入的代碼,使之在百秒內完成 (最終性能瓶頸在數(shù)據庫的處理速度上,測試服務器 4g 內存不僅放了數(shù)據庫,還放了很多微服務應用。處理能力不太行)。具體的過程如下,每一步都有列出影響性能的問題和解決的辦法。
導入 Excel 的需求在系統(tǒng)中還是很常見的,我的優(yōu)化辦法可能不是最優(yōu)的,歡迎讀者在評論區(qū)留言交流提供更優(yōu)的思路
一些細節(jié)
數(shù)據導入:導入使用的模板由系統(tǒng)提供,格式是 xlsx (支持 65535 + 行數(shù)據) ,用戶按照表頭在對應列寫入相應的數(shù)據
數(shù)據校驗:數(shù)據校驗有兩種:
字段長度、字段正則表達式校驗等,內存內校驗不存在外部數(shù)據交互。對性能影響較小
數(shù)據重復性校驗,如票據號是否和系統(tǒng)已存在的票據號重復 (需要查詢數(shù)據庫,十分影響性能)
數(shù)據插入:測試環(huán)境數(shù)據庫使用 MySQL 5.7,未分庫分表,連接池使用 Druid
迭代記錄
第一版:POI + 逐行查詢校對 + 逐行插入
這個版本是最古老的版本,采用原生 POI,手動將 Excel 中的行映射成 ArrayList 對象,然后存儲到 List
手動讀取 Excel 成 List
循環(huán)遍歷,在循環(huán)中進行以下步驟
檢驗字段長度
一些查詢數(shù)據庫的校驗,比如校驗當前行欠費對應的房屋是否在系統(tǒng)中存在,需要查詢房屋表
寫入當前行數(shù)據
返回執(zhí)行結果,如果出錯 / 校驗不合格。則返回提示信息并回滾數(shù)據
顯而易見的,這樣實現(xiàn)一定是趕工趕出來的,后續(xù)可能用的少也沒有察覺到性能問題,但是它最多適用于個位數(shù) / 十位數(shù)級別的數(shù)據。存在以下明顯的問題:
查詢數(shù)據庫的校驗對每一行數(shù)據都要查詢一次數(shù)據庫,應用訪問數(shù)據庫來回的網絡 IO 次數(shù)被放大了 n 倍,時間也就放大了 n 倍
寫入數(shù)據也是逐行寫入的,問題和上面的一樣
數(shù)據讀取使用原生 POI,代碼十分冗余,可維護性差。
第二版:EasyPOI + 緩存數(shù)據庫查詢操作 + 批量插入
針對第一版分析的三個問題,分別采用以下三個方法優(yōu)化
緩存數(shù)據,以空間換時間
逐行查詢數(shù)據庫校驗的時間成本主要在來回的網絡 IO 中,優(yōu)化方法也很簡單。將參加校驗的數(shù)據全部緩存到 HashMap 中。直接到 HashMap 去命中。
粉絲福利:教妹子手擼了50個項目實戰(zhàn)后,我住院了……
例如:校驗行中的房屋是否存在,原本是要用 區(qū)域 + 樓宇 + 單元 + 房號 去查詢房屋表匹配房屋 ID,查到則校驗通過,生成的欠單中存儲房屋 ID,校驗不通過則返回錯誤信息給用戶。而房屋信息在導入欠費的時候是不會更新的。并且一個小區(qū)的房屋信息也不會很多 (5000 以內) 因此我采用一條 SQL,將該小區(qū)下所有的房屋以 區(qū)域 / 樓宇 / 單元 / 房號 作為 key,以 房屋 ID 作為 value,存儲到 HashMap 中,后續(xù)校驗只需要在 HashMap 中命中
自定義 SessionMapper
Mybatis 原生是不支持將查詢到的結果直接寫人一個 HashMap 中的,需要自定義 SessionMapper
SessionMapper 中指定使用 MapResultHandler 處理 SQL 查詢的結果集
@Repository
public class SessionMapper extends SqlSessionDaoSupport {
@Resource
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
super.setSqlSessionFactory(sqlSessionFactory);
}
// 區(qū)域樓宇單元房號 - 房屋ID
@SuppressWarnings("unchecked")
public Map getHouseMapByAreaId(Long areaId) {
MapResultHandler handler = new MapResultHandler();
this.getSqlSession().select(BaseUnitMapper.class.getName()+".getHouseMapByAreaId", areaId, handler);
Map 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 {
// 收費標準綁定 區(qū)域樓宇單元房號 - 房屋ID
Map getHouseMapByAreaId(@Param("areaId") Long areaId) ;
}
示例 Mapper.xml
"mapResultLong" type="java.util.HashMap">
"key" column="k" javaType="string" jdbcType="VARCHAR"/>
"value" column="v" javaType="long" jdbcType="INTEGER"/>
之后在代碼中調用 SessionMapper 類對應的方法即可。
使用 values 批量插入
MySQL insert 語句支持使用 values (),(),() 的方式一次插入多行數(shù)據,通過 mybatis foreach 結合 java 集合可以實現(xiàn)批量插入,代碼寫法如下:
"insertList">
insert into table(colom1, colom2)
values
="list" item="item" index="index" separator=",">
( #{item.colom1}, #{item.colom2})
使用 EasyPOI 讀寫 Excel
EasyPOI 采用基于注解的導入導出, 修改注解就可以修改 Excel,非常方便,代碼維護起來也容易。
第三版:EasyExcel + 緩存數(shù)據庫查詢操作 + 批量插入
第二版采用 EasyPOI 之后,對于幾千、幾萬的 Excel 數(shù)據已經可以輕松導入了,不過耗時有點久 (5W 數(shù)據 10 分鐘左右寫入到數(shù)據庫) 不過由于后來導入的操作基本都是開發(fā)在一邊看日志一邊導入,也就沒有進一步優(yōu)化。但是好景不長,有新小區(qū)需要遷入,票據 Excel 有 41w 行,這個時候使用 EasyPOI 在開發(fā)環(huán)境跑直接就 OOM 了,增大 JVM 內存參數(shù)之后,雖然不 OOM 了,但是 CPU 占用 100% 20 分鐘仍然未能成功讀取全部數(shù)據。故在讀取大 Excel 時需要再優(yōu)化速度。莫非要我這個渣渣去深入 POI 優(yōu)化了嗎?別慌,先上 GITHUB 找找別的開源項目。這時阿里 EasyExcel 映入眼簾:

emmm,這不是為我量身定制的嗎!趕緊拿來試試。EasyExcel 采用和 EasyPOI 類似的注解方式讀寫 Excel,因此從 EasyPOI 切換過來很方便,分分鐘就搞定了。也確實如阿里大神描述的:41w 行、25 列、45.5m 數(shù)據讀取平均耗時 50s,因此對于大 Excel 建議使用 EasyExcel 讀取。
第四版:優(yōu)化數(shù)據插入速度
在第二版插入的時候,我使用了 values 批量插入代替逐行插入。每 30000 行拼接一個長 SQL、順序插入。整個導入方法這塊耗時最多,非常拉跨。后來我將每次拼接的行數(shù)減少到 10000、5000、3000、1000、500 發(fā)現(xiàn)執(zhí)行最快的是 1000。結合網上一些對 innodb_buffer_pool_size 描述我猜是因為過長的 SQL 在寫操作的時候由于超過內存閾值,發(fā)生了磁盤交換。限制了速度,另外測試服務器的數(shù)據庫性能也不怎么樣,過多的插入他也處理不過來。所以最終采用每次 1000 條插入。
搜索公縱號:MarkerHub,關注回復[?vue?]獲取前后端入門教程!
每次 1000 條插入后,為了榨干數(shù)據庫的 CPU,那么網絡 IO 的等待時間就需要利用起來,這個需要多線程來解決,而最簡單的多線程可以使用 并行流 來實現(xiàn),接著我將代碼用并行流來測試了一下:
10w 行的 excel、42w 欠單、42w 記錄詳情、2w 記錄、16 線程并行插入數(shù)據庫、每次 1000 行。插入時間 72s,導入總時間 95 s。

并行插入工具類
并行插入的代碼我封裝了一個函數(shù)式編程的工具類,也提供給大家
/**
* 功能:利用并行流快速插入數(shù)據
*
* @author Keats
* @date 2020/7/1 9:25
*/
public class InsertConsumer {
/**
* 每個長 SQL 插入的行數(shù),可以根據數(shù)據庫性能調整
*/
private final static int SIZE = 1000;
/**
* 如果需要調整并發(fā)數(shù)目,修改下面方法的第二個參數(shù)即可
*/
static {
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "4");
}
/**
* 插入方法
*
* @param list 插入數(shù)據集合
* @param consumer 消費型方法,直接使用 mapper::method 方法引用的方式
* @param 插入的數(shù)據類型
*/
public static void insertData(List list, Consumer> consumer)
{
if (list == null || list.size() < 1) {
return;
}
List> streamList = new ArrayList<>();
for (int i = 0; i < list.size(); i += SIZE) {
int j = Math.min((i + SIZE), list.size());
List subList = list.subList(i, j);
streamList.add(subList);
}
// 并行流使用的并發(fā)數(shù)是 CPU 核心數(shù),不能局部更改。全局更改影響較大,斟酌
streamList.parallelStream().forEach(consumer);
}
}
這里多數(shù)使用到很多 Java8 的 API,不了解的朋友可以翻看我之前關于 Java 的博客。方法使用起來很簡單
InsertConsumer.insertData(feeList, arrearageMapper::insertList);
其他影響性能的內容
日志
避免在 for 循環(huán)中打印過多的 info 日志
在優(yōu)化的過程中,我還發(fā)現(xiàn)了一個特別影響性能的東西:info 日志,還是使用 41w 行、25 列、45.5m 數(shù)據,在?開始 - 數(shù)據讀取完畢?之間每 1000 行打印一條 info 日志,緩存校驗數(shù)據 - 校驗完畢?之間每行打印 3+ 條 info 日志,日志框架使用 Slf4j 。打印并持久化到磁盤。下面是打印日志和不打印日志效率的差別
打印日志

不打印日志

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

緩存校驗數(shù)據 - 校驗完畢 不打印日志耗時僅僅是打印日志耗時的 1/10 !
總結
提升 Excel 導入速度的方法:
使用更快的 Excel 讀取框架 (推薦使用阿里 EasyExcel)
對于需要與數(shù)據庫交互的校驗、按照業(yè)務邏輯適當?shù)氖褂镁彺妗S每臻g換時間
使用 values(),(),() 拼接長 SQL 一次插入多行數(shù)據
使用多線程插入數(shù)據,利用掉網絡 IO 等待時間 (推薦使用并行流,簡單易用)
避免在循環(huán)中打印無用的日志
如果你覺得閱讀后有收獲,不妨點個推薦吧
- END -

推薦閱讀
2.??兩難!到底用Apache BeanUtils還是Spring BeanUtils?
3.??Spring的Controller是單例還是多例?怎么保證并發(fā)的安全
4.??京東把 Elasticsearch 用得真牛逼!日均5億訂單查詢完美解決!
之前博主分享了很多資源,有的已經刪除了(你懂得),如果有的你當時沒有領到還想領得就可以加我微信,我在發(fā)給你,你需要得資源也可以給我說,我盡力給你找~
Java開發(fā)大型電商系統(tǒng)商城實戰(zhàn)視頻教程
好文章,我在看?
