ABP VNext + EF Core 二级缓存:提升查询性能
ABP VNext + EF Core 二级缓存:提升查询性能 🚀
📚 目录
- ABP VNext + EF Core 二级缓存:提升查询性能 🚀
- 引言 🚀
- 一、环境与依赖 🛠️
- 二、集成步骤 ⚙️
- 2.1 安装 NuGet 包
- 2.2 注册缓存服务与拦截器
- 2.3 对特定查询启用缓存 🎯
- 三、缓存依赖与失效 🔄
- 四、性能对比测试 📈
- 4.1 测试环境 🖥️
- 4.2 对比指标 🔥
- 五、最佳实践与注意事项 ⚠️
- 六、高级配置 🧩
引言 🚀
TL;DR ✨
- 集成
EFCoreSecondLevelCacheInterceptor
v5.3.1,为 ABP VNext 应用添加跨DbContext
、跨请求的二级缓存,显著降低重复查询开销 - 几行配置即可启用内存或 Redis 缓存,并支持自动失效与手动失效策略 🔄
- 支持按实体类型或表名缓存,无需手动管理复杂缓存键 🛡️
- 实测:平均响应时间由 ~120 ms 降至 ~15 ms,QPS 从 ~500 提升至 ~3 500,数据库访问次数减少至 1 次/秒 📊
关系型数据库在高并发场景下常见瓶颈包括 CPU、IO 与连接数。EF Core 默认仅在单个 DbContext
生命周期内缓存实体,请求结束后即释放。引入二级缓存(跨 DbContext
、跨请求)可显著减少重复查询开销,缓解数据库压力。
一、环境与依赖 🛠️
-
运行平台:.NET 6.0 LTS + ABP VNext 6.x
-
EF Core 版本:6.x
-
EFCoreSecondLevelCacheInterceptor:5.3.1
-
缓存提供者:
- 内存:
EFCoreSecondLevelCacheInterceptor.MemoryCache
- Redis:
EFCoreSecondLevelCacheInterceptor.StackExchange.Redis
- 内存:
-
其他依赖:
Volo.Abp.EntityFrameworkCore
-
ABP CLI:
Volo.Abp.Cli
v6.x -
前提:项目已集成 EF Core 与 ABP 基础模块,已配置连接字符串与常规
DbContext
-
注意:如需在 .NET 7/8 下使用,请升级到 ABP 7.x 或 ABP 8.x 🔄
二、集成步骤 ⚙️
2.1 安装 NuGet 包
dotnet add package EFCoreSecondLevelCacheInterceptor --version 5.3.1
dotnet add package EFCoreSecondLevelCacheInterceptor.MemoryCache # 内存缓存
# 或
dotnet add package EFCoreSecondLevelCacheInterceptor.StackExchange.Redis # Redis 缓存
2.2 注册缓存服务与拦截器
在 ABP 模块(如 MyProjectEntityFrameworkCoreModule
)的 ConfigureServices
方法中:
public override void ConfigureServices(ServiceConfigurationContext context)
{// 1. 添加二级缓存服务context.Services.AddEFSecondLevelCache(options =>options.UseMemoryCacheProvider().ConfigureLogging(false) // 生产环境关闭日志.UseCacheKeyPrefix("EF_") // 统一前缀,便于分区管理.UseDbCallsIfCachingProviderIsDown(TimeSpan.FromMinutes(1)) // 缓存不可用时回退数据库.CacheAllQueries(CacheExpirationMode.Absolute, TimeSpan.FromMinutes(5)) // 全局缓存所有查询.AllowCachingWithExplicitTransactions(true) // 显式事务中也可缓存); // 2. 注册 DbContext 并注入拦截器(仅针对 MyDbContext)context.Services.AddAbpDbContext<MyDbContext>(options =>{options.AddDefaultRepositories();});context.Services.Configure<AbpDbContextOptions>(opts =>{opts.Configure<MyDbContext>(config =>{config.DbContextOptions.UseSqlServer(context.Services.GetConfiguration().GetConnectionString("Default")).AddInterceptors(context.Services.GetRequiredService<SecondLevelCacheInterceptor>());});});
}
2.3 对特定查询启用缓存 🎯
// 使用全局策略(5 分钟绝对过期)
var products = await _productRepository.WithDetails().Cacheable().ToListAsync();// 自定义滑动过期 1 分钟
var recentOrders = await _orderRepository.Where(o => o.CreatedDate > since).Cacheable(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(1)).ToListAsync();
三、缓存依赖与失效 🔄
-
自动失效:拦截所有
SaveChanges()
/SaveChangesAsync()
,根据受影响表自动清除相关缓存,无需额外配置 -
批量操作限制:EF Core 的
ExecuteUpdate()
与ExecuteDelete()
绕过 ChangeTracker,不会触发缓存失效,需手动清理:await context.Blogs.Where(b => b.IsObsolete).ExecuteUpdateAsync(s => s.SetProperty(b => b.IsActive, false)); _cacheServiceProvider.ClearAllCachedEntries();
-
按类型或表名缓存:
services.AddEFSecondLevelCache(options => {options.UseMemoryCacheProvider().CacheQueriesContainingTypes(CacheExpirationMode.Absolute, TimeSpan.FromMinutes(30),typeof(Product), typeof(Order)).CacheQueriesContainingTableNames(CacheExpirationMode.Absolute, TimeSpan.FromMinutes(30),TableNameComparison.ContainsOnly, "Products", "Orders"); });
-
手动清理示例:在服务中注入并使用
IEFCacheServiceProvider
public class ProductAppService : ApplicationService {private readonly IEFCacheServiceProvider _cacheServiceProvider;public ProductAppService(IEFCacheServiceProvider cacheServiceProvider){_cacheServiceProvider = cacheServiceProvider;}public void RefreshProductCache(){_cacheServiceProvider.ClearAllCachedEntries(); // 清除所有缓存_cacheServiceProvider.ClearCacheByPrefix("EF_Products"); // 按前缀清理} }
四、性能对比测试 📈
4.1 测试环境 🖥️
- 机房环境:Windows Server 2019,Intel Xeon Gold 6248(8 核/16 线程),32 GB RAM
- 数据库:SQL Server 2019
- 数据量:100 万条订单记录
- 测试工具:自编脚本 +
Stopwatch
// 预热
await WarmUpDbAsync();// 测试 1,000 次请求
var sw = Stopwatch.StartNew();
for (int i = 0; i < 1000; i++)
{await _orderRepository.WithDetails().Cacheable().FirstOrDefaultAsync();
}
sw.Stop();
Console.WriteLine($"Elapsed: {sw.ElapsedMilliseconds} ms");
控制台输出示例
Warm-up completed.
Testing 1000 requests...
Elapsed: 15000 ms
声明:以上测试基于串行脚本,仅对比缓存前后性能变化,实际生产环境下并发吞吐量会更高,读者可使用 BenchmarkDotNet 进行多线程基准测试,并查看脚本和日志以复现。
4.2 对比指标 🔥
指标 | 无缓存模式 | 启用二级缓存 |
---|---|---|
平均响应时间 | ~120 ms | ~15 ms |
QPS | ~500/sec | ~3 500/sec |
DB 访问次数 | ~10 次/秒 | ~1 次/秒 |
五、最佳实践与注意事项 ⚠️
- 读多写少:Cache-Aside 模式仅适合读多写少场景,高写场景慎用
- 缓存粒度:对超大结果集拆分分页或按关键字段缓存,避免一次性加载过多数据
- 容量管理:根据业务规模调优 MemoryCache 或 Redis 参数(如内存上限、Eviction 策略),防止 OOM
- 雪崩/穿透:结合互斥锁、预热与空值缓存策略,保障系统稳定性
- 事务内缓存:显式事务内查询默认不缓存,启用需调用
.AllowCachingWithExplicitTransactions(true)
六、高级配置 🧩
services.AddEFSecondLevelCache(options =>
{options.UseMemoryCacheProvider()// 跳过包含特定 SQL 的查询缓存.SkipCachingCommands(cmd => cmd.Contains("NEWID()"))// 跳过空结果集的缓存.SkipCachingResults(result =>result.Value == null ||(result.Value is EFTableRows rows && rows.RowsCount == 0))// 避免某些更新命令触发失效.SkipCacheInvalidationCommands(cmd =>cmd.Contains("UPDATE [Posts] SET [Views]"))// 动态覆盖某些查询的缓存策略.OverrideCachePolicy(context =>{if (context.IsCrudCommand) return null; // CRUD 不缓存if (context.CommandTableNames.Contains("posts"))return new EFCachePolicy().ExpirationMode(CacheExpirationMode.NeverRemove);return null;});
});
这些配置取自官方高级示例,可按需组合使用。