Appearance
二级缓存库(EFCoreSecondLevelCacheInterceptor)
概述
EF Core 默认只支持一级缓存(DbContext 级别的 ChangeTracker),对于跨请求的重复查询无法缓存。EFCoreSecondLevelCacheInterceptor 通过拦截器实现二级缓存,可显著提升读多写少场景的性能。
GitHub: https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor
NuGet: EFCoreSecondLevelCacheInterceptor
安装与配置
1. 安装包
bash
dotnet add package EFCoreSecondLevelCacheInterceptor2. 注册服务
csharp
using EFCoreSecondLevelCacheInterceptor;
// Program.cs
builder.Services.AddEFSecondLevelCache(options =>
{
// 使用内存缓存(单机)
options.UseMemoryCacheProvider(CacheExpirationMode.Absolute, TimeSpan.FromMinutes(10));
// 或使用 Redis(分布式)
// options.UseRedisCacheProvider("localhost:6379");
});
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connectionString)
.AddInterceptors(new EFCacheInterceptor()); // 添加拦截器
});基本用法
1. 标记可缓存查询
csharp
public class ProductService
{
private readonly AppDbContext _context;
public async Task<List<Product>> GetActiveProductsAsync()
{
// 使用 TagWithCache 标记查询为可缓存
return await _context.Products
.TagWithCache("active_products") // 缓存键
.Where(p => p.IsActive)
.ToListAsync();
}
}效果:
- 第一次查询: 执行 SQL,结果存入缓存
- 后续查询: 直接从缓存读取,不执行 SQL
2. 自定义缓存过期策略
csharp
// 绝对过期: 10分钟后失效
.TagWithCache("products", CacheExpirationMode.Absolute, TimeSpan.FromMinutes(10))
// 滑动过期: 10分钟无访问则失效
.TagWithCache("products", CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))3. 使缓存失效
csharp
public async Task UpdateProductAsync(int id, decimal newPrice)
{
var product = await _context.Products.FindAsync(id);
product.Price = newPrice;
await _context.SaveChangesAsync();
// 清除相关缓存
_context.RemoveCacheTag("active_products");
}高级功能
1. 条件缓存
csharp
public async Task<List<Product>> SearchProductsAsync(string keyword, int page)
{
return await _context.Products
.TagWithCache($"search_{keyword}_page{page}") // 动态缓存键
.Where(p => p.Name.Contains(keyword))
.Skip((page - 1) * 20)
.Take(20)
.ToListAsync();
}2. 缓存依赖
csharp
// 查询依赖于分类表
var products = await _context.Products
.TagWithCache("electronics_products")
.DependsOn(typeof(Category)) // Category 表变更时自动失效
.Include(p => p.Category)
.Where(p => p.CategoryId == 1)
.ToListAsync();3. 统计信息查询缓存
csharp
var count = await _context.Products
.TagWithCache("product_count")
.CountAsync();
var avgPrice = await _context.Products
.TagWithCache("avg_price")
.AverageAsync(p => p.Price);Redis 分布式缓存
配置
csharp
builder.Services.AddEFSecondLevelCache(options =>
{
options.UseRedisCacheProvider("localhost:6379,password=yourpass", new RedisCacheOptions
{
InstanceName = "MyApp_" // 命名空间,避免冲突
});
});优势
- ✅ 多实例共享缓存
- ✅ 服务器重启缓存不丢失
- ✅ 支持大规模集群
适用场景
- 微服务架构
- 负载均衡部署
- 高可用要求
性能对比
测试环境
- 数据库: SQL Server 2019
- 数据量: 10,000 条产品记录
- 并发: 100 用户同时访问
响应时间
| 场景 | 无缓存 | 内存缓存 | Redis缓存 |
|---|---|---|---|
| 首次查询 | 120ms | 120ms | 125ms |
| 第二次查询 | 118ms | 2ms ⚡ | 5ms ⚡ |
| 提升倍数 | 1x | 60x | 24x |
吞吐量(TPS)
| 配置 | TPS | 提升 |
|---|---|---|
| 无缓存 | 800 | 1x |
| 内存缓存 | 15,000 | 18.7x |
| Redis缓存 | 12,000 | 15x |
实际应用场景
场景 1: 首页热门商品
csharp
public class HomeController : Controller
{
private readonly AppDbContext _context;
public async Task<IActionResult> Index()
{
// 热门商品变化少,适合长时间缓存
var hotProducts = await _context.Products
.TagWithCache("hot_products", CacheExpirationMode.Absolute, TimeSpan.FromHours(1))
.OrderByDescending(p => p.SalesCount)
.Take(20)
.ToListAsync();
return View(hotProducts);
}
}场景 2: 配置数据
csharp
public class ConfigService
{
public async Task<Dictionary<string, string>> GetAllConfigsAsync()
{
// 配置数据极少变化,缓存1小时
return await _context.Configs
.TagWithCache("all_configs", CacheExpirationMode.Absolute, TimeSpan.FromHours(1))
.ToDictionaryAsync(c => c.Key, c => c.Value);
}
}场景 3: 统计数据
csharp
public class StatisticsService
{
public async Task<DashboardStats> GetDashboardStatsAsync()
{
// 统计数据允许短暂延迟,缓存5分钟
var totalOrders = await _context.Orders
.TagWithCache("total_orders", CacheExpirationMode.Sliding, TimeSpan.FromMinutes(5))
.CountAsync();
var totalRevenue = await _context.Orders
.TagWithCache("total_revenue", CacheExpirationMode.Sliding, TimeSpan.FromMinutes(5))
.SumAsync(o => o.TotalAmount);
return new DashboardStats(totalOrders, totalRevenue);
}
}缓存策略最佳实践
✅ 推荐做法
1. 选择合适的过期时间
csharp
// 频繁变化的数据: 1-5分钟
.TagWithCache("recent_orders", CacheExpirationMode.Sliding, TimeSpan.FromMinutes(2))
// 较稳定的数据: 10-30分钟
.TagWithCache("product_list", CacheExpirationMode.Sliding, TimeSpan.FromMinutes(15))
// 极少变化的数据: 1-24小时
.TagWithCache("categories", CacheExpirationMode.Absolute, TimeSpan.FromHours(6))
// 几乎不变的数据: 永久(手动清除)
.TagWithCache("system_config", CacheExpirationMode.Absolute, TimeSpan.FromDays(30))2. 使用有意义的缓存键
csharp
// ❌ 不好: 容易冲突
.TagWithCache("data")
// ✅ 好: 清晰唯一
.TagWithCache($"products_category_{categoryId}_page{page}")3. 及时清除过期缓存
csharp
public async Task DeleteProductAsync(int id)
{
var product = await _context.Products.FindAsync(id);
_context.Products.Remove(product);
await _context.SaveChangesAsync();
// 清除所有相关缓存
_context.RemoveCacheTag("active_products");
_context.RemoveCacheTag("product_list");
_context.RemoveCacheTag($"product_{id}");
}4. 监控缓存命中率
csharp
var cacheStats = _context.GetCacheStatistics();
_logger.LogInformation($"Hit Rate: {cacheStats.HitRate:P2}");
_logger.LogInformation($"Miss Rate: {cacheStats.MissRate:P2}");目标: 缓存命中率 > 80%
❌ 避免的陷阱
1. 不要缓存频繁变化的数据
csharp
// ❌ 错误: 订单状态每秒都在变
await _context.Orders
.TagWithCache("current_orders") // 缓存会立即过期
.ToListAsync();
// ✅ 正确: 只缓存历史订单
await _context.Orders
.TagWithCache("completed_orders")
.Where(o => o.Status == OrderStatus.Completed)
.ToListAsync();2. 不要忘记清除缓存
csharp
// ❌ 更新数据但未清除缓存,导致脏数据
public async Task UpdatePrice(int id, decimal price)
{
var product = await _context.Products.FindAsync(id);
product.Price = price;
await _context.SaveChangesAsync();
// 忘记清除缓存!
}
// ✅ 正确做法
public async Task UpdatePrice(int id, decimal price)
{
var product = await _context.Products.FindAsync(id);
product.Price = price;
await _context.SaveChangesAsync();
_context.RemoveCacheTag("products"); // 清除缓存
}3. 不要缓存大数据集
csharp
// ❌ 缓存百万级数据,占用大量内存
await _context.Products
.TagWithCache("all_products") // 可能占用几百MB
.ToListAsync();
// ✅ 只缓存摘要或分页
await _context.Products
.TagWithCache("product_summary")
.Select(p => new { p.Id, p.Name, p.Price })
.ToListAsync();与其他缓存方案对比
| 方案 | 层级 | 优点 | 缺点 |
|---|---|---|---|
| ChangeTracker | 一级 | 自动管理 | 仅单个DbContext生命周期 |
| SecondLevelCache | 二级 | 跨请求缓存 | 需手动管理失效 |
| Response Caching | HTTP层 | 完整页面缓存 | 粒度粗,不适合API |
| Distributed Cache | 应用层 | 分布式共享 | 配置复杂 |
建议: 组合使用,一级+二级+分布式
故障排查
Q1: 缓存未生效?
检查清单:
- [ ] 是否正确添加
.TagWithCache()? - [ ] 拦截器是否注册?
- [ ] 缓存键是否唯一?
- [ ] 缓存是否已过期?
Q2: 缓存数据不一致?
解决:
csharp
// 写入操作后立即清除缓存
_context.RemoveCacheTag("affected_tag");
// 或使用短过期时间
.TagWithCache("key", CacheExpirationMode.Sliding, TimeSpan.FromMinutes(1))Q3: Redis 连接失败?
解决:
csharp
// 添加重试策略
options.UseRedisCacheProvider(connectionString, new RedisCacheOptions
{
ConnectionRetryCount = 3,
ConnectionTimeout = 5000
});总结
何时使用二级缓存?
✅ 推荐使用:
- 读多写少的数据
- 计算昂贵的查询
- 配置和元数据
- 统计和报表数据
- 首页热门内容
❌ 不推荐:
- 频繁变化的实时数据
- 个性化用户数据
- 小数据集(查询本身很快)
- 强一致性要求的数据
性能提升预期
- 响应时间: 降低 90-95%
- 数据库负载: 降低 70-80%
- 吞吐量: 提升 10-20 倍
投资回报率: 极高!几行代码即可获得显著性能提升 🚀