我有一个后台任务(由IHostedService
启动(,每7天运行一次,该任务将数据复制到同一个表中,只更改PK以生成伪数据(用于演示(。
问题是,当它试图为第二个表保存新数据(大约2k条新记录(时,实体框架从未完成SaveChangesAsync
,该过程被阻止,并开始消耗整个可用RAM。
注意:第一个表有时会复制超过2万条新记录。
这是我目前的代码,我正在使用实体框架核心5.0.4。NET Core 3.1:
using (var context = _context.CreateNewInstance())
{
var existingStudents = context.Students.Where(s => s.UniversityId == _destUniversity.Id);
var sourceStudents = context.Students.Where(s => s.UniversityId == _sourceUniversity.Id)
.Select(s => new Student()
{
//...properties
});
var newStudents = sourceStudents.Where(s => !existingStudents.Any(es => es.DiffKey == s.DiffKey)).ToArray();
if (newStudents.Length == 0)
{
return;
}
await context.Students.AddRangeAsync(newStudents);
await context.SaveChangesAsync(_cancellationToken.Token);
}
我已经试过了:
- 禁用";"自动检测变化">
- 批量插入:这只适用于第一批
- 使用
IEnumerable
而不是IQueryable
- 任务中所有表的单个上下文
- 每个表的新上下文实例
- 比较数据库/表的配置,并将其复制到本地
我有2台服务器(1台本地服务器,1台远程服务器(和3个数据库(1个本地数据库,2个远程数据库(,代码适用于三个数据库中的两个数据库(一个本地数据库和一个远程数据库。
我用这段代码在SQL:上获得了任务的spid
int psid;
using (var command = contextTmp.Database.GetDbConnection().CreateCommand())
{
command.CommandText = "select @@spid as id";
contextTmp.Database.OpenConnection();
using (var result = command.ExecuteReader())
{
result.Read();
psid = result.GetInt16("id");
}
contextTmp.Database.CloseConnection();
}
使用上一段代码中的spid
,我监视了SQL Server端的请求,以检查其状态:
select session_id,
status,
command,
blocking_session_id,
wait_type,
wait_time,
last_wait_type,
wait_resource
from sys.dm_exec_requests
where session_id = @id
结果:
session_id | 状态 | 命令blocking_session_id | 等待时间 | >等待时间
---|---|---|---|
84 | 已挂起 | 选择0 | ASYNC_NETWORK_IO
Entity Framework不擅长批量插入东西。如果实体有一个自动生成的主键,那么在每次插入后,EF都会查询数据库以获取条目的id。这意味着,对于20K条记录中的每一条,都必须等待数据库的往返时间。这就是为什么当你调用SaveChanges()
时,一切似乎都停止了——它仍然在运行,但需要很长时间。有几种方法可以解决这个问题:
- 更改要由客户端生成的id
- 使用像EntityFrameworkPlus这样的扩展库来执行大容量插入
- 使用SqlBulkCopy而不是实体框架
- 使用存储过程
理想情况下,您的查询不需要将数据传输到客户端,只需插入FROM即可。我建议使用第三方扩展linq2db。EntityFrameworkCore(免责声明,我是创建者之一(
然后您的查询将几乎立即在服务器端执行:
using (var context = _context.CreateNewInstance())
{
var existingStudents = context.Students.Where(s => s.UniversityId == _destUniversity.Id);
var sourceStudents = context.Students.Where(s => s.UniversityId == _sourceUniversity.Id)
.Select(s => new Student()
{
//...properties
});
var newStudents = sourceStudents.Where(s => !existingStudents.Any(es => es.DiffKey == s.DiffKey));
await newStudents.InsertAsync(context.Students.ToLinqToDBTable(), x => x, _cancellationToken.Token);
}