1. 背景与需求分析
最近在数据迁移项目中遇到了一个典型问题:需要将MySQL中的200多万条数据高效导入到Apache Doris分析型数据库中。Doris作为一款MPP架构的分析型数据库,虽然语法兼容MySQL,但在大数据量导入场景下,直接使用INSERT语句效率极低。
最初尝试了两种常见方法:
- 直接执行原始INSERT语句:耗时数小时仍未完成
- 改用批量INSERT语句:性能虽有提升但仍不理想
这两种方法的主要瓶颈在于:
- 每条INSERT语句都需要经过完整的SQL解析、优化和执行过程
- 网络往返开销大,特别是跨机房传输时
- 事务日志写入成为性能瓶颈
2. 技术方案选型
经过调研,Doris对CSV格式的批量导入支持最好,其Stream Load功能可以:
- 绕过SQL解析层直接加载数据
- 支持高达100MB/s的单节点导入速度
- 自动并行化处理
但直接导出CSV会遇到两个关键问题:
- 字段内容中的分隔符会导致列错位
- 特殊字符(如换行符)会破坏CSV格式
因此需要开发一个转换工具,能够:
- 正确解析INSERT语句中的字段值
- 处理各种边界情况(NULL值、特殊字符等)
- 生成符合RFC4180标准的CSV文件
3. 核心实现解析
3.1 工具架构设计
开发了一个Java实现的SQL转CSV工具,主要处理流程如下:
- 输入处理:逐行读取SQL文件,过滤非INSERT语句
- 元数据提取:从第一条INSERT语句提取表字段名
- 值解析:使用状态机解析VALUES部分
- 格式转换:将解析结果转换为CSV格式
- 输出写入:生成标准CSV文件
3.2 关键算法实现
3.2.1 VALUES解析状态机
java复制private static List<String> parseSimple(String valuesStr) {
List<String> values = new ArrayList<>();
StringBuilder current = new StringBuilder();
boolean inQuotes = false;
char quoteChar = '\0';
for (int i = 0; i < valuesStr.length(); i++) {
char c = valuesStr.charAt(i);
if (!inQuotes) {
if (c == '\'' || c == '"') {
inQuotes = true;
quoteChar = c;
current.append(c);
} else if (c == ',') {
values.add(cleanValue(current.toString().trim()));
current.setLength(0);
} else {
current.append(c);
}
} else {
current.append(c);
if (c == quoteChar) {
if (i+1 < valuesStr.length() && valuesStr.charAt(i+1) == quoteChar) {
current.append(quoteChar);
i++;
} else {
inQuotes = false;
}
}
}
}
if (current.length() > 0) {
values.add(cleanValue(current.toString().trim()));
}
return values;
}
该算法特点:
- 正确处理嵌套引号(如
'O''Reilly') - 自动跳过转义字符
- 保留原始值的语义
3.2.2 CSV转义处理
java复制private static String escapeCsv(String value) {
if (value == null) return "";
StringBuilder result = new StringBuilder();
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (c == '\\') {
result.append("\\\\");
} else if (c == '\'') {
result.append("\\'");
} else {
result.append(c);
}
}
return "'" + result.toString() + "'";
}
处理规则:
- 字符串统一用单引号包裹
- 内部单引号转义为'
- 反斜杠转义为\
3.3 异常处理机制
工具实现了完善的错误处理:
- 列数校验:确保每条记录的字段数与表头一致
- 语法错误检测:捕获并记录解析失败的SQL
- 统计报告:输出成功/失败记录数
java复制if (values.size() != headers.size()) {
System.out.println("第 " + lineNum + " 行: 期望 " +
headers.size() + " 列,实际 " + values.size() + " 列");
errorRows++;
continue;
}
4. 性能优化实践
4.1 内存管理策略
- 采用分批处理机制,每10000行写入一次磁盘
- 使用StringBuilder减少字符串操作开销
- 文件流保持打开状态,避免频繁IO操作
4.2 实际测试数据
| 数据量 | 直接INSERT | 批量INSERT | CSV导入 |
|---|---|---|---|
| 10万行 | 25分钟 | 8分钟 | 15秒 |
| 100万行 | 4小时+ | 45分钟 | 2分钟 |
| 250万行 | 未完成 | 2小时+ | 10分钟 |
5. 完整使用指南
5.1 环境准备
- 安装Java 8+运行环境
- 准备MySQL的SQL导出文件
- 确保有足够的磁盘空间(CSV文件约为SQL文件的60%-70%大小)
5.2 执行步骤
- 编译工具:
bash复制javac InsertSqlToCSV.java
- 运行转换:
bash复制java InsertSqlToCSV input.sql output.csv
- 导入Doris:
bash复制curl --location-trusted -u user:passwd \
-H "column_separator:," \
-H "columns:col1,col2,..." \
-T output.csv \
http://doris_fe:8030/api/db/tbl/_stream_load
5.3 参数调优建议
- Doris端配置:
properties复制stream_load_default_timeout_second=3600
max_stream_load_buffer_size=1073741824
- 网络优化:
- 尽量在相同机房执行导入
- 使用千兆以上网络带宽
6. 常见问题解决方案
6.1 字段数不匹配
现象:报错"column count mismatch"
解决:
- 检查SQL文件中INSERT语句的字段数是否一致
- 确认是否有未转义的分隔符导致CSV解析错误
6.2 特殊字符处理
现象:导入后数据截断或错位
解决:
- 在工具中增加对换行符的转义处理
- 对于二进制数据,建议先Base64编码
6.3 性能优化
现象:导入速度低于预期
解决:
- 调整Doris的并行度参数
- 将大文件拆分为多个100MB左右的小文件并行导入
7. 进阶技巧
- 增量同步:通过WHERE条件筛选增量数据
- 定时任务:结合crontab实现定期同步
- 字段映射:修改工具支持字段名转换
- 数据类型处理:增强对日期、二进制等特殊类型的支持
这个方案已经在生产环境稳定运行,处理了超过10亿条记录的迁移任务。对于需要更高性能的场景,还可以考虑使用Doris的Spark Load或Routine Load功能。