VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • 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;

  1. 实体类与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");
	}
	}
  1. 查询缓存服务实现(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"); // 转为十六进制字符串
	}
	}
  1. 性能测试代码
    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
");
	}
	}

逐行讲解

  1. 索引配置部分
    普通索引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操作。
  2. 缓存服务部分
    缓存Key生成:CachePrefix+Category+QueryHash确保唯一——CachePrefix区分业务类型,Category区分查询参数,QueryHash避免SQL语句冲突(比如不同参数的相同查询)。
    缓存策略:滑动过期+绝对过期结合——滑动过期避免不常用数据占用缓存,绝对过期防止数据长期过时。
    缓存失效:商品更新后,主动移除对应分类的缓存,确保数据一致性;RegisterPostEvictionCallback可记录缓存失效日志,便于排查问题。
    AsNoTracking:关闭EF Core的ChangeTracker跟踪,减少内存占用,提升查询性能(只读场景推荐使用)。
  3. 测试代码部分
    依赖注入:通过ServiceCollection注册MemoryCache和ProductService,模拟生产环境的依赖注入。
    性能对比:第一次查询从数据库获取(耗时较长),第二次从缓存获取(耗时极短),第三次因缓存失效重新查询(耗时回到数据库级别),清晰展示缓存效果。
    基础知识拓展
  4. 查询缓存的类型与原理
    一级缓存(ChangeTracker):EF Core内置的缓存,针对同一DbContext实例的重复查询(如同一请求中多次查询相同商品),自动复用结果。但作用域仅限于DbContext实例,无法跨请求共享。
    二级缓存(外部缓存):如MemoryCache、Redis等,跨请求共享缓存结果。本节实现的就是二级缓存,适用于高频只读查询场景。
    缓存失效场景:数据更新、缓存过期、缓存容量不足(LRU算法淘汰)。
  5. 索引的核心知识
    索引原理:基于B+树结构,将数据按索引字段排序,快速定位数据(类似字典的目录)。
    索引代价:索引会增加插入/更新/删除的耗时(需维护B+树结构),因此避免过度索引(建议单表索引不超过5个)。
    索引最佳实践:
    优先为WHERE、JOIN、ORDER BY字段创建索引;
    复合索引遵循“高频过滤在前、基数大在前”原则;
    覆盖索引优先于普通索引(减少回表查询);
    避免在频繁更新的字段上创建索引(如UpdateTime)。
  6. 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


相关教程