在 .NET 中利用 PostgreSQL 与 HybridCache 构建高性能分布式缓存
背景:为什么需要两级缓存
单机内存缓存(IMemoryCache)在单实例场景下表现良好,但在水平扩展时会出现以下问题:
- 每个节点独立维护缓存,导致缓存命中率低。
- 节点重启后缓存全部失效,产生"缓存雪崩"。
- 多个实例同时回源,数据库瞬时压力激增。
解决思路:引入分布式缓存,但纯分布式缓存(如 Redis)又会带来额外的运维与成本。若系统已深度依赖 PostgreSQL,可直接复用其作为共享缓存存储,降低基础设施复杂度。
两级缓存架构
HybridCache 提供"本地内存 + 分布式存储"两层结构:
- L1(本地内存):进程内高速访问,纳秒级延迟。
- L2(PostgreSQL 缓存表):跨节点共享,毫秒级延迟,持久化。
访问顺序:
请求 → 本地内存命中 → 直接返回
└─ 未命中 → PostgreSQL 命中 → 回填本地内存 → 返回
└─ 未命中 → 回源 → 写入两级缓存 → 返回
快速上手
1. 安装 NuGet 包
dotnet add package Microsoft.Extensions.Caching.Hybrid
dotnet add package Microsoft.Extensions.Caching.Postgres
2. 配置服务
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDistributedPostgresCache(opts =>
{
opts.ConnectionString = builder.Configuration.GetConnectionString("PgCache");
opts.SchemaName = "public";
opts.TableName = "cache_store";
opts.CreateIfNotExists = true;
});
builder.Services.AddHybridCache();
3. 定义缓存实体与键
public record ProductCatalog(Guid TenantId, IReadOnlyList<ProductItem> Items);
private static string CacheKey(Guid tenantId) => $"tenant:{tenantId}:catalog:v2";
4. 业务层封装
public sealed class CatalogService(
HybridCache cache,
ICatalogApiClient apiClient,
ILogger<CatalogService> logger)
{
private static readonly HybridCacheEntryOptions Options = new()
{
LocalCacheExpiration = TimeSpan.FromSeconds(15),
Expiration = TimeSpan.FromMinutes(5)
};
public async ValueTask<ProductCatalog> GetCatalogAsync(Guid tenant, CancellationToken ct = default)
{
var key = CacheKey(tenant);
return await cache.GetOrCreateAsync(
key,
async token =>
{
logger.LogInformation("回源拉取租户 {Tenant} 的商品目录", tenant);
return await apiClient.FetchCatalogAsync(tenant, token);
},
Options,
ct);
}
}
关键设计要点
缓存键设计
- 包含作用域、版本号,避免串扰。
- 避免把可变参数(如时间戳、随机数)拼入键。
过期策略
| 数据类型 | 本地过期 | 分布式过期 |
|---|---|---|
| 静态配置 | 5 min | 1 h |
| 业务字典 | 30 s | 10 min |
| 实时库存 | 5 s | 30 s |
缓存雪崩防护
HybridCache 内置并发合并:当多个请求同时 miss 同一 key 时,只触发一次回源,其余等待结果。
失效策略
- 被动失效:依赖 TTL。
- 主动失效:数据变更后发布事件,消费者调用
RemoveAsync(key)。 - 版本化失效:修改键后缀,如
v1→v2,实现平滑迁移。
可观测性
public sealed class CacheMetrics
{
public int HitLocal { get; set; }
public int HitDistributed { get; set; }
public int Miss { get; set; }
}
// 在回源处埋点
logger.LogInformation("CacheMiss Key={Key} Duration={ElapsedMs}ms", key, sw.ElapsedMilliseconds);
何时选用 PostgreSQL 作为分布式缓存
| 场景特征 | 推荐 |
|---|---|
| 已托管 Azure Database for PostgreSQL | ✅ |
| 延迟要求 < 1 ms | ❌ 选 Redis |
| 写吞吐极高 | ❌ 选 Redis |
| 运维团队熟悉 PostgreSQL | ✅ |
| 预算敏感,需减少组件 | ✅ |
示例:完整 Minimal API 集成
var app = builder.Build();
app.MapGet("/tenants/{tenantId:guid}/catalog",
async (Guid tenantId, CatalogService svc) =>
Results.Ok(await svc.GetCatalogAsync(tenantId)));
app.Run();
请求流程:
- 首次请求 → 回源 → 写入两级缓存 → 返回 200(~200 ms)。
- 后续 15 s 内 → 本地内存命中 → 返回 200(< 1 ms)。
- 15 s–5 min → PostgreSQL 命中 → 回填本地内存 → 返回 200(~5 ms)。
小结
通过 HybridCache + PostgreSQL,可在不引入额外缓存组件的前提下,为 .NET 应用提供:
- 本地极速访问。
- 跨节点共享。
- 缓存雪崩与并发保护。
- 与现有 PostgreSQL 运维体系无缝整合。
若系统已深度依赖 PostgreSQL,且对缓存延迟要求并非极端苛刻,这套组合提供了简单、可维护、成本友好的分布式缓存方案。