You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 19:55:27