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

多线程环境下耗时操作的异步懒加载失效问题排查

问题分析与解决方案

你的核心问题是并发场景下同一缓存键对应的耗时操作被多次执行,原代码用Lazy<Task<T>>结合ConcurrentDictionary的思路方向是对的,但存在几个细节问题导致重复执行:

  1. Lazy<T>的异常重试机制:默认的Lazy<T>模式下,如果工厂方法(也就是你的异步操作)抛出异常,后续调用Lazy.Value会重新执行工厂方法,导致重复调用耗时操作。
  2. 不必要的Task.Run包装:Utils.GetProductsList本身是异步方法,用Task.Run包裹会额外占用线程池资源,虽然不是重复执行的直接原因,但属于冗余代码。
  3. 缺乏缓存过期策略:你的代码是永久缓存,如果业务需要一天过期,原代码没有实现,可能导致你误以为是重复执行(但你说实例没重启,所以更可能是前两个原因)。

方案一:改进ConcurrentDictionary实现(无过期)

直接用ConcurrentDictionary存储Task<T>,利用GetOrAdd的原子性确保同一键仅创建一次异步任务,避免Lazy<T>的潜在问题:

private static readonly ConcurrentDictionary<Guid, Task<List<ProductsList>>> _cache = 
    new ConcurrentDictionary<Guid, Task<List<ProductsList>>>();

public async Task<List<ProductsList>> Get(AnalysisParams aParams, string refUrlApi)
{
    // 原子性获取或创建任务,同一Project_ID只会执行一次工厂函数
    var task = _cache.GetOrAdd(aParams.Project_ID, async pid =>
    {
        // 这里用pid而非直接捕获aParams,避免闭包捕获导致的潜在问题
        return await Utils.GetProductsList(aParams, refUrlApi);
    });

    // 所有请求等待同一个任务完成
    return await task;
}

这个方案中,GetOrAdd会保证同一键对应的工厂函数仅执行一次,后续请求直接复用已有的Task,即使任务执行失败,后续请求也会拿到同一个失败的Task,不会重新执行耗时操作。

方案二:用IMemoryCache实现(带过期,推荐)

ASP.NET自带的IMemoryCache专门处理缓存场景,自带过期策略,且原生支持并发下仅执行一次缓存项创建,是更优的生产级方案:

首先确保已注入IMemoryCache(ASP.NET Core默认已注册):

private readonly IMemoryCache _memoryCache;

// 通过构造函数注入IMemoryCache
public YourService(IMemoryCache memoryCache)
{
    _memoryCache = memoryCache;
}

public async Task<List<ProductsList>> Get(AnalysisParams aParams, string refUrlApi)
{
    var cacheKey = aParams.Project_ID;
    
    // GetOrCreateAsync会自动处理并发:同一键仅执行一次工厂方法
    return await _memoryCache.GetOrCreateAsync(cacheKey, async entry =>
    {
        // 设置缓存一天后过期,可根据业务调整
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1);
        
        // 执行耗时异步操作
        return await Utils.GetProductsList(aParams, refUrlApi);
    });
}

这个方案不仅解决了并发重复执行的问题,还自带过期清理,符合你“一天内可能需要更新缓存”的隐含需求,同时代码更简洁,是ASP.NET中缓存异步操作的标准做法。

原代码重复执行的关键原因

你看到的重复执行,大概率是因为某次Utils.GetProductsList调用抛出异常,触发了Lazy<T>的重试机制——默认Lazy<T>在工厂方法抛出异常时,下次访问Value会重新执行工厂方法,导致耗时操作被多次调用。而改用Task<T>或IMemoryCache后,即使操作失败,也不会重复执行,而是返回同一个失败的任务/缓存项。

内容的提问来源于stack exchange,提问作者alfoks

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 09:36:10