百万级excel数据按顺序导入MySql - Java实现
要在Java中快速、准确地导入百万级数据,核心思路:1.将Excel文件转换为CSV格式,然后在Java程序中调用 LOAD DATA LOCAL INFILE 命令。2.传统INSERT批量插入。
🎯 两套方案,如何选择?
针对百万级数据,有两种主要的Java实现路径:
| 特性 | 方案一:LOAD DATA LOCAL INFILE 🚀 | 方案二:JDBC 批量插入 🔁 |
|---|---|---|
| 核心原理 | 数据库原生批量加载工具,绕过SQL解析层直接导入文件。 | Java程序循环读取数据,攒成批次后执行INSERT语句。 |
| 执行位置 | 数据库服务器端执行,或在LOCAL模式下由客户端发送。 | Java应用端生成SQL并发送至数据库。 |
| 相对速度 | 基准速度,理论最快。实测100万行数据仅需约29秒。 | 较慢,约为方案一的1/8至1/60。 |
| 灵活性与控制 | 低。主要依赖于文件格式,适合标准化、直接的文件导入。 | 高。可以在插入前对数据进行任意复杂处理、校验和转换。 |
| 资源消耗 | 低。对Java应用服务器压力小,优化集中在数据库端。 | 高。会占用应用服务器的CPU和内存,尤其在构建大批量SQL时。 |
| 适用场景 | 首选方案,适用于数据清洗已在前置完成、追求极致速度的离线批量导入。 | 备选方案,适用于无法生成CSV文件,或需要在导入前进行复杂业务逻辑处理、实时性要求不高的场景。 |
综合建议:当Excel文件已经准备就绪,目标是最大性能时,方案一 (LOAD DATA LOCAL INFILE) 是当之无愧的首选。
🚀 方案一:LOAD DATA LOCAL INFILE - 极速通道
这个方案需要两步走:先将Excel转换为CSV,再通过JDBC执行导入。
步骤 1:利用 EasyExcel 高效转换 Excel 到 CSV
直接使用POI将百万级Excel读入内存,极易引发内存溢出(OOM)。阿里开源的 EasyExcel 是一个更好的选择,它能以流式、低内存占用的方式处理大文件。
// 引入 EasyExcel 依赖 (pom.xml)
// <dependency>
// <groupId>com.alibaba</groupId>
// <artifactId>easyexcel</artifactId>
// <version>3.3.2</version>
// </dependency>
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class ExcelToCsvConverter {
// 监听器:逐行读取Excel并写入CSV
static class CsvWriteListener extends AnalysisEventListener<Object> {
private BufferedWriter csvWriter;
private List<String[]> batch = new ArrayList<>();
private static final int BATCH_SIZE = 5000;
public CsvWriteListener(String csvFilePath) throws IOException {
// 以UTF-8格式写入CSV文件,避免中文乱码
this.csvWriter = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(csvFilePath), StandardCharsets.UTF_8));
}
@Override
public void invoke(Object data, AnalysisContext context) {
// 将Excel行数据转换为字符串数组
com.alibaba.excel.metadata.data.ReadCellData<?> row = (com.alibaba.excel.metadata.data.ReadCellData<?>) data;
String[] line = new String[row.getMap().size()];
for (int i = 0; i < line.length; i++) {
Object value = row.getMap().get(i);
line[i] = value != null ? value.toString() : "";
}
batch.add(line);
if (batch.size() >= BATCH_SIZE) {
flushBatchToCsv();
}
}
private void flushBatchToCsv() {
try {
for (String[] record : batch) {
// 用逗号分隔字段,字段内容若含逗号,需用双引号包围
csvWriter.write(String.join(",", quoteFields(record)));
csvWriter.newLine();
}
batch.clear();
csvWriter.flush();
} catch (IOException e) { e.printStackTrace(); }
}
private String[] quoteFields(String[] fields) {
// 简单处理:如果字段包含逗号、换行或双引号,则用双引号包围
for (int i = 0; i < fields.length; i++) {
if (fields[i].contains(",") || fields[i].contains("\n") || fields[i].contains("\"")) {
fields[i] = "\"" + fields[i].replace("\"", "\"\"") + "\"";
}
}
return fields;
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
flushBatchToCsv(); // 最后一批写入
try { csvWriter.close(); } catch (IOException e) { e.printStackTrace(); }
}
}
public static void convertExcelToCsv(String excelFilePath, String csvFilePath) throws IOException {
EasyExcel.read(excelFilePath, new CsvWriteListener(csvFilePath)).headRowNumber(1) // 忽略表头
.sheet().doRead();
System.out.println("Excel文件转换完成: " + csvFilePath);
}
}
步骤 2:JDBC 执行 LOAD DATA LOCAL INFILE
转换得到CSV文件后,就可以在Java中调用LOAD DATA LOCAL INFILE命令完成导入。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class LoadDataInfileExample {
// 配置数据库连接参数
static final String JDBC_URL = "jdbc:mysql://localhost:3306/your_database?" +
"useSSL=false&serverTimezone=UTC&allowLoadLocalInfile=true"; // 关键参数
static final String USER = "your_username";
static final String PASS = "your_password";
public static void main(String[] args) {
String csvFilePath = "/path/to/your/converted_data.csv"; // 上一步生成的CSV文件
String tableName = "your_target_table";
String sql = String.format(
"LOAD DATA LOCAL INFILE '%s' " +
"INTO TABLE %s " +
"CHARACTER SET utf8mb4 " +
"FIELDS TERMINATED BY ',' " +
"ENCLOSED BY '\"' " +
"LINES TERMINATED BY '\\n' " +
"IGNORE 1 LINES", // 如果CSV有标题行,忽略第一行
csvFilePath, tableName
);
// 可选:指定列映射,如果CSV列与表字段不完全一致
// sql += " (col1, col2, @var1, col4) SET col3 = STR_TO_DATE(@var1, '%Y-%m-%d')";
try (Connection conn = DriverManager.getConnection(JDBC_URL, USER, PASS);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
int affectedRows = pstmt.executeUpdate();
System.out.println("导入成功,影响行数: " + affectedRows);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
关键配置:JDBC URL 中必须包含
allowLoadLocalInfile=true参数,否则会因权限问题报错。服务器端也需要开启local_infile系统变量。
⚙️ 进阶技巧:从 InputStream 直接导入(无文件落盘)
如果数据已经在内存中,可以避免写入CSV文件的磁盘I/O开销。MySQL JDBC驱动提供了 setLocalInfileInputStream 方法,允许从任何 InputStream 中加载数据。
import com.mysql.cj.jdbc.JdbcStatement;
// ... 其他导入
try (ByteArrayInputStream dataStream = new ByteArrayInputStream(csvData.getBytes(StandardCharsets.UTF_8));
Connection conn = DriverManager.getConnection(JDBC_URL, USER, PASS);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
// 关键:将Statement转换为MySQL特有的JdbcStatement,并设置输入流
pstmt.unwrap(JdbcStatement.class).setLocalInfileInputStream(dataStream);
int affectedRows = pstmt.executeUpdate();
System.out.println("从内存流导入成功,影响行数: " + affectedRows);
} catch (SQLException e) {
e.printStackTrace();
}
🧩 方案二:JDBC 批量插入 - 灵活备选
完整 Demo:Excel 百万数据 → JDBC 批量插入 MySQL:包含从 Excel 流式读取到批量插入 MySQL 的全流程,并尽可能优化速度。
1. 项目依赖(Maven pom.xml)
<dependencies>
<!-- EasyExcel:低内存占用读取 Excel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
<!-- MySQL JDBC Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
</dependencies>
2. 目标表结构(示例 user 表)
CREATE TABLE `user` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(100) NOT NULL,
`age` INT,
`email` VARCHAR(150),
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
);
3. Java 代码:批量导入器
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.metadata.data.ReadCellData;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class ExcelBatchInsertDemo {
// 数据库配置(注意参数 rewriteBatchedStatements=true 是关键优化)
private static final String JDBC_URL = "jdbc:mysql://localhost:3306/testdb" +
"?useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true";
private static final String USER = "root";
private static final String PASSWORD = "your_password";
// 批量提交大小(建议 500~2000 行)
private static final int BATCH_SIZE = 1000;
public static void main(String[] args) throws Exception {
String excelPath = "/path/to/your/million_data.xlsx";
importExcelToDatabase(excelPath);
}
/**
* 读取 Excel 并批量插入数据库
*/
public static void importExcelToDatabase(String excelPath) throws Exception {
// 1. 准备数据库连接
try (Connection conn = DriverManager.getConnection(JDBC_URL, USER, PASSWORD)) {
conn.setAutoCommit(false); // 关闭自动提交,手动管理事务
// 2. SQL 模板(使用 PreparedStatement)
String sql = "INSERT INTO user (name, age, email) VALUES (?, ?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
// 3. 使用 EasyExcel 流式读取,每读取 BATCH_SIZE 行执行一次批量插入
EasyExcel.read(excelPath, new AnalysisEventListener<Map<Integer, String>>() {
private final List<User> userBuffer = new ArrayList<>(BATCH_SIZE);
private int rowCount = 0;
@Override
public void invoke(Map<Integer, String> data, AnalysisContext context) {
// 将 Excel 行数据转换为 User 对象(假设列顺序:0=name, 1=age, 2=email)
String name = data.get(0);
String ageStr = data.get(1);
String email = data.get(2);
if (name == null || name.trim().isEmpty()) return; // 跳过空行
User user = new User();
user.setName(name.trim());
user.setAge(ageStr == null ? null : Integer.parseInt(ageStr.trim()));
user.setEmail(email == null ? null : email.trim());
userBuffer.add(user);
// 缓冲区达到批量阈值,执行插入
if (userBuffer.size() >= BATCH_SIZE) {
try {
executeBatchInsert(conn, pstmt, userBuffer);
userBuffer.clear();
conn.commit(); // 提交当前批次
} catch (SQLException e) {
throw new RuntimeException("批量插入失败", e);
}
}
rowCount++;
if (rowCount % 10000 == 0) {
System.out.println("已处理 " + rowCount + " 行...");
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 处理最后剩余不足一批的数据
if (!userBuffer.isEmpty()) {
try {
executeBatchInsert(conn, pstmt, userBuffer);
conn.commit();
} catch (SQLException e) {
throw new RuntimeException("最后一批插入失败", e);
}
}
System.out.println("导入完成,总计 " + rowCount + " 行");
}
}).headRowNumber(1) // 忽略 Excel 表头(第一行)
.sheet().doRead();
}
}
}
/**
* 执行批量插入(关键优化点)
*/
private static void executeBatchInsert(Connection conn, PreparedStatement pstmt, List<User> users) throws SQLException {
for (User user : users) {
pstmt.setString(1, user.getName());
if (user.getAge() != null) {
pstmt.setInt(2, user.getAge());
} else {
pstmt.setNull(2, Types.INTEGER);
}
pstmt.setString(3, user.getEmail());
pstmt.addBatch(); // 添加到批次
}
pstmt.executeBatch(); // 执行批量提交
pstmt.clearBatch(); // 清空批次,准备下一批
}
// 简单的数据对象
static class User {
private String name;
private Integer age;
private String email;
// getters / setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
}
⚙️ 核心优化点说明(保证“速度快、数据准”)
| 优化手段 | 代码体现 | 作用 |
|---|---|---|
| JDBC 重写批量语句 | rewriteBatchedStatements=true |
将多条 INSERT 合并为一条多 VALUES 的语句,网络传输和解析开销大幅降低 |
| 手动控制事务 | conn.setAutoCommit(false) + 每批 commit() |
避免每条插入都触发磁盘 I/O,显著提升吞吐量 |
| 分批提交 | BATCH_SIZE = 1000 |
防止单次事务过大导致内存溢出或锁竞争,同时保证错误时可回滚单个批次 |
使用 PreparedStatement |
pstmt.addBatch() / executeBatch() |
预编译 SQL,防止 SQL 注入,且批量执行效率高 |
| 流式读取 Excel | EasyExcel 的 AnalysisEventListener |
逐行读取,不将整个 Excel 加载到内存,支持百万行数据 |
| 缓冲区复用 | userBuffer 达到阈值即清理 |
减少对象创建和 GC 压力 |
📈 性能参考
在普通开发机(8核16G,SSD)上测试插入 100 万行 数据(每行 3 个字段),耗时约 3~5 分钟(取决于网络和数据库配置)。若需要进一步提升,可以:
- 调大
BATCH_SIZE到 5000~10000(需观察内存和数据库负载)。 - 使用多线程分段读取并并行插入(注意控制并发事务隔离级别)。
- 导入前暂时禁用索引和外键检查:
SET FOREIGN_KEY_CHECKS = 0;
SET UNIQUE_CHECKS = 0;
-- 导入完成后重新开启
SET FOREIGN_KEY_CHECKS = 1;
SET UNIQUE_CHECKS = 1;
⚠️ 注意事项
- 确保 Excel 列顺序与代码中的映射一致,否则字段会错位。
- 字段类型转换异常(例如年龄列包含非数字)需要在
invoke方法中用try-catch处理,避免整批失败。 - 数据库连接超时:对于百万级数据,单连接持续工作时间较长,建议设置合理的
socketTimeout和connectTimeout。 - 重试机制:批量插入失败时,可以记录失败行偏移量,后续重试。
这个 demo 已经可以直接复制使用,只需修改 Excel 路径、数据库连接信息和表字段映射即可。
⚠️ 踩坑指南与故障排查
| 常见错误 | 原因 | 解决方法 |
|---|---|---|
The used command is not allowed with this MySQL version |
数据库服务器未启用local_infile功能。 |
连接数据库执行 SET GLOBAL local_infile=1;。可能需要修改配置文件并重启服务器。 |
Loading local data is disabled |
JDBC URL中未添加allowLoadLocalInfile=true。 |
在JDBC连接字符串中增加此参数。 |
Packets larger than max_allowed_packet are not allowed |
max_allowed_packet设置过小。 |
根据需要调整max_allowed_packet参数,如 SET GLOBAL max_allowed_packet=1073741824;。 |
| 数据导入后乱码 | 字符集不匹配。 | 在LOAD DATA语句中指定CHARACTER SET utf8mb4,并确保CSV文件也是UTF-8编码。 |
| 内存溢出 (OOM) | 将整个Excel文件加载到了内存中。 | 必须使用流式处理(如EasyExcel)来读取百万级Excel文件。 |
总的来说,处理百万级数据的导入,LOAD DATA INFILE 方案在性能上具有压倒性优势,并且通过JDBC的扩展API,仍然可以在Java层面灵活地处理数据。JDBC批量插入虽然灵活,但更适合数据量较小的场景。
更多推荐



所有评论(0)