-
EF Core性能优化——C#编写查询缓存与索引
第一部分:C#基础入门
EF Core性能优化——查询缓存与索引
实例介绍
EF Core性能优化的核心是减少数据库访问次数和加速查询执行,而查询缓存与索引是实现这两个目标的关键手段。查询缓存通过复用重复查询的结果,避免频繁访问数据库;索引则通过优化数据存储结构,将全表扫描变为高效的索引查找。本节通过“电vb.net教程C#教程python教程SQL教程access 2010教程
商商品查询系统”的性能瓶颈场景,带你掌握查询缓存的手动实现、EF Core索引配置、缓存失效策略、索引最佳实践,让你的EF Core应用从“慢查询”变为“秒响应”。
需求分析
设计一个高性能的电商商品查询系统,核心需求:
1.查询缓存:对高频查询(如商品分类列表)实现缓存,减少数据库访问;
2.索引优化:对商品查询的过滤字段(如Category、Price)配置合理索引;
3.性能对比:对比缓存前后、索引前后的查询耗时;
4.缓存失效:商品更新后自动失效相关缓存;
5.生产级缓存:实现滑动过期、绝对过期的缓存策略。
代码实现
前置条件:
安装包:Microsoft.EntityFrameworkCore、Microsoft.EntityFrameworkCore.SqlServer、Microsoft.Extensions.Caching.Memory;
创建数据库ECommerceDB;
-
实体类与DbContext配置(含索引)
csharp
using System;
using Microsoft.EntityFrameworkCore;
// 商品实体
public class Product {
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty; // 商品分类(高频过滤字段)
public decimal Price { get; set; } // 价格(高频过滤字段)
public int Stock { get; set; }
public DateTime UpdateTime { get; set; } // 更新时间(用于缓存失效)
}
// 电商DbContext(含索引配置)
public class ECommerceDbContext : DbContext {
private readonly string _connStr;
public ECommerceDbContext(string connStr) => _connStr = connStr;
public DbSet<Product> Products { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options) {
options.UseSqlServer(_connStr)
.LogTo(Console.WriteLine); // 输出SQL日志,便于查看执行计划
}
protected override void OnModelCreating(ModelBuilder model) {
// 1. 普通索引:优化Category字段的过滤查询
model.Entity<Product>()
.HasIndex(p => p.Category)
.HasDatabaseName("IX_Products_Category"); // 指定索引名称
// 2. 复合索引:优化"Category+Price"的联合过滤(顺序重要!先过滤Category,再过滤Price)
model.Entity<Product>()
.HasIndex(p => new { p.Category, p.Price })
.HasDatabaseName("IX_Products_Category_Price");
// 3. 覆盖索引:优化"只查询Name+Price"的场景(避免回表查询)
model.Entity<Product>()
.HasIndex(p => p.Category)
.IncludeProperties(p => new { p.Name, p.Price }) // 包含需要查询的字段
.HasDatabaseName("IX_Products_Category_Cover");
}
}
-
查询缓存服务实现(MemoryCache)
csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
// 商品服务(含查询缓存)
public class ProductService {
private readonly ECommerceDbContext _dbContext;
private readonly IMemoryCache _cache;
private const string CachePrefix = "Product_";
private const int CacheSlidingExpirationMinutes = 5; // 滑动过期:5分钟未访问则失效
private const int CacheAbsoluteExpirationMinutes = 30; // 绝对过期:最多缓存30分钟
public ProductService(ECommerceDbContext dbContext, IMemoryCache cache) {
_dbContext = dbContext;
_cache = cache;
}
// 带缓存的商品分类查询
public async Task<List<Product>> GetProductsByCategoryAsync(string category) {
// 生成缓存Key:确保唯一(查询类型+参数+SQL语句哈希)
var cacheKey = $"{CachePrefix}Category_{category}_{GetQueryHash(category)}";
// 尝试从缓存获取
if (_cache.TryGetValue(cacheKey, out List<Product>? cachedProducts)) {
Console.WriteLine(" 从缓存获取商品列表");
return cachedProducts!;
}
// 缓存不存在:查询数据库
Console.WriteLine(" 从数据库查询商品列表");
var products = await _dbContext.Products
.Where(p => p.Category == category)
.AsNoTracking() // 关闭跟踪,提升性能
.ToListAsync();
// 设置缓存策略
var cacheOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(CacheSlidingExpirationMinutes))
.SetAbsoluteExpiration(TimeSpan.FromMinutes(CacheAbsoluteExpirationMinutes))
.RegisterPostEvictionCallback((key, value, reason, state) => {
// 缓存失效回调:可记录日志
Console.WriteLine($" 缓存失效:Key={key}, Reason={reason}");
});
// 存入缓存
_cache.Set(cacheKey, products, cacheOptions);
return products;
}
// 更新商品(自动失效缓存)
public async Task UpdateProductAsync(Product product) {
_dbContext.Products.Update(product);
await _dbContext.SaveChangesAsync();
// 失效该商品所属分类的缓存
var cacheKey = $"{CachePrefix}Category_{product.Category}_{GetQueryHash(product.Category)}";
_cache.Remove(cacheKey);
Console.WriteLine($" 商品更新后,失效缓存Key={cacheKey}");
}
// 生成查询哈希:避免缓存Key过长(基于查询参数和SQL语句)
private string GetQueryHash(string category) {
// 模拟SQL语句哈希(实际可通过EF Core获取生成的SQL)
var sql = $"SELECT * FROM Products WHERE Category = '{category}'";
return sql.GetHashCode().ToString("X"); // 转为十六进制字符串
}
}
-
性能测试代码
csharp
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Caching.Memory;
class Program {
static async Task Main() {
// 依赖注入配置
var services = new ServiceCollection()
.AddMemoryCache() // 注册内存缓存
.AddScoped<ECommerceDbContext>(sp => new ECommerceDbContext(
"Data Source=(localdb)MSSQLLocalDB;Initial Catalog=ECommerceDB;Integrated Security=True"
))
.AddScoped<ProductService>()
.BuildServiceProvider();
var productService = services.GetRequiredService<ProductService>();
var category = "Electronics";
// 1. 第一次查询:无缓存
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
await productService.GetProductsByCategoryAsync(category);
stopwatch.Stop();
Console.WriteLine($"第一次查询耗时:{stopwatch.ElapsedMilliseconds}ms
");
// 2. 第二次查询:有缓存
stopwatch.Restart();
await productService.GetProductsByCategoryAsync(category);
stopwatch.Stop();
Console.WriteLine($"第二次查询耗时:{stopwatch.ElapsedMilliseconds}ms
");
// 3. 更新商品:触发缓存失效
var product = new Product { Id = 1, Category = category, Name = "Laptop", Price = 999, UpdateTime = DateTime.Now };
await productService.UpdateProductAsync(product);
// 4. 第三次查询:缓存已失效,重新查询
stopwatch.Restart();
await productService.GetProductsByCategoryAsync(category);
stopwatch.Stop();
Console.WriteLine($"第三次查询耗时:{stopwatch.ElapsedMilliseconds}ms
");
}
}
逐行讲解
-
索引配置部分
普通索引IX_Products_Category:针对Category字段的过滤查询(如WHERE Category='Electronics'),将全表扫描变为索引查找,耗时从秒级降到毫秒级。
复合索引IX_Products_Category_Price:针对联合查询(如WHERE Category='Electronics' AND Price<1000),复合索引的顺序遵循“高频过滤在前”原则——先过滤Category(基数大,过滤后数据少),再过滤Price,提升查询效率。
覆盖索引IX_Products_Category_Cover:当查询只需要Name和Price时,索引直接包含这两个字段,无需回表查询原数据,进一步减少IO操作。 -
缓存服务部分
缓存Key生成:CachePrefix+Category+QueryHash确保唯一——CachePrefix区分业务类型,Category区分查询参数,QueryHash避免SQL语句冲突(比如不同参数的相同查询)。
缓存策略:滑动过期+绝对过期结合——滑动过期避免不常用数据占用缓存,绝对过期防止数据长期过时。
缓存失效:商品更新后,主动移除对应分类的缓存,确保数据一致性;RegisterPostEvictionCallback可记录缓存失效日志,便于排查问题。
AsNoTracking:关闭EF Core的ChangeTracker跟踪,减少内存占用,提升查询性能(只读场景推荐使用)。 -
测试代码部分
依赖注入:通过ServiceCollection注册MemoryCache和ProductService,模拟生产环境的依赖注入。
性能对比:第一次查询从数据库获取(耗时较长),第二次从缓存获取(耗时极短),第三次因缓存失效重新查询(耗时回到数据库级别),清晰展示缓存效果。
基础知识拓展 -
查询缓存的类型与原理
一级缓存(ChangeTracker):EF Core内置的缓存,针对同一DbContext实例的重复查询(如同一请求中多次查询相同商品),自动复用结果。但作用域仅限于DbContext实例,无法跨请求共享。
二级缓存(外部缓存):如MemoryCache、Redis等,跨请求共享缓存结果。本节实现的就是二级缓存,适用于高频只读查询场景。
缓存失效场景:数据更新、缓存过期、缓存容量不足(LRU算法淘汰)。 -
索引的核心知识
索引原理:基于B+树结构,将数据按索引字段排序,快速定位数据(类似字典的目录)。
索引代价:索引会增加插入/更新/删除的耗时(需维护B+树结构),因此避免过度索引(建议单表索引不超过5个)。
索引最佳实践:
优先为WHERE、JOIN、ORDER BY字段创建索引;
复合索引遵循“高频过滤在前、基数大在前”原则;
覆盖索引优先于普通索引(减少回表查询);
避免在频繁更新的字段上创建索引(如UpdateTime)。 -
EF Core性能优化工具
EF Profiler:监控EF Core生成的SQL语句,分析执行计划;
EFCoreSecondLevelCacheInterceptor:第三方库,自动实现二级缓存,无需手动编写缓存逻辑;
SQL Server Execution Plan:通过SET SHOWPLAN_XML ON查看SQL执行计划,识别全表扫描、缺失索引等问题。
总结
EF Core性能优化的关键是“缓存减少访问次数,索引加速单次访问”。查询缓存适用于高频只读场景,需注意缓存失效策略;索引适用于慢查询场景,需平衡查询性能与写入代价。本节通过电商商品查询的实例,展示了从索引配置到缓存实现的完整流程,结合性能测试验证了优化效果。掌握这些技巧,你可以轻松解决EF Core应用中的性能瓶颈,打造高效稳定的系统。
关键Takeaway:
缓存Key必须唯一,避免冲突;
复合索引顺序决定查询效率;
缓存失效与数据一致性需同步考虑;
索引不是越多越好,需按需配置。
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49441.html
最新更新
操作MySQL数据库——跨数据库适配
EF Core性能优化——C#编写查询缓存与索引
EF Core迁移——c#开发数据库版本控制
EF Core关联查询——C#编写订单与用户数据
EF Core CRUD——c#处理用户信息管理
EF Core基础映射——C#编写实体与表关联
DataReader与DataSet——用C#进行大数据量读取
参数化查询——C#编写防止SQL注入攻击
C#操作ADO.NET连接SQL Server——基础查询
模式匹配——C#编写复杂对象类型判断
SQL SERVER中递归
2个场景实例讲解GaussDB(DWS)基表统计信息估
常用的 SQL Server 关键字及其含义
动手分析SQL Server中的事务中使用的锁
openGauss内核分析:SQL by pass & 经典执行
一招教你如何高效批量导入与更新数据
天天写SQL,这些神奇的特性你知道吗?
openGauss内核分析:执行计划生成
[IM002]Navicat ODBC驱动器管理器 未发现数据
初入Sql Server 之 存储过程的简单使用
uniapp/H5 获取手机桌面壁纸 (静态壁纸)
[前端] DNS解析与优化
为什么在js中需要添加addEventListener()?
JS模块化系统
js通过Object.defineProperty() 定义和控制对象
这是目前我见过最好的跨域解决方案!
减少回流与重绘
减少回流与重绘
如何使用KrpanoToolJS在浏览器切图
performance.now() 与 Date.now() 对比










