mcpField
C
caching

caching

github.com/codewithmukesh开发7912.9k 次安装12 周前更新
代码仓库

关于 caching

面向 .NET 开发者的缓存实践技能,介绍 HybridCache、输出缓存及失效策略,并强调显式过期时间和避免常见反模式。

主要功能

  1. 01HybridCache 缓存建议
  2. 02缓存失效策略
  3. 03HTTP 响应输出缓存
  4. 04TTL 与用户级缓存键

适用场景

  1. 01为频繁读取的数据添加缓存
  2. 02在数据变更时清除缓存
  3. 03缓存完整 HTTP 响应

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill caching

安装后在 Claude Code 中输入/caching确认已加载。

详细介绍

缓存

核心原则

  1. HybridCache 是默认选择 — .NET 9+ 引入了 HybridCache,作为统一的缓存抽象。它结合了内存(L1)和分布式(L2)缓存,并提供缓存击穿保护。参见 ADR-004。
  2. 缓存读取,而非写入 — 缓存 GET 操作。在发生变更时失效。绝不缓存 POST/PUT/DELETE 响应。
  3. 对完整响应使用输出缓存 — 当整个 HTTP 响应都可以缓存时(公共 API、静态数据),使用输出缓存中间件。
  4. 设置明确的 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