【问题标题】:Entity Framework Core traverse big blob data without memory overflow, best practiceEntity Framework Core 遍历大 blob 数据没有内存溢出,最佳实践
【发布时间】:2020-03-09 00:27:21
【问题描述】:

我正在编写遍历大量图片数据的代码,准备一个包含所有图片数据的大增量块以供发送。

这是一个关于如何处理这些数据的示例

[MessagePackObject]
public class Blob : VersionEntity
{
    [Key(2)]
    public Guid Id { get; set; }
    [Key(3)]
    public DateTime CreatedAt { get; set; }
    [Key(4)]
    public string Mediatype { get; set; }
    [Key(5)]
    public string Filename { get; set; }
    [Key(6)]
    public string Comment { get; set; }
    [Key(7)]
    public byte[] Data { get; set; }
    [Key(8)]
    public bool IsTemporarySmall { get; set; }
}

public class BlobDbContext : DbContext
{
    public DbSet<Blob> Blob { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blob>().HasKey(o => o.Id);
    }
}

在处理这个问题时,我将所有内容都处理成一个文件流,并且我希望在任何给定时间尽可能少地保留在内存中。

这样就够了吗?

foreach(var b in context.Where(o => somefilters).AsNoTracking())
    MessagePackSerializer.Serialize(stream, b);

这仍然会用所有 blob 记录填满内存,还是会在我迭代枚举器时一一处理。它不使用任何 ToList,只使用枚举器,因此 Entity Framework 应该能够随时随地处理它,但我不确定它是否这样做。

这里的任何实体框架专家都可以就如何正确处理提供一些指导。

【问题讨论】:

  • 我不是 100% 确定,但我认为这会导致将单个查询发送到数据库,但是它会在 c# 端 1 接 1 处理它。(你可以用 sql 检查这个profiler)你可以改变你的循环并使用 skip 和 take 来确保你得到一个项目,但这不是 ef 的用途,所以我不确定你是否会找到最佳实践。
  • 如果我理解正确,SqlDataReader 将在您迭代 Read() 时连接到数据库并获取部分。如果枚举器在这里的工作方式相同,那应该没问题。但是如果它缓冲所有,然后迭代,我们就有问题了。这里有人可以确认这是如何工作的吗?我希望它执行一个查询,但有一个到数据库的流连接,并在你处理数据时工作,一次处理和释放一个实体。
  • 为什么不对代码进行内存分析?我们不能为你这样做。此外,由于未知的组件和周围的代码,这个问题很广泛/不清楚(如果不是为了赏金,就会被搁置)。 (比如,stream 来自哪里?)。最后,快速处理 SQL Server 文件流数据和流式处理需要一种超越 EF 的不同方法。

标签: c# entity-framework-core out-of-memory blob traversal


【解决方案1】:

通常,当您在实体上创建 LINQ 过滤器时,就像以代码形式编写 SQL 语句一样。它返回一个IQueryable,它实际上并未针对数据库执行。当您使用 foreach 迭代 IQueryable 或调用 ToList() 时,将执行 sql 并返回所有结果并存储在内存中。

https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/ef/language-reference/query-execution

虽然 EF 可能不是纯粹性能的最佳选择,但有一种相对简单的方法可以处理此问题,而无需过多担心内存使用情况:

考虑以下

var filteredIds = BlobDbContext.Blobs
                      .Where(b => b.SomeProperty == "SomeValue")
                      .Select(x => x.Id)
                      .ToList();

现在您已经根据您的要求过滤了 Blob,并对数据库执行了此操作,但只返回了内存中的 Id 值。

然后

foreach (var id in filteredIds)
{
    var blob = BlobDbContext.Blobs.AsNoTracking().Single(x => x.Id == id);
    // Do your work here against a single in-memory blob
}

大的 blob 应该可以在你完成后用于垃圾回收,并且你不应该用完内存。

显然,您可以感知检查 id 列表中的记录数,或者您可以将元数据添加到第一个查询中,以帮助您决定如何处理它,如果您想完善这个想法。

【讨论】:

  • 这不能回答我的问题。我想知道 EF 在遍历枚举器时是否按顺序处理从查询中获取,就像 SqlDataReader 对 Next 所做的那样。应该是可以的,也是首选的方式,而不是一个个去取。我在这里最接近答案的是 Smit Patel 在此处的答案中所说的:github.com/aspnet/EntityFrameworkCore/issues/14640 他说:“这意味着,我们不需要在内部进行缓冲。因此,在您的情况下,无跟踪查询将不获取/存储比当前结果行更多的数据。”。
  • 如果您可以 100% 确认 EF 在枚举之前获取所有内容,如果您还提供了一种使用 SqlDataReader 以正确方式执行此操作的方法,那么这将是答案的一部分。或者,如果 EF 确实做到了这一点,那么对此的确认将是一个答案。无论如何,这开始花费的时间比我调试 ​​EF 进行确认所花费的时间更多;)
  • 对不起 - 我做了一点挖掘,但没有深入到它的底部。我建议如果你担心纯粹的性能,EF 不是要走的路,如果你想保持 EF 范式,那么我的回答确保你不会耗尽内存。假设Id 有一个聚集索引,那么很多顺序查询的性能影响可能没有你想象的那么糟糕。
【解决方案2】:

只要你按照nottracking的说明直接使用它,它只会读取你在迭代时获取和dispose的部分,所以我的第一个假设是正确的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-01-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-11
    相关资源
    最近更新 更多