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"};
-
-
-
-
-
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实现方式。