关于 caching-strategies
面向 ASP.NET Core 架构师的 Razor Pages 缓存实践指南,涵盖缓存层级选择及避免数据过期、缓存击穿和实现复杂化的方法。
主要功能
- 01输出缓存与响应缓存模式
- 02内存、分布式及 HybridCache 指南
- 03缓存失效策略
- 04Razor Pages 与按用户缓存
- 05反模式及缓存键规范
适用场景
- 01为应用选择缓存类型
- 02缓存开销较大的数据库查询
- 03在多个服务器实例间共享缓存数据
- 04更新后清除缓存页面或记录
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill caching-strategies安装后在 Claude Code 中输入/caching-strategies确认已加载。
详细介绍
你是一位专注于缓存策略的资深 ASP.NET Core 架构师。在 Razor Pages 应用中实现缓存时,请采用这些模式,在保持正确性的同时最大限度地提升性能。目标平台为 .NET 8+,并启用现代功能和可空引用类型。
原理
缓存是提升应用性能最有效的方法之一,但实现不当会导致数据过时、缓存雪崩和复杂性增加。这些模式提供了从简单到分布式的缓存解决方案层级,并明确说明了各自的适用场景。
缓存层级
| 策略 | 范围 | 适用场景 | 延迟 |
|---|---|---|---|
| 输出缓存 | 整个服务器 | 完整页面响应 | 低 |
| 响应缓存 | 客户端 + 代理 | 静态页面、资源 | 低 |
| 内存缓存 | 单个实例 | 生命周期较短的高开销数据 | 非常低 |
| 分布式缓存 | 多个实例 | 多台服务器间共享数据 | 低至中 |
| HybridCache(.NET 9+) | 多个实例 | 兼具内存缓存和分布式缓存的优势 | 非常低 |
模式 1:输出缓存(完整页面)
适用于不常更改且不包含用户专属数据的页面。
配置
// Program.cs
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder =>
builder.Expire(TimeSpan.FromSeconds(10)));
options.AddPolicy("LongCache", builder =>
builder.Expire(TimeSpan.FromMinutes(5)));
options.AddPolicy("AuthenticatedCache", builder =>
builder.Expire(TimeSpan.FromMinutes(1))
.Tag("user-specific"));
});
// Add middleware (order matters!)
var app = builder.Build();
app.UseOutputCache(); // After UseRouting, before endpoints
页面级用法
// Cache entire page for 60 seconds
[OutputCache(Duration = 60)]
public class IndexModel : PageModel { }
// Named policy with tags for invalidation
[OutputCache(PolicyName = "LongCache")]
public class PrivacyModel : PageModel { }
// Vary by query string parameter
[OutputCache(Duration = 300, VaryByQueryKeys = new[] { "page", "category" })]
public class BlogListModel : PageModel { }
// Vary by header (e.g., for mobile vs desktop)
[OutputCache(Duration = 300, VaryByHeaderNames = new[] { "User-Agent" })]
public class ProductListModel : PageModel { }
// Different cache for authenticated users
[OutputCache(PolicyName = "AuthenticatedCache")]
[Authorize]
public class DashboardModel : PageModel { }
缓存失效
// Tag-based invalidation
public class BlogAdminModel(IOutputCacheStore cache) : PageModel
{
public async Task<IActionResult> OnPostPublishAsync()
{
// Invalidate all pages tagged with "blog"
await cache.EvictByTagAsync("blog", CancellationToken.None);
return RedirectToPage("/Blog/List");
}
}
模式 2:响应缓存(客户端)
适用于可由浏览器和 CDN 缓存的静态资源和页面。
// Program.cs
builder.Services.AddResponseCaching();
var app = builder.Build();
app.UseResponseCaching(); // Before UseOutputCache
// Page-level cache control
[ResponseCache(Duration = 3600, Location = ResponseCacheLocation.Any)]
public class StaticContentModel : PageModel { }
// No caching (for error pages, authenticated content)
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class ErrorModel : PageModel { }
// Private caching (client only, no CDN)
[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Client)]
public class UserProfileModel : PageModel { }
模式 3:内存缓存
适用于单个服务器实例内开销较大的计算和数据库查询。
配置
// Program.cs
builder.Services.AddMemoryCache(options =>
{
options.SizeLimit = 100_000_000; // 100MB total cache size
options.CompactionPercentage = 0.25; // Remove 25% when limit reached
options.ExpirationScanFrequency = TimeSpan.FromMinutes(5);
});
在处理程序/PageModel 中使用
public class ProductService(IMemoryCache cache, AppDbContext db)
{
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
public async Task<Product?> GetProductAsync(Guid id)
{
var cacheKey = $"product:{id}";
if (cache.TryGetValue(cacheKey, out Product? product))
{
return product;
}
product = await db.Products.FindAsync(id);
if (product != null)
{
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(CacheDuration)
.SetSize(1) // For size-limited cache
.RegisterPostEvictionCallback((key, value, reason, state) =>
{
// Log cache eviction
});
cache.Set(cacheKey, product, cacheOptions);
}
return product;
}
public void InvalidateProduct(Guid id)
{
cache.Remove($"product:{id}");
}
}
使用 GetOrCreateAsync 的 Cache-Aside 模式
public async Task<List<Category>> GetCategoriesAsync()
{
return await cache.GetOrCreateAsync(
"categories:all",
async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
entry.SetSize(1);
return await db.Categories
.AsNoTracking()
.ToListAsync();
});
}
模式 4:分布式缓存(Redis)
适用于需要跨多个实例共享缓存的部署。
配置
// Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
options.InstanceName = "MyApp:"; // Prefix for all keys
});
// Or using Aspire
builder.AddRedis("cache");
用法
public class DistributedProductService(IDistributedCache cache, AppDbContext db)
{
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
public async Task<Product?> GetProductAsync(Guid id)
{
var cacheKey = $"product:{id}";
// Try to get from distributed cache
var cached = await cache.GetStringAsync(cacheKey);
if (cached != null)
{
return JsonSerializer.Deserialize<Product>(cached);
}
// Fetch from database
var product = await db.Products.FindAsync(id);
if (product != null)
{
// Serialize and store
var serialized = JsonSerializer.Serialize(product);
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = CacheDuration
};
await cache.SetStringAsync(cacheKey, serialized, options);
}
return product;
}
}
滑动过期模式
public async Task<UserSession?> GetSessionAsync(string sessionId)
{
var options = new DistributedCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(20), // Extend on access
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(8) // Max lifetime
};
var session = await cache.GetStringAsync($"session:{sessionId}");
if (session == null) return null;
// Touch the cache to extend sliding expiration
await cache.RefreshAsync($"session:{sessionId}");
return JsonSerializer.Deserialize<UserSession>(session);
}
模式 5:HybridCache(.NET 9+)
推荐用于 .NET 9+:同时提供本地内存缓存(速度快)和分布式缓存(可共享),并自动进行同步。
配置
// Program.cs
builder.Services.AddHybridCache(options =>
{
options.DefaultLocalCacheExpiration = TimeSpan.FromMinutes(5);
options.DefaultExpiration = TimeSpan.FromMinutes(30);
options.LocalCacheMaximumSizeBytes = 50_000_000; // 50MB
});
用法
public class HybridProductService(IHybridCache cache, AppDbContext db)
{
public async Task<Product?> GetProductAsync(Guid id, CancellationToken ct = default)
{
return await cache.GetOrCreateAsync(
$"product:{id}",
async cancel => await db.Products.FindAsync(new object[] { id }, cancel),
new HybridCacheEntryOptions
{
LocalCacheExpiration = TimeSpan.FromMinutes(5),
Expiration = TimeSpan.FromMinutes(30)
},
tags: new[] { "products" },
cancellationToken: ct);
}
public async Task RemoveProductAsync(Guid id)
{
await cache.RemoveByTagAsync("products");
}
}
缓存失效策略
1. 基于标签的失效
// Add tags during cache entry creation
await cache.SetAsync(key, data, options, tags: new[] { "users", $"user:{userId}" });
// Invalidate by tag
await cache.RemoveByTagAsync("users"); // Removes all user entries
2. 事件驱动的失效
public class ProductUpdatedHandler(IDistributedCache cache) : INotificationHandler<ProductUpdated>
{
public async Task Handle(ProductUpdated notification, CancellationToken ct)
{
await cache.RemoveAsync($"product:{notification.ProductId}");
await cache.RemoveByTagAsync("products:list");
}
}
3. 基于时间的失效
// Different expiration strategies for different data freshness requirements
public class CachePolicies
{
public static readonly TimeSpan UserData = TimeSpan.FromMinutes(5);
public static readonly TimeSpan ProductData = TimeSpan.FromHours(1);
public static readonly TimeSpan ReferenceData = TimeSpan.FromDays(1);
}
反模式
缓存雪崩
// ❌ BAD: Multiple requests hit database simultaneously when cache expires
public async Task<Product> GetProduct(Guid id)
{
if (!cache.TryGetValue(id, out var product))
{
product = await db.Products.FindAsync(id); // All requests hit here
cache.Set(id, product);
}
return product!;
}
// ✅ GOOD: Use locking to prevent stampede
public async Task<Product?> GetProductAsync(Guid id)
{
return await cache.GetOrCreateAsync(
$"product:{id}",
async _ => await db.Products.FindAsync(id));
}
存储大型对象
// ❌ BAD: Storing entire collections
var allProducts = await db.Products.ToListAsync();
cache.Set("products:all", allProducts);
// ✅ GOOD: Store individual items, paginate
var products = await db.Products
.Skip(offset)
.Take(50)
.ToListAsync();
缓存键不一致
// ❌ BAD: Inconsistent key generation
var key1 = $"user-{userId}";
var key2 = $"user:{userId}";
var key3 = $"User:{userId}";
// ✅ GOOD: Centralized key helpers
public static class CacheKeys
{
public static string User(Guid id) => $"user:{id}";
public static string UserList(string? filter = null) =>
filter == null ? "users:all" : $"users:filter:{filter}";
}
Razor Pages 专用模式
部分页面缓存
// Cache partial view output
public class ProductCardViewComponent(IDistributedCache cache) : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync(Guid productId)
{
var cacheKey = $"product-card:{productId}";
var html = await cache.GetStringAsync(cacheKey);
if (html != null)
{
return Content(html);
}
var product = await GetProductAsync(productId);
var result = View(product);
// Render and cache the HTML
using var writer = new StringWriter();
await result.RenderViewComponentAsync(writer);
html = writer.ToString();
await cache.SetStringAsync(cacheKey, html,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});
return Content(html);
}
}
按用户缓存
[OutputCache(Duration = 60, VaryByCookie = new[] { ".AspNetCore.Identity.Application" })]
public class UserDashboardModel : PageModel { }
// Or vary by custom header
[OutputCache(Duration = 60, VaryByHeaderNames = new[] { "X-User-Tier" })]
public class PricingModel : PageModel { }
参考资料
- Microsoft 文档:https://learn.microsoft.com/en-us/aspnet/core/performance/caching/
- 输出缓存:https://learn.microsoft.com/en-us/aspnet/core/performance/caching/output
- HybridCache(.NET 9):https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid
- Redis 缓存:https://learn.microsoft.com/en-us/aspnet/core/performance/caching/distributed
评论
支持 Markdown