一尘不染

通过实体框架保存需要大量资源

cs

我正在使用通过实体框架连接到我的 ASP.NET Mvc 应用程序的 Sql Server Db。应用程序部署在由集群管理的虚拟机上。目前最大内存设置为 32 GB。我可以把它提高到 64 GB。用户可用的解决方案之一是将csv文件加载到数据库。每个文件为一天,由200 列的 80-90k 条记录组成。对于这个解决方案,我使用了这个设置:

public ActionResult AddRecordsFromCSVFile2(FileInput fileInputs)
        {
            BWSERWISEntities bWDiagnosticEntities2 = new BWSERWISEntities();
            bWDiagnosticEntities2.Configuration.AutoDetectChangesEnabled = true;
            bWDiagnosticEntities2.Configuration.ValidateOnSaveEnabled = false;

            var Files = fileInputs.File;
            if (ModelValidate())
            {
                foreach (var fileInput in Files)
                {
                    List<VOLTER_LOGS> LogsToAdd = new List<VOLTER_LOGS>();
                    using (MemoryStream memoryStream = new MemoryStream())
                    {
                        fileInput.InputStream.CopyTo(memoryStream);
                        memoryStream.Seek(0, SeekOrigin.Begin);
                        {
                            using (var streamReader = new StreamReader(memoryStream))
                            {
                                var csvConfig = new CsvConfiguration(CultureInfo.InvariantCulture)
                                {
                                    Delimiter = ";",
                                    HasHeaderRecord = false
                                };
                                using (var csvReader = new CsvReader(streamReader, csvConfig))
                                {
                                    var records = csvReader.GetRecords<ReadAllRecords>().ToList();
                                    var firstRecord = records.Where(x => x.Parametr4 != "0" && x.Parametr5 != "0" && x.Parametr6 != "0").FirstOrDefault();
                                    var StartDateToCheck = Convert.ToDateTime(firstRecord.Parametr4 + "-" + firstRecord.Parametr5 + "-" + firstRecord.Parametr6 + " " + firstRecord.Parametr3 + ":" + firstRecord.Parametr2 + ":" + firstRecord.Parametr1, new CultureInfo("en-GB", true));
                                    var lastRecord = records.Where(x => x.Parametr4 != "0" && x.Parametr5 != "0" && x.Parametr6 != "0").LastOrDefault();
                                    var EndDateToCheck = Convert.ToDateTime(lastRecord.Parametr4 + "-" + lastRecord.Parametr5 + "-" + lastRecord.Parametr6 + " " + lastRecord.Parametr3 + ":" + lastRecord.Parametr2 + ":" + lastRecord.Parametr1, new CultureInfo("en-GB", true));
                                    int matchingMachineID = int.Parse(firstRecord.Parametr7);
                                    var matchingElements = bWDiagnosticEntities2.VOLTER_LOGS.Where(x => (x.Parametr7 == matchingMachineID) && (x.Parametr1 >= StartDateToCheck && x.Parametr1 <= EndDateToCheck)).ToList();
                                    bWDiagnosticEntities2.VOLTER_LOGS.RemoveRange(matchingElements);
                                    bWDiagnosticEntities2.SaveChanges();                                
                                    foreach (var record in records)
                                    {
                                        if (record.Parametr4 != "0" && record.Parametr5 != "0" && record.Parametr6 != "0")
                                        {
                                            bWDiagnosticEntities2.Configuration.AutoDetectChangesEnabled = false;
                                            string date = record.Parametr4 + "-" + record.Parametr5 + "-" + record.Parametr6 + " " + record.Parametr3 + ":" + record.Parametr2 + ":" + record.Parametr1;
                                            DateTime recordDate = DateTime.Parse(date, new CultureInfo("en-GB", true));
                                            // DateTime recordDate = DateTime.ParseExact(date, "dd-MM-yyyy HH:mm:ss", new CultureInfo("en-GB"));
                                            VOLTER_LOGS vOLTER_LOGS = new VOLTER_LOGS();
                                            vOLTER_LOGS.Parametr1 = recordDate;
                                            vOLTER_LOGS.Parametr2 = 0;
                                            vOLTER_LOGS.Parametr3 = 0;
                                            vOLTER_LOGS.Parametr4 = 0;
                                            vOLTER_LOGS.Parametr5 = 0;
                                            vOLTER_LOGS.Parametr6 = 0;
                                            vOLTER_LOGS.Parametr7 = int.Parse(record.Parametr7);

7-200...
                                            vOLTER_LOGS.Parametr200 = int.Parse(record.Parametr200);

                                            LogsToAdd.Add(vOLTER_LOGS);

                                        }
                                    }
                                }
                            }
                        }
                        bWDiagnosticEntities2.VOLTER_LOGS.AddRange(LogsToAdd);
                        bWDiagnosticEntities2.SaveChanges();
                    }
                }
            }    
            return View();
        }

