LazyCache并发请求返回相同错误结果问题排查
LazyCache并发场景下失败请求的处理问题
核心问题根源
LazyCache的默认实现依赖.NET的Lazy<T>类型,而Lazy<T>有一个关键特性:如果初始化工厂方法抛出异常,这个异常会被缓存,后续所有访问该Lazy<T>实例的请求都会直接抛出同一异常,不会重新执行工厂方法。
在你的并发测试场景中,多个请求同时触发缓存加载时,它们会共享同一个Lazy<T>实例。一旦第一个请求的底层调用失败,这个Lazy<T>就会把异常缓存下来,后续所有请求都会直接拿到这个异常,而不会重新尝试调用底层服务——这就是为什么你看到4或5次请求都返回错误的原因。
能否实现你要的预期效果?
可以,但不能用LazyCache的默认行为,需要通过自定义逻辑规避Lazy<T>的异常缓存特性,确保失败的加载请求不会污染缓存,后续请求能重新尝试加载。
是否需要手动加锁?
不需要全局锁,但需要针对单个缓存Key的并发加载进行控制,或者在请求失败时主动清理缓存项,避免异常状态被保留。
具体解决方法
方法1:用SemaphoreSlim控制单Key并发加载+双重检查缓存
这种方式能确保同一时间只有一个线程尝试加载指定Key的缓存,失败时不缓存结果,后续请求可以重新尝试:
private readonly IAppCache _cache; private readonly IDataProvider _dataProvider; private readonly ConcurrentDictionary<string, SemaphoreSlim> _keyLocks = new(); public async Task<MyData> GetCachedDataAsync(string cacheKey) { // 先检查缓存,命中直接返回 var cachedData = await _cache.GetAsync<MyData>(cacheKey); if (cachedData != null) return cachedData; // 获取当前Key的信号量,控制并发 var semaphore = _keyLocks.GetOrAdd(cacheKey, _ => new SemaphoreSlim(1, 1)); try { // 再次检查缓存,避免等待期间其他线程已加载完成 cachedData = await _cache.GetAsync<MyData>(cacheKey); if (cachedData != null) return cachedData; // 执行底层调用 var freshData = await _dataProvider.LoadDataAsync(); // 缓存成功结果 await _cache.SetAsync(cacheKey, freshData, new CacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) }); return freshData; } catch { // 失败时不缓存,直接抛出异常 throw; } finally { semaphore.Release(); // 可选:如果Key不再被使用,可以移除信号量避免内存占用 _keyLocks.TryRemove(cacheKey, out _); } }
方法2:失败时主动清理缓存项
如果不想用信号量,可以在GetOrAddAsync抛出异常后,主动移除对应的缓存Key,让后续请求重新创建加载逻辑:
public async Task<MyData> GetOrLoadDataAsync(string cacheKey) { try { return await _cache.GetOrAddAsync(cacheKey, async entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); return await _dataProvider.LoadDataAsync(); }); } catch { // 移除缓存项,避免Lazy<T>的异常状态被保留 await _cache.RemoveAsync(cacheKey); throw; } }
注意:这个方法的前提是,当工厂方法失败时,LazyCache不会缓存成功结果,但Lazy<T>实例可能仍在缓存中,所以必须调用RemoveAsync来清除这个无效的Lazy实例,让后续请求重新触发加载。
总结
- 你的预期效果是可以实现的,但必须处理
Lazy<T>的异常缓存特性。 - 不需要全局锁,但需要针对单个缓存Key的并发加载做控制,或者在失败时清理缓存。
内容的提问来源于stack exchange,提问作者Anthony
相关产品推荐
相关产品推荐

