# 關於EF Core更新速度隨時間越來越慢的解決辦法 ## 概要 本篇主要介紹使用 `context.ChangeTracker.Clear() `方法,在通過迴圈進行批量更新時,通過手動清除跟蹤實體以提高性能的示例。 ## 背景 最近在做一些數據分析時,遇到了一個問題,當我把計算結果更新到資料庫 ...
關於EF Core更新速度隨時間越來越慢的解決辦法
概要
本篇主要介紹使用 context.ChangeTracker.Clear()
方法,在通過迴圈進行批量更新時,通過手動清除跟蹤實體以提高性能的示例。
背景
最近在做一些數據分析時,遇到了一個問題,當我把計算結果更新到資料庫時,一開始速度會很快,但隨著時間的推移,更新速度會越來越慢。
本篇博客就來說明這種現象的原因和解決辦法。
環境:
ASP.NET Core 7
和EF Core 7
.
事例說明
我有1000W已處理好的數據需要更新到資料庫,這些數據我也是從資料庫中一次性查詢出來的,這樣可以只進行一次查詢,並使用AsNoTracking()
提高查詢效率,然後我對這些數據進行了並行計算,最後將計算完的結果更新到資料庫。最費時的操作就是更新到資料庫。
請看以下代碼示例:
var bc = new ConcurrentBag<List<StockDailyKLineInfo>>();
// 並行計算
var computeTasks = group.AsParallel()
.WithDegreeOfParallelism(Environment.ProcessorCount)
.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
.Select(async g =>
{
var computedData = await service.ComputeAsync(g.ToList());
if (computedData != null)
{
bc.Add(computedData);
}
});
await Task.WhenAll(computeTasks);
// 數據插入
var batchSize = 5000;
var items = bc.SelectMany(x => x).ToList();
left = items.Count;
_logger.LogInformation($"need update {left} daily!");
foreach (var batch in items.Chunk(batchSize))
{
context.AttachRange(batch);
foreach (var entity in batch)
{
var entry = context.Entry(entity);
entry.Property(e => e.A).IsModified = true;
entry.Property(e => e.B).IsModified = true;
entry.Property(e => e.C).IsModified = true;
entry.State = EntityState.Modified;
}
var count = await context.SaveChangesAsync();
}
await Console.Out.WriteLineAsync("[done] update all data");
並行計算速度非常快,幾秒就能都完成了。
數據插入,我分批進行迴圈插入,每次5000條,通常不到1秒時間就能插入成功。但隨著時間的推移,插入速度越來越慢。
[!NOTE]
由於我有1000W的數據插入,如果最終一次性提交,如果出現了異常,那麼所有數據都不會插入成功,並且會等待很長的時間,並且在最終執行完成之前,你得不到任何信息,以預估可能花費的時間。所以我需要分批插入。
原因
EF Core 會在上下文中跟蹤所有已載入或附加的實體。隨著迴圈的進行,上下文將追蹤越來越多的實體,這可能會導致性能下降。
也就是說在同一個DbContext
上下文中,SaveChangesAsync()方法調用後,不會清除已更新的內容,這意味著追蹤的實體越來越多,最終多達1000W,並且這些都是已經標記為要更新的內容,也意味著你每次都會更新更多的內容到資料庫。
解決辦法
只進行一次SaveChanges
既然每次saveChanges
不會清除,那麼最後我只提交一次不就行了麽?但這個方案不符合實際需求,上面已經提到過了。
使用多個DbContext
既然 同一個DbContext
下會出現這個問題,那麼每次更新,我再創建一個新的DbContext不就可以了麽?
這個方法雖然可行,但對於1000W的數據來說,即使我每次更新1W條數據,也需要創建1000+次DbContext
,也有一定的消耗。
清除追蹤
既然問題是SaveChanges不會自動清除已追蹤的更改,如果我可以手動去清除,不就可以了麽?清除的操作比起創建新的DbContext
實例,還是更快捷的。
那麼我們修改代碼:
foreach (var batch in items.Chunk(batchSize))
{
context.AttachRange(batch);
foreach (var entity in batch)
{
var entry = context.Entry(entity);
entry.Property(e => e.A).IsModified = true;
entry.Property(e => e.B).IsModified = true;
entry.Property(e => e.C).IsModified = true;
entry.State = EntityState.Modified;
}
var count = await context.SaveChangesAsync();
// ⚒️ add this line
context.ChangeTracker.Clear();
}
[!TIP]
context.ChangeTracker.Clear()
方法清除上下文中的所有已跟蹤實體。這將重置更改跟蹤器並清除其跟蹤的所有實體,從而釋放記憶體並提高性能。
總結
EF Core 7
中已經添加了批量更新的方法,但這種方法也不適用於我遇到的場景,因為我不是按條件進行批量更新,而是每一條數據都需要更新。
context.ChangeTracker.Clear()
可以在這樣的場景下發揮作用,在一些關聯插入或更新的場景,為避免追蹤帶來的衝突問題,也可以通過該方法清除追蹤,然後再手動建立關係,進行提交。