厲害了!12秒將百萬數據通過EasyExcel導入MySQL數據庫中

架構的小事 2024-06-09 15:16:49
一、寫在開頭

我們在上一篇文章中提到了通過EasyExcel處理Mysql百萬數據的導入功能(原文:https://www.cnblogs.com/JavaBuild/p/18185854),當時我們經過測試數據的反複測驗,100萬條放在excel中的數據,4個字段的情況下,導入數據庫,平均耗時500秒,這對于我們來說肯定難以接受,今天我們就來做一次性能優化。

二、性能瓶頸分析

一般的大數據量excel入庫的場景中,耗時大概在如下幾點裏:

耗時1: 百萬數據讀取,字段數量,sheet頁個數,文件體積;針對這種情況,我們要選擇分片讀取,選擇合適的集合存儲。耗時2: 百萬數據的校驗,逐行分字段校驗;這種情況的耗時會隨著字段個數逐漸增加,目前我們的案例中不設及,暫不展開。耗時3: 百萬數據的寫入;選擇合適的寫入方式,如Mybatis-plus的分批插入,采用多線程處理等。三、針對耗時1進行優化

耗時2的場景我們在案例中並未用到,耗時1中針對百萬級數據的讀取,我們必然要選擇分片讀取,分片處理,這在我們上一篇文章中就已經采用了該方案,這裏通過實現EasyExcel的ReadListener頁面讀取監聽器,實現其invoke方法,在方法中我們增加BATCH_COUNT(單次讀取條數)配置,來進行分片讀取。讀取完後,我們一定要選擇合適的集合容器存放臨時數據,不同集合之間的增加數據性能存在差異這裏我們選擇ArrayList。

【優化前代碼片段】

@Slf4j@Servicepublic EasyExcelImportHandler implements ReadListener<User> { /*成功數據*/ private final CopyOnWriteArrayList<User> successList = new CopyOnWriteArrayList<>(); /*單次處理條數*/ private final static int BATCH_COUNT = 20000; @Resource private ThreadPoolExecutor threadPoolExecutor; @Resource private UserMapper userMapper; @Override public void invoke(User user, AnalysisContext analysisContext) { if(StringUtils.isNotBlank(user.getName())){ successList.add(user); return; } if(successList.size() >= BATCH_COUNT){ log.info("讀取數據:{}", successList.size()); saveData(); } } /// ///}

【優化後代碼片段】

@Slf4j@Servicepublic EasyExcelImportHandler implements ReadListener<User> { /*成功數據*/ // private final CopyOnWriteArrayList<User> successList = new CopyOnWriteArrayList<>(); private final List<User> successList = new ArrayList<>(); /*單次處理條數,有原來2萬變爲10萬*/ private final static int BATCH_COUNT = 100000; @Resource private ThreadPoolExecutor threadPoolExecutor; @Resource private UserMapper userMapper; @Override public void invoke(User user, AnalysisContext analysisContext) { if (StringUtils.isNotBlank(user.getName())) { successList.add(user); return; } //size是否爲100000條:這裏其實就是分批.當數據等于10w的時候執行一次插入 if (successList.size() >= BATCH_COUNT) { log.info("讀取數據:{}", successList.size()); saveData(); //清理集合便于GC回收 successList.clear(); } } /// /// }

這裏面我們主要做了2點優化,1)將原來的線程安全的CopyOnWriteArrayList換爲ArrayList,前者雖然可保線程安全,但存儲數據性能很差;2)將原來單批次2000調整爲100000,這個參數是因電腦而異的,並沒有最佳數值。

【注】:本文中的代碼僅針對優化點貼出,完整代碼參考文首中的上一篇文章連接哈!

四、針對耗時3進行優化

針對耗時3的處理方案,我們這裏准備了2個:JDBC分批插入+手動事務控制、多線程+Mybatis-Plus批量插入。

4.1 JDBC分批插入+手動事務控制

很多博文中都說mybatis批量插入性能低,有人建議使用原生的JDBC進行處理,那咱們就采用這種方案來測試一下。

首先我們既然要通過jdbc連接數據庫進行操作,那就先准備一個連接工具類吧

public JdbcConnectUtil { private static String driver; private static String url; private static String name; private static String password; /** * 創建數據Properties集合對象加載加載配置文件 */ static { Properties properties = new Properties(); try { properties.load(JdbcConnectUtil.class.getClassLoader().getResourceAsStream("generator.properties")); driver = properties.getProperty("jdbc.driverClass"); url = properties.getProperty("jdbc.connectionURL"); name = properties.getProperty("jdbc.userId"); password = properties.getProperty("jdbc.password"); Class.forName(driver); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } /** * 獲取數據庫連接對象 * @return * @throws Exception */ public static Connection getConnect() throws Exception { return DriverManager.getConnection(url, name, password); } /** * 關閉數據庫相關資源 * @param conn * @param ps * @param rs */ public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (conn != null) conn.close(); if (ps != null) ps.close(); if (rs != null) rs.close(); } catch (SQLException e) { throw new RuntimeException(e); } } public static void close(Connection conn, PreparedStatement ps) { close(conn, ps, null); } public static void close(Connection conn, ResultSet rs) { close(conn, null, rs); }}

