在日常开发中,我们经常遇到需要将大量数据快速导入MySQL数据库的场景。比如电商平台的订单批量导入、物联网设备的海量传感器数据存储、金融系统的交易记录迁移等。传统的单条INSERT语句执行效率极低,实测插入1万条数据可能需要几十秒甚至几分钟。
我曾在物流系统中遇到过这样的问题:每天需要处理超过50万条运单数据。最初使用常规的ADO.NET逐条插入,不仅耗时长达2小时,还经常导致数据库连接池耗尽。后来改用MySqlBulkCopy后,同样的数据量处理时间缩短到20秒以内,性能提升超过300倍!
MySqlBulkCopy的核心优势在于:
首先需要通过NuGet安装最新版的MySQL Connector/NET。我推荐使用8.0以上版本,因为它在性能和稳定性方面都有显著提升。在Visual Studio的包管理器控制台中执行:
bash复制Install-Package MySqlConnector -Version 2.2.6
注意:MySqlConnector是官方推荐的替代品,比传统的MySQL.Data性能更好,且完全兼容MySqlBulkCopy。
很多开发者容易忽略连接字符串的配置,这里有几个关键参数必须设置:
csharp复制string connectionString = "server=localhost;port=3306;user=root;password=your_pwd;database=test_db;
AllowLoadLocalInfile=true;
DefaultCommandTimeout=600;
ConnectionTimeout=30;
SslMode=Preferred";
让我们从一个完整的示例开始。假设我们要导入一个包含产品信息的DataTable:
csharp复制public void BulkInsertProducts(DataTable products)
{
using (var connection = new MySqlConnection(connectionString))
{
connection.Open();
using (var bulkCopy = new MySqlBulkCopy(connection))
{
bulkCopy.DestinationTableName = "products";
bulkCopy.BulkCopyTimeout = 600; // 10分钟超时
// 自动映射同名字段
foreach(DataColumn col in products.Columns)
{
bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);
}
// 执行批量插入
var result = bulkCopy.WriteToServer(products);
Console.WriteLine($"插入 {result.RowsInserted} 行, 耗时 {result.Elapsed}");
}
}
}
批量操作应该放在事务中以保证原子性:
csharp复制using (var transaction = connection.BeginTransaction())
{
try
{
bulkCopy.WriteToServer(data);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
对于超大数据集(如超过100万行),建议分批次处理:
csharp复制int batchSize = 50000;
for (int i = 0; i < data.Rows.Count; i += batchSize)
{
var batchData = data.AsEnumerable()
.Skip(i)
.Take(batchSize)
.CopyToDataTable();
bulkCopy.WriteToServer(batchData);
}
MySQL服务端的这些参数会显著影响导入性能:
ini复制# my.cnf 配置
[mysqld]
local_infile=1
innodb_buffer_pool_size=4G
innodb_log_file_size=1G
innodb_flush_log_at_trx_commit=0
bulk_insert_buffer_size=256M
修改后需要重启MySQL服务:
bash复制sudo systemctl restart mysql
实测对比(插入100万条记录):
| 优化措施 | 耗时(秒) | 提升幅度 |
|---|---|---|
| 无优化 | 120.5 | - |
| 批处理(5万/批) | 45.2 | 62.5% |
| 禁用索引 | 28.7 | 76.2% |
| 服务器调优 | 18.3 | 84.8% |
| 综合优化 | 12.6 | 89.5% |
如果遇到"Loading local data is disabled"错误,需要:
sql复制SET GLOBAL local_infile=1;
常见的数据类型转换问题:
建议在ColumnMappings中显式指定:
csharp复制bulkCopy.ColumnMappings.Add("Price", "price DECIMAL(10,2)");
对于大数据量导入,需要设置足够的超时时间:
csharp复制bulkCopy.BulkCopyTimeout = 0; // 0表示无超时限制
connection.ConnectionTimeout = 300;
经过多个项目的实战验证,我总结出这些经验:
一个健壮的生产级实现应该包含:
csharp复制public BulkInsertResult SafeBulkInsert(DataTable data, int maxRetries = 3)
{
int retryCount = 0;
while (retryCount < maxRetries)
{
try
{
// 实现代码...
return new BulkInsertResult { IsSuccess = true };
}
catch (MySqlException ex) when (IsTransientError(ex))
{
retryCount++;
Thread.Sleep(1000 * retryCount); // 指数退避
}
}
return new BulkInsertResult { IsSuccess = false };
}
private bool IsTransientError(MySqlException ex)
{
return ex.Number == 2013 || // 连接丢失
ex.Number == 1205 || // 锁等待超时
ex.Number == 1213; // 死锁
}
虽然MySqlBulkCopy很强大,但有时也需要考虑其他方案:
LOAD DATA INFILE:
存储过程:
Entity Framework:
性能对比(插入10万条简单记录):
| 方法 | 耗时(ms) | 内存占用(MB) |
|---|---|---|
| MySqlBulkCopy | 1,200 | 50 |
| LOAD DATA | 800 | 10 |
| EF Core | 45,000 | 300 |
| 单条INSERT | 180,000 | 100 |
结合C#的ETL管道实现复杂转换:
csharp复制var transformedData = rawData.AsEnumerable()
.Select(row => {
var newRow = dataTable.NewRow();
// 实现转换逻辑...
return newRow;
})
.CopyToDataTable();
对于使用Dapper的项目,可以混合使用:
csharp复制var lookupData = connection.Query<LookupItem>("SELECT * FROM lookup_table")
.ToDictionary(x => x.Key);
foreach (var row in data.Rows)
{
row["CategoryId"] = lookupData[row["CategoryName"]].Id;
}
现代应用应该使用异步API:
csharp复制public async Task BulkInsertAsync(DataTable data)
{
using (var connection = new MySqlConnection(connectionString))
{
await connection.OpenAsync();
using (var bulkCopy = new MySqlBulkCopy(connection))
{
await bulkCopy.WriteToServerAsync(data);
}
}
}
关键指标监控:
常见性能瓶颈及解决方案:
网络延迟:
磁盘I/O:
CPU限制:
最近在物流系统中处理运单数据时,我们遇到了一个特殊需求:需要同时导入主表和多个关联表的数据。最终实现的方案是:
关键代码片段:
csharp复制// 导入主表
var mainResult = bulkCopy.WriteToServer(mainData);
// 获取生成的ID
var newIds = connection.Query<long>("SELECT LAST_INSERT_ID() - ROW_COUNT() + 1 as first_id, LAST_INSERT_ID() as last_id")
.First();
// 构建关联表数据
for (int i = 0; i < detailData.Rows.Count; i++)
{
detailData.Rows[i]["main_id"] = newIds.first_id + i;
}
// 导入关联表
detailBulkCopy.WriteToServer(detailData);
这个方案将原本需要2小时的串行操作缩短到了3分钟以内,同时保证了数据的完整性和一致性。