此实现需要5-8 分钟并占用虚拟机内存的45-55%,但在保存过程中有时很难得到其他简单请求的响应(获取第一个和最后一个日期)并出现错误:

The request limited has expired or server is not responding.

服务器有很多可用内存,但表已经有 7kk 条记录并且还在增长。在其他用户请求期间,最终可能的文件不会被保存。现在我想知道将实体框架用于该数据量是个好主意。Application的主要目标是检测一段时间内数据量的错误。有没有人碰巧有通过实体框架处理这么多数据的经验?是否有任何解决方案可以将资源分配给每个任务,以便每个请求都能得到响应?


阅读 155

收藏
2022-07-21

共1个答案

一尘不染

EF 是 ORM,而不是导入工具。ORM 的工作是加载相关实体的图,并给人以使用对象而不是表和 SQL 的印象。导入作业中没有实体或图形,但有文件、字段、表格和映射。

在 .NET 中导入数据 SQL Server 表的最快方法是使用SqlBulkCopy类。这使用bcp工具或BULK INSERT命令中使用的相同机制,以使用最少的日志记录将数据作为流导入。输入可以是 DataTable 或 IDataReader 实例。

CsvHelperExcelDataReader等库提供开箱即用的 IDataReader 派生类。在其他情况下,FastMember的 ObjectReader 可用于在任何 IEnumerable 上创建 IDataReader 包装器。

使用 CsvHelper 和 SqlBulkCopy 将 CSV 加载到表中可以像这样简单,只要文件和表列匹配:

public async Task SimpleImport(string path, string table, 
                               string connectionString)
{
    using var reader = new StreamReader(path);
    var csvConfig = new CsvConfiguration(CultureInfo.InvariantCulture)
        {
            Delimiter = ";"
        };
    using var csv = new CsvReader( reader, csvConfig);

    using var dr = new CsvDataReader(csv);
    using var bcp = new SqlBulkCopy(connectionString);
    bcp.DestinationTableName = table;

    await bcp.WriteToServerAsync(dr);
}

如果没有标头,则必须通过 CsvHelper 的配置提供它们,例如通过GetDynamicPropertyName或类映射:

string[] fields=new[] {.....};
csvConfig.GetDynamicPropertyName = args=> {
    if (args.FieldIndex<10)
    {
        return fields[args.FieldIndex];
    }
    else
    {
        return $"Value_{args.FieldIndex}";
    }
};

使用 ClassMap 允许指定类型转换,包括复杂的转换

public class Foo
{
    ...
    public int Year { get; set; }
    public int Month { get; set; }
    public int Day { get; set; }
    public DateTime Date { get; set; }
}

public class FooMap : ClassMap<Foo>
{
    public FooMap()
    {
        AutoMap(CultureInfo.InvariantCulture);

        Map(m => m.Date).Convert(row => DateFromParts(row.GetField(5),row.GetField(6),row.GetField(7));
    }

    DateTime DateFromParts(string year, string month, string say)
    {
        return DateTime.Parse($"{year}-{month}-{day}");
    }
}
2022-07-21