天天看點

MySQL優化之LOAD DATA LOCAL INFILE實作大批量插入

如今mysql普遍的插入方式有如下兩種:

1、循環單條插入

<insert id="insert" parameterType="com.chargeProject.consumer.entity.Test">
    insert into test (id, nums, name)
    values (#{id,jdbcType=INTEGER}, #{nums,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR})
  </insert>
           

2、拼裝批量插入

<insert id="batchInsert" parameterType="java.util.List">
    insert into test (id, nums, name)
    values
    <foreach collection="list" item="item" separator=",">
      (#{item.id,jdbcType=INTEGER}, #{item.nums,jdbcType=INTEGER}, #{item.name,jdbcType=VARCHAR})
    </foreach>
</insert>
           

一般都是通過mybatis架構進行輔助實作的,當然也可以自動拼裝。今天介紹的是mysql自帶的一種批量插入方式且效率更高,通過LOAD DATA LOCAL INFILE實作大批量插入。

MySQL使用LOAD DATA LOCAL INFILE從檔案中導入資料比insert語句要快,MySQL文檔上說要快20倍左右。

但是這個方法有個缺點,就是導入資料之前,必須要有檔案,也就是說從檔案中導入。這樣就需要去寫檔案,以及檔案删除等維護。某些情況下,比如資料源并發的話,還會出現寫檔案并發問題,很難處理。

那麼有沒有什麼辦法,可以達到同樣的效率,直接從記憶體(IO流中)中導入資料,而不需要寫檔案呢?

MySQL社群提供這樣一個方法:setLocalInfileInputStream(),此方法位于com.mysql.jdbc.PreparedStatement 類中。通過使用 MySQL JDBC 的setLocalInfileInputStream 方法實作從Java InputStream中load data local infile 到MySQL資料庫中。

代碼如下:

@Component
public class LoadDataInFileUtil {

    private Logger logger = LoggerFactory.getLogger(LoadDataInFileUtil.class);
    private Connection conn = null;
    @Resource
    private JdbcTemplate jdbcTemplate;

    /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --*/

    /**
     * 将資料從輸入流加載到MySQL。
     *
     * @param loadDataSql  SQL語句。
     * @param dataStream   輸入流。
     * @param jdbcTemplate JDBC。
     * @return int         成功插入的行數。
     */
    private int bulkLoadFromInputStream(String loadDataSql,
                                        InputStream dataStream,
                                        JdbcTemplate jdbcTemplate) throws SQLException {
        if (null == dataStream) {
            logger.info("輸入流為NULL,沒有資料導入。");
            return 0;
        }
        conn = jdbcTemplate.getDataSource().getConnection();
        PreparedStatement statement = conn.prepareStatement(loadDataSql);
        int result = 0;
        if (statement.isWrapperFor(com.mysql.jdbc.Statement.class)) {
            com.mysql.jdbc.PreparedStatement mysqlStatement = statement.unwrap(com.mysql.jdbc.PreparedStatement.class);
            mysqlStatement.setLocalInfileInputStream(dataStream);
            result = mysqlStatement.executeUpdate();
        }
        return result;
    }

    /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --*/

    /**
     * 組裝 SQL 語句。
     *
     * @param dataBaseName 資料庫名。
     * @param tableName    表名。
     * @param columnName   要插入資料的列名。
     */
    public String assembleSql(String dataBaseName, String tableName, String columnName[]) {
        String insertColumnName = StringUtils.join(columnName, ",");
        String sql = "LOAD DATA LOCAL INFILE 'sql.csv' IGNORE INTO TABLE " + dataBaseName + "." + tableName + "(" + insertColumnName + ")";
        return sql;
    }

    /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --*/

    /**
     * 往 StringBuilder 裡追加資料。
     *
     * @param builder StringBuilder。
     * @param object  資料。
     */
    public void builderAppend(StringBuilder builder, Object object) {
        builder.append(object);
        builder.append("\t");
    }

    /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --*/

    /**
     * 往 StringBuilder 裡追加一條資料的最後一個字段。
     *
     * @param builder StringBuilder。
     * @param object  資料。
     */
    public void builderEnd(StringBuilder builder, Object object) {
        builder.append(object);
        builder.append("\n");
    }

    /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --*/

    /**
     * 通過 LOAD DATA LOCAL INFILE 大批量導入資料到 MySQL。
     *
     * @param sql     SQL語句。
     * @param builder 組裝好的資料。
     */
    public int fastInsertData(String sql, StringBuilder builder) {
        int rows = 0;
        InputStream is = null;
        try {
            byte[] bytes = builder.toString().getBytes();
            if (bytes.length > 0) {
                is = new ByteArrayInputStream(bytes);
                //批量插入資料。
                long beginTime = System.currentTimeMillis();
                rows = bulkLoadFromInputStream(sql, is, jdbcTemplate);
                long endTime = System.currentTimeMillis();
                logger.info("LOAD DATA LOCAL INFILE :【插入" + rows + "行資料至MySql中,耗時" + (endTime - beginTime) + "ms。】");
            }

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != is) {
                    is.close();
                }
                if (null != conn) {
                    conn.close();
                }
            } catch (IOException | SQLException e) {
                e.printStackTrace();
            }
        }
        return rows;
    }

    /*-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --*/

}
           

調試代碼如下:

// 資料庫名。
    public static final String DATA_BASE_NAME = "charge";
    // 表名。
    public static final String TABLE_NAME = "test";
    // 要插入資料的列名。(必須與插入的資料一一對應)
    public static final String COLUMN_NAME[] = {"id", "nums", "name"};



    @Override
    public ResultContent insert(String name) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        StringBuilder sb = new StringBuilder();
        List<Test> list = new ArrayList<>();
        for(int i = 1; i < 100000; i++) {
            loadDataInFileUtil.builderAppend(sb, UUID.randomUUID().toString());
            loadDataInFileUtil.builderAppend(sb, i);
            loadDataInFileUtil.builderEnd(sb, name + i);
        }


        String sql = loadDataInFileUtil.assembleSql(DATA_BASE_NAME, TABLE_NAME, COLUMN_NAME);
        int insertRow = loadDataInFileUtil.fastInsertData(sql, sb);
        System.out.println("insert應收報表數量insertRow:"+insertRow);


        stopWatch.stop();
        System.out.println("花費時間" + stopWatch.getTotalTimeSeconds());



        System.out.println("---------方法執行結束--------------");
        return new ResultContent(0, "success", name);
    }
           

經過測試插入1w條資料時候與拼裝批量插入語句時間差别不大,當插入數量達到10w出現了明顯的時間差:

拼裝批量插入語句花費時間:6.83s

LOAD DATA LOCAL INFILE實作大批量插入花費時間:1.23s

當表格的字段更多資料量更大出現的時間差就越大。

總結:當需要進行大批量資料插入的時候,可以優先考慮LOAD DATA LOCAL INFILE實作方式。

繼續閱讀