ASP.NET Web API长耗时请求如何避免重复调用并实现服务端缓存?
嘿,你的思路完全没问题——用ConcurrentDictionary跟踪正在运行的请求任务,这是解决缓存击穿(冷启动时并发请求同一资源)的经典方案之一!不过你的代码确实可以优化得更简洁、更安全,我来给你捋捋:
首先,原代码里先ContainsKey再赋值的逻辑存在竞态条件:两个线程可能同时通过ContainsKey的判断,导致重复创建Task。另外,存储Task而非Task<T>需要强制类型转换,既不安全也不优雅。
这里给你两个优化方向,都是更简洁且线程安全的实现:
方案一:用ConcurrentDictionary.GetOrAdd原子化管理Task
这个方案直接优化你原有的Task思路,消除竞态,代码更紧凑:
private static readonly ConcurrentDictionary<string, Task<T>> _pendingTasks = new ConcurrentDictionary<string, Task<T>>(); public static T GetFromCache<T>(this ICacheManager<object> cacheManager, string name, Func<T> func) { // 优先检查缓存,命中直接返回 if (cacheManager.Exists(name)) return (T)cacheManager[name]; // 原子性获取或创建任务:确保同一个key只会生成一个Task var runningTask = _pendingTasks.GetOrAdd(name, _ => Task.Run(func)); try { // 等待任务完成,获取结果 var result = runningTask.Result; // 将结果存入缓存 cacheManager.Put(name, result); return result; } finally { // 无论任务成功/失败,都清理pending任务,避免后续请求等待失效任务 _pendingTasks.TryRemove(name, out _); } }
优化点说明:
- 用
GetOrAdd代替先判断再赋值,彻底消除了多线程下的竞态问题 - 直接存储
Task<T>,不需要类型转换,类型更安全 - 把任务清理放在
finally块里,确保无论执行结果如何,都不会留下无效的任务占用资源
方案二:用Lazy<T>简化线程安全初始化
如果你的func是纯同步的长耗时操作,用Lazy<T>会更简洁——它本身就内置了线程安全的初始化逻辑,确保只有一个线程执行初始化代码:
private static readonly ConcurrentDictionary<string, Lazy<T>> _lazyInitializers = new ConcurrentDictionary<string, Lazy<T>>(); public static T GetFromCache<T>(this ICacheManager<object> cacheManager, string name, Func<T> func) { if (cacheManager.Exists(name)) return (T)cacheManager[name]; // 原子性获取或创建Lazy实例,确保同一key只有一个初始化逻辑 var lazyLoader = _lazyInitializers.GetOrAdd(name, _ => new Lazy<T>(func, LazyThreadSafetyMode.ExecutionAndPublication)); try { var result = lazyLoader.Value; cacheManager.Put(name, result); return result; } finally { _lazyInitializers.TryRemove(name, out _); } }
优势:
- 代码更简洁,不需要手动管理Task的生命周期
LazyThreadSafetyMode.ExecutionAndPublication模式会自动让所有等待线程共享第一个线程的初始化结果,完全符合你的需求
两种方案都能很好解决你的问题,选哪个取决于你的场景:如果需要支持异步操作(比如func是Func<Task<T>>),方案一更适合;如果是纯同步场景,方案二更简洁。
内容的提问来源于stack exchange,提问作者Kindzoku
相关产品推荐
相关产品推荐

