ABP VNext + EF Core 二级缓存:提升查询性能 🚀
📚 目录
引言 🚀
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;
});
});
这些配置取自官方高级示例,可按需组合使用。