1. 前言
今天和听到同事们在讨论一个关于使用EFCore时,为什么第一次查询数据库总是很慢的原因。我们在工作中经常使用EFCore进行数据访问,但发现每次第一次查询都需要较长的时间,这给我们带来了困扰。因此,我们聚在一起,探讨了这个问题的原因和可能的解决方案。通过查询相关资料,于是就有了这篇博客,现在分享给有需要的你。
2. EFCore首次使用缓慢
在使用Entity Framework Core(EF Core)时,首次查询可能会比较慢,这是因为EF Core需要进行模型构建、元数据加载、数据库连接建立和查询计划生成等操作。那么有什么办法可以解决这个问题呢,答案之一就是使用EF Core的预热来处理问题。
3. 什么是EF Core的预热问题?
EF Core是一个轻量级、可扩展的ORM(对象关系映射)框架,用于在.NET应用程序中处理数据库操作。在应用程序启动时,EF Core需要进行一些初始化操作,如构建模型、加载元数据和建立数据库连接等。这些操作会导致首次查询的耗时增加,影响应用程序的性能。当使用Entity Framework Core(EF Core)进行数据库操作时,会涉及以下几个操作:
- 模型构建(Model Building):
在使用EF Core之前,需要定义领域模型(Domain Model),即表示数据库表格的CLR对象。可以使用属性注解、Fluent API或实现IEntityTypeConfiguration接口等方式来配置模型。在运行时,EF Core会根据模型定义生成相应的数据库表结构。 - 元数据加载(Metadata Loading):
EF Core通过反射和模型构建过程中的元数据提供程序来加载模型的元数据。这些元数据包括实体类型的属性、关系、索引等信息。在第一次创建DbContext实例时,EF Core会从模型构建器中加载元数据。 - 数据库连接建立(Database Connection Establishment):
当执行数据库操作时,EF Core会根据配置连接字符串(Connection String)建立与数据库的连接。连接字符串包含数据库服务器的地址、身份验证方式、数据库名称等信息。EF Core会根据连接字符串选择合适的数据库提供程序来建立连接。 - 查询计划生成(Query Plan Generation):
当执行查询操作时,EF Core会将LINQ查询表达式或查询方法转换为相应的SQL查询语句。这个过程称为查询翻译(Query Translation)。EF Core会根据查询表达式和模型的元数据生成查询计划,包括选择合适的表格、列、关联以及执行顺序等。
上述的操作都是在第一次进行数据库查询时执行的,因此首次查询可能会比较慢。这是因为EF Core需要进行模型构建、元数据加载、数据库连接建立和查询计划生成等操作。为了优化应用程序的性能,可以采取预热操作,提前执行这些操作,从而减少首次查询的耗时。
4. 预热EF Core的解决方案
为了解决EF Core的预热问题,我们可以采取以下措施来优化应用程序的性能:
- 显式调用EnsureCreated方法
在EF Core 3.0及更高版本中,可以通过显式调用EnsureCreated方法来预先构建模型并加载元数据。这样,在第一次查询时,EF Core就不需要再执行这些操作,从而减少查询的耗时。示例代码如下:
using (var context = new MyDbContext())
{
context.Database.EnsureCreated();
}
- 执行迁移操作
如果应用程序使用了EF Core的迁移功能,我们可以在应用程序启动时执行迁移操作。这可以通过调用Database.Migrate方法来实现。该方法会执行所有的迁移操作,并初始化数据库中的表结构和数据。示例代码如下:
using (var context = new MyDbContext())
{
context.Database.Migrate();
}
- 预热连接池
EF Core使用连接池来管理数据库连接。在应用程序启动时,我们可以预先创建和配置一组数据库连接,以减少首次查询时连接建立的时间。这可以通过设置连接池的MinPoolSize属性和调用连接的Open方法来实现。示例代码如下:
var connectionString = "Server=(localdb)\\mssqllocaldb;Database=MyDatabase;Trusted_Connection=True;";
for (int i = 0; i < 10; i++)
{
var connection = new SqlConnection(connectionString);
connection.Open();
connection.Close();
}
5. 测试预热与不预热的对比
为了验证预热操作对EF Core性能的影响,我们可以编写测试代码来比较预热与不预热的情况下的查询执行时间。具体步骤如下:
- 创建一个简单的测试应用程序,包含一个使用EF Core的查询操作。
- 在应用程序的入口点处,分别添加预热操作和不预热操作的代码。
- 编写测试方法,分别对应预热和不预热的情况。在每个测试方法中,创建一个新的DbContext实例,并执行相同的查询操作。
- 使用Stopwatch类来测量每个测试方法的执行时间,并比较两者之间的差异。
通过以上步骤,我们可以得出预热与不预热的情况下查询执行时间的对比结果,从而判断预热操作对EF Core性能的影响。
5.1 示例代码
- 初始化数据库,写入10万条数据
using System;
using Microsoft.EntityFrameworkCore;
namespace EFCoreWarmup
{
// 定义实体类
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
// 定义数据库上下文
public class AppDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
string connectionString = "Data Source=mydb.db";
optionsBuilder.UseSqlite(connectionString);
}
public DbSet<User> Users { get; set; }
}
class Program
{
// 初始化并添加10万条记录
static void InitializeAndAddRecords(AppDbContext context)
{
const int recordsToAdd = 100000;
for (int i = 1; i <= recordsToAdd; i++)
{
context.Users.Add(new User { Name = $"User_{i}" });
// 每1000条保存一次以提高效率
if (i % 1000 == 0)
{
context.SaveChanges();
context.Dispose();
context = new AppDbContext(); // 重新创建上下文以确保内存管理
}
Console.WriteLine(i);
}
}
static void Main(string[] args)
{
using(var db = new AppDbContext())
{
db.Database.EnsureCreated();
InitializeAndAddRecords(db);
}
}
}
}
2.预热前的执行查询
/// <summary>
/// 测试预热前执行查询
/// </summary>
static void TestEfCoreWithoutWarmup()
{
var stopwatch = Stopwatch.StartNew();
using (var context = new AppDbContext())
{
var users = context.Users.ToList().Take(100);
}
stopwatch.Stop();
Console.WriteLine($"预热前执行时间: {stopwatch.ElapsedMilliseconds} ms");
}
static void Main(string[] args)
{
TestEfCoreWithoutWarmup();
}
- 运行结果
4. 预热后的执行查询
/// <summary>
/// 测试预热后执行查询
/// </summary>
static void TestEfCoreWithWarmup()
{
var stopwatch = Stopwatch.StartNew();
using (var context = new AppDbContext())
{
var users = context.Users.ToList();
}
stopwatch.Stop();
Console.WriteLine($"预热后执行时间: {stopwatch.ElapsedMilliseconds} ms");
}
static void Main(string[] args)
{
// 执行预热
using (var context = new AppDbContext())
{
context.Database.EnsureCreated();
}
TestEfCoreWithWarmup();
}
- 运行结果
- 运行结果对比
预热前后的对比,有接近1.2s的差距。由此可见预热的情况,在一定程度上提高了首次执行的效率。
6. EF Core 预热处理的优化技巧
提前执行一些查询操作,以便 EF Core 可以缓存查询计划、连接到数据库并建立连接池等资源,下面是一些 EF Core 预热处理的优化技巧:
- 在应用程序启动时进行预热处理:
在应用程序启动时,执行一些常见的查询操作,以便 EF Core 可以缓存查询计划并建立数据库连接池。这样,在后续的请求中,EF Core 就可以直接使用已经建立好的连接和查询计划,提高性能和响应速度。 - 使用后台任务进行预热处理:
可以将预热处理操作放在一个后台任务中,以避免应用程序启动时的阻塞。例如,可以使用 .NET Core 中的 Hosted Service 或者定时任务库(如 Hangfire)来执行预热处理。 - 选择适当的查询进行预热处理:
根据应用程序的需求,选择一些常用的查询进行预热处理。这些查询通常是应用程序中频繁执行的查询,可以帮助 EF Core 建立查询计划并缓存结果,从而提高性能。 - 注意预热处理的时机和频率:
预热处理不应该过于频繁,否则可能会对数据库造成额外的负担。根据应用程序的特点和数据库的性能,选择适当的时机和频率进行预热处理。 - 监控和调整预热处理的效果:
在进行预热处理后,监控应用程序的性能和响应速度变化。如果发现预热处理没有达到预期的效果,可以考虑调整预热处理的查询内容或者时机。
7. 总结
EF Core的预热问题是由于模型构建、元数据加载、数据库连接建立和查询计划生成等操作导致的。为了提高应用程序的性能,我们可以采取上述解决方案来优化EF Core的预热问题。通过显式调用EnsureCreated方法、执行迁移操作和预热连接池,我们可以减少首次查询的耗时,并提升应用程序的性能。
希望本文对你理解和解决EF Core的预热问题有所帮助!
8. 参考文档:
- EF优化之启动预热-CSDN博客
- C#中 EF(EntityFramework) 性能优化_c# ef core 预热-CSDN博客
- C# EFCore学习总结-CSDN博客
- 探究EFCore8.0同时更新万条记录的SQL生成和执行效率
- 掌握EF Core:全方位面试指南,助你从初级到高级轻松晋级