当我们的团队注意到 API 响应时间从毫秒级上升到秒级时,我们知道出了问题。但我们没想到的是,原本旨在提高性能的缓存策略实际上才是问题的根源。接下来,让我们一起探索、调试并最终解决 .NET 微服务架构中的复杂缓存问题。
我们的系统为全球支付平台处理金融交易(正如我在之前的文章中提到的那样),在高峰时段每分钟处理大约 50,000 个请求。
架构包括:
缓存层最初的设计是为了减少数据库负载并提高响应时间。每个服务都维护自己的缓存,结合了以下内容:
// 初始缓存实现
public class CacheService
{
private readonly IDistributedCache _distributedCache;
private readonly IMemoryCache _memoryCache;
public async Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null)
{
// 首先检查内存缓存
if (_memoryCache.TryGetValue(key, out T value))
return value;
// 然后检查分布式缓存
var cached = await _distributedCache.GetAsync(key);
if (cached != null)
{
value = JsonSerializer.Deserialize<T>(cached);
// 设置到内存缓存
_memoryCache.Set(key, value, expiration ?? TimeSpan.FromMinutes(5));
return value;
}
// 如果未找到,生成值
value = await factory();
// 存储到两个缓存中
await _distributedCache.SetAsync(
key,
JsonSerializer.SerializeToUtf8Bytes(value),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiration
}
);
_memoryCache.Set(key, value, expiration ?? TimeSpan.FromMinutes(5));
return value;
}
}
我们的第一个主要问题出现在高峰时段。当缓存项过期时,多个并发请求会触发相同的昂贵数据库查询。这种“缓存雪崩”效应在服务之间级联:
public async Task<PaymentDetails> GetPaymentDetails(string paymentId)
{
return await _cacheService.GetOrSetAsync(
$"payment:{paymentId}",
async () => await _dbContext.Payments
.Include(p => p.Customer)
.Include(p => p.TransactionHistory)
.FirstOrDefaultAsync(p => p.Id == paymentId),
TimeSpan.FromMinutes(15));
}
当此缓存项过期时,数百个并发请求会同时命中数据库,导致 CPU 飙升和响应时间增加。
我们的内存使用显示出一种令人担忧的模式 → 即使配置了缓存过期,内存使用量仍会随着时间的推移而增长。真正的罪魁祸首是什么?我们在内存中存储了大型对象,但没有适当的大小限制:
// 初始实现中的内存泄漏
public class PaymentDetails
{
public string Id { get; set; }
public Customer Customer { get; set; }
public List<Transaction> TransactionHistory { get; set; } // 无界列表
public byte[] Receipt { get; set; } // 大型二进制数据
}
由于多个服务管理自己的缓存,我们面临数据一致性问题。当一个服务中的支付信息更新时,其他服务中的相关缓存并不总是正确失效:
// 不一致的缓存失效
public async Task UpdatePayment(Payment payment)
{
await _dbContext.Payments.UpdateAsync(payment);
await _cacheService.RemoveAsync($"payment:{payment.Id}");
// 其他服务的缓存仍然有旧数据
}
为了防止缓存雪崩,我们实现了滑动窗口锁模式:
public class SlidingWindowCache<T>
{
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
private readonly IDistributedCache _cache;
private const int StaleBufferSeconds = 30;
public async Task<T> GetOrSetAsync(string key, Func<Task<T>> factory, TimeSpan expiration)
{
var value = await TryGetValue(key);
if (value != null) return value;
try
{
await _lock.WaitAsync();
// 获取锁后再次检查
value = await TryGetValue(key);
if (value != null) return value;
// 生成新值
value = await factory();
// 存储并添加过期缓冲
await _cache.SetAsync(
key,
JsonSerializer.SerializeToUtf8Bytes(new CacheEntry<T>
{
Value = value,
ExpiresAt = DateTime.UtcNow.Add(expiration),
IsStale = false
}),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiration.Add(TimeSpan.FromSeconds(StaleBufferSeconds))
});
return value;
}
finally
{
_lock.Release();
}
}
private class CacheEntry<TValue>
{
public TValue Value { get; set; }
public DateTime ExpiresAt { get; set; }
public bool IsStale { get; set; }
}
我们实现了具有适当淘汰策略的大小感知缓存:
public class SizeAwareCache
{
private readonly MemoryCache _cache;
private long _currentSize;
private readonly long _sizeLimit;
public SizeAwareCache(long sizeLimit)
{
_sizeLimit = sizeLimit;
_cache = new MemoryCache(new MemoryCacheOptions
{
SizeLimit = sizeLimit,
ExpirationScanFrequency = TimeSpan.FromMinutes(5)
});
}
public void Set<T>(string key, T value, TimeSpan expiration)
{
var size = CalculateSize(value);
var entryOptions = new MemoryCacheEntryOptions
{
Size = size,
AbsoluteExpirationRelativeToNow = expiration,
Priority = CacheItemPriority.Normal
};
_cache.Set(key, value, entryOptions);
}
private long CalculateSize<T>(T value)
{
// 根据对象类型实现大小计算
// 对于字符串:返回 UTF8 字节
// 对于对象:使用序列化大小
// 添加缓存条目元数据的开销
}
}
我们使用 Azure Service Bus 实现了发布/订阅系统,用于协调缓存失效:
public class DistributedCacheInvalidator
{
private readonly IServiceBusClient _serviceBus;
private readonly IDistributedCache _cache;
private readonly string _topicName = "cache-invalidation";
public async Task InvalidateAsync(string key, string reason)
{
var message = new InvalidationMessage
{
Key = key,
Timestamp = DateTime.UtcNow,
Reason = reason
};
await _serviceBus.SendMessageAsync(_topicName, message);
}
public async Task HandleInvalidationMessage(InvalidationMessage message)
{
await _cache.RemoveAsync(message.Key);
// 记录失效原因和时间戳
}
}
我们实现了详细的指标收集:
public class CacheMetrics
{
private readonly IMetricClient _metrics;
public async Task TrackCacheOperation(string cacheType, string operation, string key, long duration)
{
_metrics.TrackMetric(new MetricTelemetry
{
Name = $"Cache.{cacheType}.{operation}",
Value = duration,
Properties = new Dictionary<string, string>
{
["Key"] = key,
["Success"] = "true"
}
});
}
}
我们添加了内存监控和警报:
public class CacheHealthCheck : IHealthCheck
{
private readonly SizeAwareCache _cache;
private readonly ILogger<CacheHealthCheck> _logger;
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context)
{
var metrics = _cache.GetMetrics();
if (metrics.CurrentSize > metrics.SizeLimit * 0.9)
{
_logger.LogWarning("缓存大小接近限制: {CurrentSize}/{SizeLimit}",
metrics.CurrentSize, metrics.SizeLimit);
return HealthCheckResult.Degraded();
}
return HealthCheckResult.Healthy();
}
}
在实施这些更改后,我们看到了显著的改进:
缓存条目大小
过期策略
失效模式
监控
错误处理
缓存是提高应用程序性能的强大工具,但它需要仔细考虑实现细节、监控和维护。我们从响应时间缓慢到高性能系统的旅程教会了我们以下重要经验:
请记住,缓存不是“设置后就不管”的解决方案。它需要持续的监控、维护,以及随着系统增长和需求变化而进行的偶尔重构。