caching

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Caching

缓存

Core Principles

核心原则

  1. HybridCache is the default — .NET 9+ introduced
    HybridCache
    as the unified caching abstraction. It combines in-memory (L1) and distributed (L2) caching with stampede protection. See ADR-004.
  2. Cache reads, not writes — Cache GET operations. Invalidate on mutations. Never cache POST/PUT/DELETE responses.
  3. Output caching for entire responses — When the full HTTP response can be cached (public APIs, static data), use output caching middleware.
  4. Set explicit TTLs — Every cached item needs an expiration. No unbounded caches.
  1. HybridCache为默认选择 — .NET 9及以上版本引入
    HybridCache
    作为统一的缓存抽象层。它结合了内存缓存(L1)和分布式缓存(L2),并具备缓存击穿防护功能。详见ADR-004。
  2. 缓存读取操作,而非写入操作 — 缓存GET请求。在数据变更时失效缓存。绝不要缓存POST/PUT/DELETE请求的响应。
  3. 输出缓存用于完整响应 — 当整个HTTP响应可被缓存时(如公开API、静态数据),使用输出缓存中间件。
  4. 设置明确的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. Manual
IDistributedCache
cache-aside lacks stampede protection, requires manual serialization, and has no L1/L2 layering. Use only when integrating with existing code that already uses
IDistributedCache
directly.
优先使用HybridCache开发所有新代码。手动使用
IDistributedCache
的Cache-Aside模式缺乏缓存击穿防护,需要手动序列化,且没有L1/L2分层。仅在与已直接使用
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

决策指南

ScenarioRecommendation
General data cachingHybridCache (
GetOrCreateAsync
)
Full HTTP responseOutput caching with
.CacheOutput()
Frequently read, rarely writtenHybridCache with longer TTL
User-specific dataHybridCache with user-scoped key
Cache invalidation on write
cache.RemoveAsync()
or output cache tags
Distributed deploymentHybridCache + Redis L2 backend
Single-server deploymentHybridCache with in-memory only
场景推荐方案
通用数据缓存HybridCache(
GetOrCreateAsync
完整HTTP响应使用
.CacheOutput()
的输出缓存
频繁读取、极少写入带有较长TTL的HybridCache
用户特定数据带有用户范围键的HybridCache
写入时失效缓存
cache.RemoveAsync()
或输出缓存标签
分布式部署HybridCache + Redis L2后端
单服务器部署仅使用内存缓存的HybridCache