caching
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCaching
缓存
Core Principles
核心原则
- HybridCache is the default — .NET 9+ introduced as the unified caching abstraction. It combines in-memory (L1) and distributed (L2) caching with stampede protection. See ADR-004.
HybridCache - Cache reads, not writes — Cache GET operations. Invalidate on mutations. Never cache POST/PUT/DELETE responses.
- Output caching for entire responses — When the full HTTP response can be cached (public APIs, static data), use output caching middleware.
- Set explicit TTLs — Every cached item needs an expiration. No unbounded caches.
- HybridCache为默认选择 — .NET 9及以上版本引入作为统一的缓存抽象层。它结合了内存缓存(L1)和分布式缓存(L2),并具备缓存击穿防护功能。详见ADR-004。
HybridCache - 缓存读取操作,而非写入操作 — 缓存GET请求。在数据变更时失效缓存。绝不要缓存POST/PUT/DELETE请求的响应。
- 输出缓存用于完整响应 — 当整个HTTP响应可被缓存时(如公开API、静态数据),使用输出缓存中间件。
- 设置明确的TTL(过期时间) — 每个缓存项都需要设置过期时间。禁止使用无期限缓存。
Patterns
模式
HybridCache (Recommended Default)
HybridCache(推荐默认方案)
csharp
// 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");
});csharp
// 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);
}
}
}csharp
// Program.cs
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(5),
LocalCacheExpiration = TimeSpan.FromMinutes(2)
};
});
// 可选:添加Redis作为L2分布式缓存
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});csharp
// 在处理器中使用
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);
}
}
}Cache Invalidation
缓存失效
csharp
// 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();
}
}
}csharp
// 在数据变更时失效缓存
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);
// 失效缓存条目
await cache.RemoveAsync($"products:{command.Id}", ct);
return Result.Success();
}
}
}Output Caching (Full Response Caching)
输出缓存(完整响应缓存)
csharp
// 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();
});csharp
// Program.cs
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(b => b.NoCache()); // 默认不缓存
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();
// 应用到端点
group.MapGet("/", ListProducts).CacheOutput("ProductList");
group.MapGet("/{id:guid}", GetProduct).CacheOutput("ProductById");
// 在数据变更时按标签失效缓存
group.MapPut("/{id:guid}", async (Guid id, UpdateProductRequest request,
IOutputCacheStore store, CancellationToken ct) =>
{
// ... 更新逻辑 ...
await store.EvictByTagAsync("products", ct);
return TypedResults.NoContent();
});Cache-Aside Pattern (Legacy)
Cache-Aside模式(遗留方案)
Prefer HybridCache for all new code. Manualcache-aside lacks stampede protection, requires manual serialization, and has no L1/L2 layering. Use only when integrating with existing code that already usesIDistributedCachedirectly.IDistributedCache
优先使用HybridCache开发所有新代码。手动使用的Cache-Aside模式缺乏缓存击穿防护,需要手动序列化,且没有L1/L2分层。仅在与已直接使用IDistributedCache的现有代码集成时使用。IDistributedCache
Anti-patterns
反模式
Don't Cache Without Expiration
不要使用无过期时间的缓存
csharp
// 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)
});csharp
// 错误示例 — 缓存永久存在,必然导致数据过期
await cache.SetStringAsync(key, value);
// 正确示例 — 始终设置TTL
await cache.SetStringAsync(key, value, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});Don't Cache Mutable User-Specific Data
不要缓存可变的用户特定数据
csharp
// 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}", ...);csharp
// 错误示例 — 使用全局键缓存用户购物车
await cache.GetOrCreateAsync("shopping-cart", ...);
// 正确示例 — 在键中包含用户ID
await cache.GetOrCreateAsync($"shopping-cart:{userId}", ...);Don't Build Your Own Stampede Protection
不要自行实现缓存击穿防护
csharp
// 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);csharp
// 错误示例 — 使用手动锁防止缓存击穿
private static readonly SemaphoreSlim Lock = new(1, 1);
await Lock.WaitAsync();
try { /* 检查缓存,若缺失则填充 */ }
finally { Lock.Release(); }
// 正确示例 — HybridCache内置缓存击穿防护
await hybridCache.GetOrCreateAsync(key, factory);Decision Guide
决策指南
| Scenario | Recommendation |
|---|---|
| General data caching | HybridCache ( |
| Full HTTP response | Output caching with |
| Frequently read, rarely written | HybridCache with longer TTL |
| User-specific data | HybridCache with user-scoped key |
| Cache invalidation on write | |
| Distributed deployment | HybridCache + Redis L2 backend |
| Single-server deployment | HybridCache with in-memory only |
| 场景 | 推荐方案 |
|---|---|
| 通用数据缓存 | HybridCache( |
| 完整HTTP响应 | 使用 |
| 频繁读取、极少写入 | 带有较长TTL的HybridCache |
| 用户特定数据 | 带有用户范围键的HybridCache |
| 写入时失效缓存 | |
| 分布式部署 | HybridCache + Redis L2后端 |
| 单服务器部署 | 仅使用内存缓存的HybridCache |