关于 caching
面向 .NET 开发者的缓存实践技能,介绍 HybridCache、输出缓存及失效策略,并强调显式过期时间和避免常见反模式。
主要功能
- 01HybridCache 缓存建议
- 02缓存失效策略
- 03HTTP 响应输出缓存
- 04TTL 与用户级缓存键
适用场景
- 01为频繁读取的数据添加缓存
- 02在数据变更时清除缓存
- 03缓存完整 HTTP 响应
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill caching安装后在 Claude Code 中输入/caching确认已加载。
详细介绍
缓存
核心原则
- HybridCache 是默认选择 — .NET 9+ 引入了
HybridCache,作为统一的缓存抽象。它结合了内存(L1)和分布式(L2)缓存,并提供缓存击穿保护。参见 ADR-004。 - 缓存读取,而非写入 — 缓存 GET 操作。在发生变更时失效。绝不缓存 POST/PUT/DELETE 响应。
- 对完整响应使用输出缓存 — 当整个 HTTP 响应都可以缓存时(公共 API、静态数据),使用输出缓存中间件。
- 设置明确的 TTL — 每个缓存项都需要过期时间。不要使用没有上限的缓存。
模式
HybridCache(推荐的默认选择)
// Program.cs
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(5),
LocalCacheExpiration = TimeSpan.FromMinutes(2)
};
});
// Optional: Add Redis as the L2 distributed cache
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
// Usage in a handler
public class GetProduct
{
public record Query(Guid Id);
public record Response(Guid Id, string Name, decimal Price);
internal class Handler(AppDbContext db, HybridCache cache)
{
public async Task<Response?> Handle(Query query, CancellationToken ct)
{
return await cache.GetOrCreateAsync(
$"products:{query.Id}",
async token => await db.Products
.Where(p => p.Id == query.Id)
.Select(p => new Response(p.Id, p.Name, p.Price))
.FirstOrDefaultAsync(token),
new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(10)
},
cancellationToken: ct);
}
}
}
缓存失效
// Invalidate on mutation
public class UpdateProduct
{
internal class Handler(AppDbContext db, HybridCache cache)
{
public async Task<Result> Handle(Command command, CancellationToken ct)
{
var product = await db.Products.FindAsync([command.Id], ct);
if (product is null) return Result.Failure("Product not found");
product.Update(command.Name, command.Price);
await db.SaveChangesAsync(ct);
// Invalidate the cached entry
await cache.RemoveAsync($"products:{command.Id}", ct);
return Result.Success();
}
}
}
输出缓存(完整响应缓存)
// Program.cs
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(b => b.NoCache()); // Don't cache by default
options.AddPolicy("ProductList", b => b
.Expire(TimeSpan.FromMinutes(5))
.Tag("products"));
options.AddPolicy("ProductById", b => b
.Expire(TimeSpan.FromMinutes(10))
.SetVaryByRouteValue("id")
.Tag("products"));
});
app.UseOutputCache();
// Apply to endpoints
group.MapGet("/", ListProducts).CacheOutput("ProductList");
group.MapGet("/{id:guid}", GetProduct).CacheOutput("ProductById");
// Invalidate by tag on mutations
group.MapPut("/{id:guid}", async (Guid id, UpdateProductRequest request,
IOutputCacheStore store, CancellationToken ct) =>
{
// ... update logic ...
await store.EvictByTagAsync("products", ct);
return TypedResults.NoContent();
});
Cache-Aside 模式(旧版)
对所有新代码,优先使用 HybridCache。手动使用
IDistributedCache实现 Cache-Aside 缺乏缓存击穿保护,需要手动序列化,也没有 L1/L2 分层。仅在与现有代码集成,且该代码已经直接使用IDistributedCache时使用。
反模式
不要在没有过期时间的情况下缓存
// BAD — cache lives forever, stale data guaranteed
await cache.SetStringAsync(key, value);
// GOOD — always set TTL
await cache.SetStringAsync(key, value, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});
不要缓存可变的用户专属数据
// BAD — caching user's cart with a global key
await cache.GetOrCreateAsync("shopping-cart", ...);
// GOOD — include user ID in key
await cache.GetOrCreateAsync($"shopping-cart:{userId}", ...);
不要自行实现缓存击穿保护
// BAD — manual lock to prevent cache stampede
private static readonly SemaphoreSlim Lock = new(1, 1);
await Lock.WaitAsync();
try { /* check cache, populate if missing */ }
finally { Lock.Release(); }
// GOOD — HybridCache has built-in stampede protection
await hybridCache.GetOrCreateAsync(key, factory);
决策指南
| 场景 | 建议 |
|---|---|
| 一般数据缓存 | HybridCache(GetOrCreateAsync) |
| 完整 HTTP 响应 | 使用 .CacheOutput() 进行输出缓存 |
| 频繁读取、很少写入 | 使用较长 TTL 的 HybridCache |
| 用户专属数据 | 使用带用户作用域键的 HybridCache |
| 写入时使缓存失效 | cache.RemoveAsync() 或输出缓存标签 |
| 分布式部署 | HybridCache + Redis L2 后端 |
| 单服务器部署 | 仅使用内存缓存的 HybridCache |
评论
支持 Markdown