public LocalCacheItemCallbackWrapper(LocalCacheItem item, string key, Func<object, object> callback, object callBackState, int timeToLiveInSeconds, params string[] tags) { this.Item = item; this.Key = key; this.Callback = callback; this.CallBackState = callBackState; this.TTL = TimeSpan.FromSeconds(timeToLiveInSeconds); this.Tags = tags; }
public object GetAndSetItem(string key, Func<object, object> callback, object callBackState, params string[] tags) { DataCacheItem cachedItem = this.GetItemInternal(key); LocalCacheItem localCacheItem; // 缓存项不存在时立即添加新的缓存项并返回 if (cachedItem == null) { object cacheItemLock = null; lock (cacheItemLocks) { if (!cacheItemLocks.ContainsKey(key)) { cacheItemLock = new object(); cacheItemLocks[key] = cacheItemLock; } else { cacheItemLock = cacheItemLocks[key]; } } object value = null; lock (cacheItemLock) { cachedItem = this.GetItemInternal(key); if (cachedItem == null) { value = callback(callBackState); if (value != null) { localCacheItem = new LocalCacheItem(); localCacheItem.NextUpdateTime = DateTime.Now.AddSeconds(this.asyncUpdateInterval); // 下次更新时间 localCacheItem.IsUpdating = false; // 缓存项在缓存服务器中的生存时间为:异步更新间隔+5分钟,这足够保证缓存项能够及时成功更新 // 同时,可以避免添加当前缓存项的客户端应用终止时(客户端应用中保存的 cacheItemVersions 失效,其它客户端因为仅读取该缓存项而没有存储其版本,永远不会更新该缓存项) // 缓存项将可能在很长时间(由传入的 timeToLive 值决定)内不再更新的问题 localCacheItem.Version = this.SetItemInternal(key, value, TimeSpan.FromSeconds(this.asyncUpdateInterval + this.asyncUpdateAdditionalLiveTime), tags); this.lock4cacheItemVersions.EnterWriteLock(); try { this.cacheItemVersions[key] = localCacheItem; } finally { this.lock4cacheItemVersions.ExitWriteLock(); } } } else { value = cachedItem.Value; } } lock (cacheItemLocks) { cacheItemLocks.Remove(key); } return value; } this.lock4cacheItemVersions.EnterReadLock(); try { localCacheItem = this.cacheItemVersions.ContainsKey(key) ? this.cacheItemVersions[key] : null; } finally { this.lock4cacheItemVersions.ExitReadLock(); } // 首先不是处于更新过程中且时间上判断应更新缓存项,使用双重检查锁定机制并通过任务并行库异步更新缓存项 if (localCacheItem != null) { //如果缓存服务器返回的缓存项的版本与本地存储的版本相同的话,说明服务器的缓存项是在本地设置的 if (localCacheItem.Version == cachedItem.Version) { if (!localCacheItem.IsUpdating && localCacheItem.NextUpdateTime < DateTime.Now) { lock (localCacheItem) // 仅锁定当前缓存项,尽可能的避免阻塞对其它缓存项的访问 { if (!localCacheItem.IsUpdating && localCacheItem.NextUpdateTime < DateTime.Now) { localCacheItem.IsUpdating = true; System.Threading.Tasks.Task.Factory.StartNew(this.UpdateCacheItem, new LocalCacheItemCallbackWrapper(localCacheItem, key, callback, callBackState, this.asyncUpdateInterval + this.asyncUpdateAdditionalLiveTime, tags)); } } } } else//缓存项已经被其它程序更新,移除 localCacheItem,以便仅在其它程序中维护它 { // 以下两种情况会造成 localCacheItem 的版本与 cachedItem 的版本不相等 // 1. 当前应用的其它运行实例或其它应用主动修改了同一键值的缓存 // 这种情况下,localCacheItem.NextUpdateTime 和 localCacheItem.Version 之后将永远不会再被修改,因此,只要在超过 localCacheItem.NextUpdateTime 时从 cacheItemVersions 中移除当前键值对应的 localCacheItem 即可。 // 2. 在从 cacheItemVersions 中获取到 localCacheItem 后并且执行到 localCacheItem.Version == cachedItem.Version 进行比较的过程中,恰好有另外一个线程命中 // 这种情况下,localCacheItem.NextUpdateTime 通常大于 当前时间(因为 UpdateCacheItem 中更新 localCacheItem.Version 时会先更新其 NextUpdateTime 属性),但可能存在以下例外: // 对于更新间隔时间很小的情况(比如 0.1 秒),有可能因为在该间隔内从取到 localCacheItem 开始未执行到这里而造成 localCacheItem.NextUpdateTime 小于 当前时间。 // 综上,假设程序在 1 分钟之内从取到 localCacheItem 开始一定能执行到这里(通常一定能满足),则只需要判断 localCacheItem.NextUpdateTime + 1 分钟 小于 当前时间 时移除 localCacheItem 即可。 if (localCacheItem.NextUpdateTime.AddMinutes(1) < DateTime.Now) { this.lock4cacheItemVersions.EnterWriteLock(); try { if (this.cacheItemVersions[key] == localCacheItem) { this.cacheItemVersions.Remove(key); } } finally { this.lock4cacheItemVersions.ExitWriteLock(); } } } } // 在异步更新缓存项之前立即返回当前缓存项的值。 return cachedItem.Value; }
/// <summary> /// Checks the local cache timestamp against the distributed cache timestamp if the local timestamp /// is blank or old than the distributed cache's, it updates it with a fresh copy from the database, /// it also updates it if the timestamp in the distributed cache is blank or invalid /// </summary> private void EnsureFreshnessOfCaches(params string[] cultureNames) { // Determine which caches represent a stale caches and need refreshing List <CacheInfo> staleCaches = new List <CacheInfo>(); foreach (var cultureName in cultureNames) { // Retrieve the timestamps from the distributed cache var cacheKey = CacheKey(cultureName); var distVersion = _distributedCache.GetString(cacheKey); // If the distributed cache is blank or invalid, set it to a new version if (distVersion == null) { // Either the cache was flushed, or has been illegally tampered with, // simply reset the cache to NOW, to invalidate all local caches distVersion = Guid.NewGuid().ToString(); _distributedCache.SetString(cacheKey, distVersion, GetDistributedCacheOptions()); // NOTE: This should work fine with concurrency, the worst that could happen is // that if multiple nodes independently find that the distributed cache is blank // then one of the nodes may overwrite the version inserted by the other nodes // and then for those other nodes, the freshly retrieved translations may end up // being fetched once more in the next request, since their local cache will have // a different version } _localCache.TryGetValue(cacheKey, out LocalCacheItem localCacheItem); // If the local cache is blank or outdated, grab a fresh copy from the DB if (localCacheItem == null || localCacheItem.Version != distVersion) { // Remember the version from the distributed cache staleCaches.Add(new CacheInfo { LatestVersion = distVersion, CultureName = cultureName }); } } ////// Efficiently retrieve a fresh list of translations for all stale caches Dictionary <string, Dictionary <string, string> > freshTranslations = new Dictionary <string, Dictionary <string, string> >(); // Get translations from AdminContext if (staleCaches.Any()) { var staleCacheKeys = staleCaches.Select(e => e.CultureName); using (var scope = _serviceProvider.CreateScope()) { using (var ctx = scope.ServiceProvider.GetRequiredService <AdminContext>()) { var freshTranslationsQuery = from e in ctx.Translations where (e.Tier == Constants.Server || e.Tier == Constants.Shared) && staleCacheKeys.Contains(e.CultureId) group e by e.CultureId into g select g; freshTranslations = freshTranslationsQuery.AsNoTracking().ToDictionary( g => CacheKey(g.Key), g => g.ToDictionary(e => e.Name, e => e.Value)); } } } // Update the stale caches with the fresh translations foreach (var staleCache in staleCaches) { // Get the cache key var cacheKey = CacheKey(staleCache.CultureName); // Prepare the translations in a dictionary, even an empty one if no list came from SQL Dictionary <string, string> translations = new Dictionary <string, string>(); if (freshTranslations.ContainsKey(cacheKey)) { translations = freshTranslations[cacheKey]; } // Set the local cache _localCache[cacheKey] = new LocalCacheItem { Version = staleCache.LatestVersion, Translations = translations }; } }