有了工具類後,我們就可以在EasyExcelImportHandler類中進行JDBC導入邏輯的實現啦。

/** * jdbc+事務處理 */ public void import4Jdbc(){ //分批讀取+JDBC分批插入+手動事務控制 Connection conn = null; //JDBC存儲過程 PreparedStatement ps = null; try { //建立jdbc數據庫連接 conn = JdbcConnectUtil.getConnect(); //關閉事務默認提交 conn.setAutoCommit(false); String sql = "insert into user (id,name, phone_num, address) values"; sql += "(?,?,?,?)"; ps = conn.prepareStatement(sql); for (int i = 0; i < successList.size(); i++) { User user = new User(); ps.setInt(1,successList.get(i).getId()); ps.setString(2,successList.get(i).getName()); ps.setString(3,successList.get(i).getPhoneNum()); ps.setString(4,successList.get(i).getAddress()); //將一組參數添加到此 PreparedStatement 對象的批處理命令中。 ps.addBatch(); } //執行批處理 ps.executeBatch(); //手動提交事務 conn.commit(); } catch (Exception e) { e.printStackTrace(); } finally { //記得關閉連接 JdbcConnectUtil.close(conn,ps); } }

這裏我們通過PreparedStatement的addBatch()和executeBatch()實現JDBC的分批插入,然後用import4Jdbc()替換原來的savaData()即可。

經過多次導入測試,這種方案的平均耗時爲140秒。相比之前的500秒確實有了大幅度提升,但是2分多鍾仍然感覺有點慢。

4.2 多線程+Mybatis-Plus批量插入

我們知道Mybatis-Plus的IService中提供了saveBatch的批量插入方法,但經過查看日志發現Mybatis-Plus的saveBatch在最後還是循環調用的INSERT INTO語句!

這種情況下,測試多線程速度和單線程相差不大,所以需要實現真正的批量插入語句,兩種方式,一種是通過給Mybatis-Plus注入器,增強批量插入,一種是在xml文件中自己拼接SQL語句,我們在這裏選用後一種,因爲我們只做一個表,直接手寫xml很方便,如果是在企業開發時建議使用sql注入器實現(自定義SQL注入器實現DefaultSqlInjector,添加InsertBatchSomeColumn方法,通過使用InsertBatchSomeColumn方法批量插入。)。

【XML中手動批量插入】

<insert id="insertSelective" parameterType="java.util.List"> insert into user (id,name, phone_num, address ) values <foreach collection="list" item="item" separator=","> (#{item.id},#{item.name},#{item.phoneNum},#{item.address}) </foreach> </insert>

在在EasyExcelImportHandler類中的saveData()方法中實現多線程批量插入。

/** * 采用多線程讀取數據 */ private void saveData() { List<List<User>> lists = ListUtil.split(successList, 1000); CountDownLatch countDownLatch = new CountDownLatch(lists.size()); for (List<User> list : lists) { threadPoolExecutor.execute(() -> { try { userMapper.insertSelective(list.stream().map(o -> { User user = new User(); user.setName(o.getName()); user.setId(o.getId()); user.setPhoneNum(o.getPhoneNum()); user.setAddress(o.getAddress()); return user; }).collect(Collectors.toList())); } catch (Exception e) { log.error("啓動線程失敗,e:{}", e.getMessage(), e); } finally { //執行完一個線程減1,直到執行完 countDownLatch.countDown(); } }); } // 等待所有線程執行完 try { countDownLatch.await(); } catch (Exception e) { log.error("等待所有線程執行完異常,e:{}", e.getMessage(), e); } // 提前將不再使用的集合清空,釋放資源 successList.clear(); lists.clear(); }

經過多次導入測試,100萬數據量導入耗時平均在20秒,這就是一個很客觀且友好用戶的導入功能啦,畢竟100萬的xlsx文件,打開都需要七八秒呢!

五、總結

OK!以上就是SpringBoot項目下,通過阿裏開源的EasyExcel技術進行百萬級數據的導入功能的優化步驟啦,由原來的500秒優化到20秒!

來源:https://www.cnblogs.com/JavaBuild/p/18187977

作者;JavaBuild

0 阅读:1

架構的小事

簡介:感謝大家的關注