💾 软件架构中的缓存设计全解析
“There are only two hard things in Computer Science: cache
invalidation and naming things.” — Phil Karlton
1. 缓存概述
1.1 什么是缓存?
缓存(Cache)
是一种将数据存储在更快访问层的技术,用于减少数据访问延迟、降低后端负载、提升系统吞吐量。
缓存 = 用空间换时间
CPU 寄存器 (~1ns) → 内存 (~100ns) → 磁盘 (~100μs)
访问速度差异可达 10万倍!
1.2 多级缓存架构
客户端 → CDN → 网关层 → 应用层 → 分布式缓存(Redis) → 本地缓存 → 数据库
1.3 .NET 缓存技术栈
| 本地缓存 |
IMemoryCache、LazyCache |
| 分布式缓存 |
IDistributedCache、StackExchange.Redis |
| 缓存抽象 |
Microsoft.Extensions.Caching |
| 混合缓存 |
HybridCache(.NET 9)、FusionCache |
2. 常见缓存问题与解决方案
2.1 缓存穿透 (Cache
Penetration)
问题:查询不存在的数据,缓存永远无法命中,请求直接打到数据库。
解决方案一:缓存空对象
public class UserService
{
private readonly IDistributedCache _cache;
private readonly IUserRepository _repository;
public async Task<User?> GetUserAsync(int userId)
{
var cacheKey = $"user:{userId}";
var cached = await _cache.GetStringAsync(cacheKey);
if (cached != null)
{
// 空对象标记
return cached == "NULL" ? null : JsonSerializer.Deserialize<User>(cached);
}
var user = await _repository.GetByIdAsync(userId);
if (user != null)
{
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(user),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) });
}
else
{
// 缓存空对象,短过期时间
await _cache.SetStringAsync(cacheKey, "NULL",
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) });
}
return user;
}
}
解决方案二:布隆过滤器
// 使用 BloomFilter.NetCore 包
public class UserServiceWithBloom
{
private readonly Filter<int> _bloomFilter;
private readonly IDistributedCache _cache;
public UserServiceWithBloom()
{
// 初始化布隆过滤器,预加载所有有效ID
_bloomFilter = FilterBuilder.Build<int>(10000000, 0.001);
}
public async Task<User?> GetUserAsync(int userId)
{
// 布隆过滤器检查:不存在则一定不存在
if (!_bloomFilter.Contains(userId))
return null;
// 可能存在,继续查询缓存和数据库...
return await GetFromCacheOrDbAsync(userId);
}
}
2.2 缓存击穿 (Cache Breakdown)
问题:热点 Key
突然过期,大量并发请求直接打到数据库。
解决方案一:分布式锁
public class HotDataService
{
private readonly IDistributedCache _cache;
private readonly IConnectionMultiplexer _redis;
public async Task<Product?> GetHotProductAsync(int productId)
{
var cacheKey = $"product:{productId}";
var cached = await _cache.GetStringAsync(cacheKey);
if (cached != null)
return JsonSerializer.Deserialize<Product>(cached);
var lockKey = $"lock:{cacheKey}";
var db = _redis.GetDatabase();
// 尝试获取分布式锁
if (await db.StringSetAsync(lockKey, "1", TimeSpan.FromSeconds(10), When.NotExists))
{
try
{
// 双重检查
cached = await _cache.GetStringAsync(cacheKey);
if (cached != null)
return JsonSerializer.Deserialize<Product>(cached);
var product = await _repository.GetByIdAsync(productId);
if (product != null)
{
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(product),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) });
}
return product;
}
finally
{
await db.KeyDeleteAsync(lockKey);
}
}
// 未获取到锁,等待后重试
await Task.Delay(100);
return await GetHotProductAsync(productId);
}
}
解决方案二:使用
FusionCache(推荐)
// FusionCache 内置防击穿机制
builder.Services.AddFusionCache()
.WithDefaultEntryOptions(new FusionCacheEntryOptions
{
Duration = TimeSpan.FromHours(1),
// 启用故障安全机制:即使缓存过期,仍返回旧数据
IsFailSafeEnabled = true,
FailSafeMaxDuration = TimeSpan.FromHours(24),
// 后台刷新
FactorySoftTimeout = TimeSpan.FromMilliseconds(100)
})
.WithDistributedCache(
new RedisCache(new RedisCacheOptions { Configuration = "localhost:6379" })
);
// 使用
public class ProductService
{
private readonly IFusionCache _cache;
public async Task<Product?> GetProductAsync(int id)
{
return await _cache.GetOrSetAsync(
$"product:{id}",
async _ => await _repository.GetByIdAsync(id),
TimeSpan.FromHours(1)
);
}
}
2.3 缓存雪崩 (Cache Avalanche)
问题:大量 Key
同时过期,或缓存服务宕机,导致请求全部涌向数据库。
解决方案一:随机过期时间
public static class CacheExtensions
{
private static readonly Random _random = new();
public static async Task SetWithJitterAsync<T>(
this IDistributedCache cache,
string key,
T value,
TimeSpan baseDuration)
{
// 在基础 TTL 上增加随机偏移(0-10分钟)
var jitter = TimeSpan.FromMinutes(_random.Next(0, 10));
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = baseDuration + jitter
};
await cache.SetStringAsync(key, JsonSerializer.Serialize(value), options);
}
}
解决方案二:多级缓存
public class MultiLevelCache
{
private readonly IMemoryCache _l1Cache; // 本地缓存
private readonly IDistributedCache _l2Cache; // Redis
public async Task<T?> GetAsync<T>(string key) where T : class
{
// L1: 本地缓存
if (_l1Cache.TryGetValue(key, out T? value))
return value;
// L2: Redis
try
{
var cached = await _l2Cache.GetStringAsync(key);
if (cached != null)
{
value = JsonSerializer.Deserialize<T>(cached);
// 回填 L1(短过期)
_l1Cache.Set(key, value, TimeSpan.FromSeconds(30));
return value;
}
}
catch (RedisConnectionException)
{
// Redis 不可用,降级
}
return null;
}
}
解决方案三:熔断降级(Polly)
// 使用 Polly 实现熔断
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
});
builder.Services.AddResiliencePipeline("cache-pipeline", builder =>
{
builder
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
SamplingDuration = TimeSpan.FromSeconds(10),
MinimumThroughput = 10,
BreakDuration = TimeSpan.FromSeconds(30)
})
.AddTimeout(TimeSpan.FromSeconds(1));
});
2.4 三大问题对比
| 穿透 |
查询不存在的数据 |
缓存空对象、布隆过滤器 |
| 击穿 |
热点Key过期 |
分布式锁、FusionCache |
| 雪崩 |
大量Key同时过期 |
随机TTL、多级缓存、熔断 |
3. 缓存更新策略
3.1 Cache Aside(最常用)
public class CacheAsideService
{
public async Task<T?> GetAsync<T>(string key, Func<Task<T?>> factory) where T : class
{
// 1. 先查缓存
var cached = await _cache.GetStringAsync(key);
if (cached != null)
return JsonSerializer.Deserialize<T>(cached);
// 2. 查数据库
var data = await factory();
// 3. 回填缓存
if (data != null)
{
await _cache.SetStringAsync(key, JsonSerializer.Serialize(data),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) });
}
return data;
}
public async Task UpdateAsync<T>(string key, T value, Func<Task> updateDb)
{
// 1. 先更新数据库
await updateDb();
// 2. 再删除缓存(而非更新)
await _cache.RemoveAsync(key);
}
}
3.2 延迟双删
public async Task UpdateWithDelayDeleteAsync(string key, Product product)
{
// 1. 先删除缓存
await _cache.RemoveAsync(key);
// 2. 更新数据库
await _repository.UpdateAsync(product);
// 3. 延迟再删一次
_ = Task.Run(async () =>
{
await Task.Delay(500);
await _cache.RemoveAsync(key);
});
}
3.3 基于消息队列的最终一致性
// 使用 MassTransit + RabbitMQ
public class ProductUpdatedConsumer : IConsumer<ProductUpdated>
{
private readonly IDistributedCache _cache;
public async Task Consume(ConsumeContext<ProductUpdated> context)
{
var key = $"product:{context.Message.ProductId}";
await _cache.RemoveAsync(key);
}
}
// 发布事件
public async Task UpdateProductAsync(Product product)
{
await _repository.UpdateAsync(product);
await _publishEndpoint.Publish(new ProductUpdated { ProductId = product.Id });
}
4. 使用缓存注意事项
4.1 Key 设计规范
public static class CacheKeys
{
// 命名规范:业务前缀:对象类型:唯一标识[:版本号]
public static string User(int id) => $"user:profile:{id}";
public static string Product(int id) => $"product:detail:{id}";
public static string Order(string orderId) => $"order:info:{orderId}";
public static string List(string category, int page) => $"list:{category}:page:{page}";
}
注意事项: - ✅ 使用冒号分隔层级 - ✅ Key 长度 <
200 字节 - ❌ 避免过长的 Key - ❌ 避免包含用户输入(注入风险)
4.2 大 Key 处理
// 拆分大列表
public class BigListCache
{
private const int PageSize = 100;
public async Task SetLargeListAsync<T>(string key, List<T> items)
{
var pageCount = (items.Count + PageSize - 1) / PageSize;
// 存储元数据
await _cache.SetStringAsync($"{key}:meta",
JsonSerializer.Serialize(new { Total = items.Count, Pages = pageCount }));
// 分页存储
for (int i = 0; i < pageCount; i++)
{
var pageItems = items.Skip(i * PageSize).Take(PageSize).ToList();
await _cache.SetStringAsync($"{key}:page:{i}", JsonSerializer.Serialize(pageItems));
}
}
}
4.3 监控指标
| 命中率 |
> 90% |
< 80% |
| P99 延迟 |
< 10ms |
> 50ms |
| 内存使用率 |
< 80% |
> 90% |
| 连接数 |
< 80% max |
> 90% max |
4.4 淘汰策略
allkeys-lru |
淘汰最近最少使用(推荐) |
maxmemory-policy allkeys-lru |
allkeys-lfu |
淘汰最不经常使用 |
maxmemory-policy allkeys-lfu |
volatile-ttl |
优先淘汰即将过期 |
maxmemory-policy volatile-ttl |
5. .NET 最佳实践
5.1 使用 HybridCache(.NET 9+)
// .NET 9 内置的混合缓存,自动处理多级缓存
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromHours(1),
LocalCacheExpiration = TimeSpan.FromMinutes(5)
};
});
public class ProductService
{
private readonly HybridCache _cache;
public async Task<Product> GetProductAsync(int id)
{
return await _cache.GetOrCreateAsync(
$"product:{id}",
async cancel => await _repository.GetByIdAsync(id, cancel)
);
}
}
5.2 使用 FusionCache(推荐)
builder.Services.AddFusionCache()
.WithDefaultEntryOptions(new FusionCacheEntryOptions
{
Duration = TimeSpan.FromHours(1),
IsFailSafeEnabled = true,
FailSafeMaxDuration = TimeSpan.FromDays(1),
FactorySoftTimeout = TimeSpan.FromMilliseconds(100),
AllowBackgroundDistributedCacheOperations = true
})
.WithDistributedCache(new RedisCache(new RedisCacheOptions
{
Configuration = "localhost:6379"
}))
.WithSerializer(new FusionCacheSystemTextJsonSerializer());
// 自动处理:多级缓存、防击穿、故障安全、后台刷新
public class ProductService
{
private readonly IFusionCache _cache;
public async Task<Product?> GetAsync(int id) =>
await _cache.GetOrSetAsync($"product:{id}",
_ => _repository.GetByIdAsync(id));
public async Task UpdateAsync(Product product)
{
await _repository.UpdateAsync(product);
await _cache.RemoveAsync($"product:{product.Id}");
}
}
5.3 完整示例:电商商品缓存
public class ProductCacheService
{
private readonly IFusionCache _cache;
private readonly IProductRepository _repository;
private readonly ILogger<ProductCacheService> _logger;
public async Task<Product?> GetProductAsync(int productId)
{
return await _cache.GetOrSetAsync(
CacheKeys.Product(productId),
async ct =>
{
_logger.LogInformation("Cache miss for product {Id}", productId);
return await _repository.GetByIdAsync(productId, ct);
},
new FusionCacheEntryOptions
{
Duration = TimeSpan.FromHours(1),
JitterMaxDuration = TimeSpan.FromMinutes(10), // 防雪崩
IsFailSafeEnabled = true,
FailSafeMaxDuration = TimeSpan.FromHours(24)
}
);
}
public async Task UpdateProductAsync(Product product)
{
await _repository.UpdateAsync(product);
// 删除缓存并发布失效事件
await _cache.RemoveAsync(CacheKeys.Product(product.Id));
_logger.LogInformation("Product {Id} cache invalidated", product.Id);
}
public async Task<List<Product>> GetTopProductsAsync(int count)
{
return await _cache.GetOrSetAsync(
$"products:top:{count}",
_ => _repository.GetTopAsync(count),
TimeSpan.FromMinutes(10)
) ?? new List<Product>();
}
}
6. 总结
核心要点
| 缓存问题 |
穿透→空对象/布隆过滤器,击穿→分布式锁/FusionCache,雪崩→随机TTL/多级缓存 |
| 更新策略 |
Cache Aside:先更新 DB,再删除缓存 |
| 推荐方案 |
FusionCache(成熟)或 HybridCache(.NET 9+) |
| Key 设计 |
业务:类型:ID,长度适中,避免大 Key |
| 监控 |
命中率 > 90%,P99 < 10ms |
设计检查清单
参考资源