最近在开发一个用户数据迁移功能时,遇到了需要快速插入大量数据的需求。经过反复测试和优化,最终实现了13秒插入30万条数据的性能表现。本文将详细分享从最初4小时的单条插入,到最终批量插入优化的完整过程,包含MyBatis和JDBC两种实现方式的具体代码和调优经验。
在实际项目中,我们经常会遇到需要批量导入数据的场景:
这些场景的共同特点是需要高效地插入大量数据,传统的单条插入方式性能极差,必须采用批量处理的方式。
我们主要对比了两种实现方案:
MyBatis批量插入
JDBC批量插入
最终我们两种方案都实现了,根据实际场景选择使用。
我们使用以下简单的用户表作为测试表:
sql复制CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`username` varchar(64) DEFAULT NULL COMMENT '用户名称',
`age` int(4) DEFAULT NULL COMMENT '年龄',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户信息表';
对于批量插入场景,建议:
ALTER TABLE ... DISABLE KEYS和ALTER TABLE ... ENABLE KEYS命令这样可以显著减少插入时的索引维护开销。
首先配置MyBatis环境和Mapper:
java复制@Data
public class User {
private int id;
private String username;
private int age;
}
java复制public interface UserMapper {
void batchInsertUser(@Param("list") List<User> userList);
}
xml复制<insert id="batchInsertUser" parameterType="java.util.List">
insert into t_user(username,age) values
<foreach collection="list" item="item" index="index" separator=",">
(#{item.username}, #{item.age})
</foreach>
</insert>
经过多次测试,我们总结出最优的批量插入方案:
java复制@Test
public void testBatchInsertUser() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession session = sqlSessionFactory.openSession();
System.out.println("===== 开始插入数据 =====");
long startTime = System.currentTimeMillis();
try {
List<User> userList = new ArrayList<>();
for (int i = 1; i <= 300000; i++) {
User user = new User();
user.setId(i);
user.setUsername("用户_" + i);
user.setAge((int) (Math.random() * 100));
userList.add(user);
if (i % 5000 == 0) {
session.insert("batchInsertUser", userList);
session.commit();
userList.clear();
}
}
if(!userList.isEmpty()) {
session.insert("batchInsertUser", userList);
session.commit();
}
long spendTime = System.currentTimeMillis()-startTime;
System.out.println("成功插入30万条数据,耗时:"+spendTime+"毫秒");
} finally {
session.close();
}
}
关键优化点:
我们测试了不同批处理大小的性能表现:
| 批处理大小 | 耗时(秒) | 内存占用 |
|---|---|---|
| 单条插入 | 14909 | 低 |
| 1000条 | 50 | 中 |
| 5000条 | 13 | 较高 |
| 10000条 | 12 | 高 |
从测试结果看,5000条是一个较好的平衡点。
java复制@Test
public void testJDBCBatchInsertUser() throws SQLException {
Connection connection = null;
PreparedStatement preparedStatement = null;
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "root";
try {
connection = DriverManager.getConnection(url, user, password);
connection.setAutoCommit(false);
System.out.println("===== 开始插入数据 =====");
long startTime = System.currentTimeMillis();
String sql = "INSERT INTO t_user (username, age) VALUES (?, ?)";
preparedStatement = connection.prepareStatement(sql);
Random random = new Random();
for (int i = 1; i <= 300000; i++) {
preparedStatement.setString(1, "用户_" + i);
preparedStatement.setInt(2, random.nextInt(100));
preparedStatement.addBatch();
if (i % 5000 == 0) {
preparedStatement.executeBatch();
connection.commit();
System.out.println("已插入:" + i + "条");
}
}
preparedStatement.executeBatch();
connection.commit();
long spendTime = System.currentTimeMillis()-startTime;
System.out.println("成功插入30万条数据,耗时:"+spendTime+"毫秒");
} finally {
if (preparedStatement != null) preparedStatement.close();
if (connection != null) connection.close();
}
}
addBatch()和executeBatch()方法max_allowed_packet参数现象:批处理过程中出现OOM错误
解决方案:
现象:执行过程中出现超时错误
解决方案:
现象:相同代码在不同时段执行时间差异大
解决方案:
对于超大规模数据,可以考虑使用多线程并行插入:
java复制ExecutorService executor = Executors.newFixedThreadPool(4);
List<Future<?>> futures = new ArrayList<>();
int total = 300000;
int batchSize = 5000;
int threads = 4;
int perThread = total / threads;
for (int t = 0; t < threads; t++) {
final int start = t * perThread;
final int end = (t == threads - 1) ? total : start + perThread;
futures.add(executor.submit(() -> {
// 每个线程执行自己的批量插入逻辑
}));
}
// 等待所有线程完成
for (Future<?> future : futures) {
future.get();
}
executor.shutdown();
注意事项:
针对MySQL的特别优化:
LOAD DATA INFILE命令innodb_buffer_pool_sizeinnodb_log_file_sizeINSERT DELAYED(MyISAM引擎)根据我们的实践经验,给出以下建议:
在最近的一个实际项目中,我们使用5000条的批处理大小,配合多线程插入,成功在5分钟内完成了1000万条数据的迁移工作,服务器资源使用平稳,没有出现明显的性能问题。