如何正确用IMemoryCache缓存Task<T>?WCF服务缓存封装问题
解决WCF服务缓存Task的失败缓存问题
我之前在封装WCF远程服务调用的缓存层时,完全踩过你说的这个坑——直接缓存Task<int[]>的话,第一次调用失败的错误任务会被死死存在缓存里,后面所有请求拿Result的时候都会抛出同一个异常,除非缓存过期或者手动清掉,简直头疼。
后来我重构了缓存逻辑,核心就是放弃缓存Task对象,转而缓存实际的业务数据,同时处理好并发请求的问题,具体思路是这样的:
- 缓存实际结果而非任务:把
int[]这类最终数据存在缓存里,而不是包裹它的Task。但这里要注意,不能让多个并发请求同时触发WCF调用(也就是缓存击穿),得加个“正在加载”的标记来协调。 - 用TaskCompletionSource做并发占位符:当第一个请求发起时,先在缓存里放一个
TaskCompletionSource<int[]>作为占位,表示“我正在拿数据,你们等我”。后续请求看到这个占位符,就直接await它的Task,不用再去调用WCF。 - 失败时不缓存任何内容:如果WCF调用失败,直接把缓存里的占位符删掉,这样下次请求会重新尝试调用,不会一直拿到错误的任务。
给你贴一段我当时写的核心代码示例,你可以参考下:
private readonly ConcurrentDictionary<string, object> _cacheStore = new ConcurrentDictionary<string, object>(); private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1); public async Task<int[]> FetchRemoteDataAsync(string cacheKey) { // 先尝试获取缓存的结果 if (_cacheStore.TryGetValue(cacheKey, out var cachedItem) && cachedItem is int[] dataResult) { return dataResult; } // 检查是否有请求正在加载数据,有的话直接等它完成 if (_cacheStore.TryGetValue(cacheKey, out var loadingMarker) && loadingMarker is TaskCompletionSource<int[]> loadingTcs) { return await loadingTcs.Task; } // 加锁做双重检查,避免并发创建多个占位符 using (await _asyncLock.WaitAsync()) { // 再次检查缓存,防止加锁期间已经有请求完成了 if (_cacheStore.TryGetValue(cacheKey, out var recheckItem) && recheckItem is int[] recheckResult) { return recheckResult; } if (_cacheStore.TryGetValue(cacheKey, out var recheckMarker) && recheckMarker is TaskCompletionSource<int[]> recheckTcs) { return await recheckTcs.Task; } // 创建新的加载占位符并存入缓存 var loadTcs = new TaskCompletionSource<int[]>(); _cacheStore.TryAdd(cacheKey, loadTcs); try { // 实际调用WCF远程服务 var remoteData = await CallWcfServiceAsync(); // 用实际结果替换缓存里的占位符 _cacheStore.TryUpdate(cacheKey, remoteData, loadTcs); return remoteData; } catch (Exception ex) { // 调用失败,移除占位符,不缓存错误 _cacheStore.TryRemove(cacheKey, out _); // 把异常抛出去,让上层处理 throw; } } }
这段代码的逻辑很清晰:用ConcurrentDictionary做缓存容器,SemaphoreSlim处理异步锁,TaskCompletionSource协调并发请求。成功就把实际数据放进缓存,失败就清空占位符,完美解决了错误任务被缓存的问题,还避免了缓存击穿。
内容的提问来源于stack exchange,提问作者Pallaris
相关产品推荐
相关产品推荐

