using System.Collections.Concurrent; using FreeRedis; using Microsoft.Extensions.Caching.Memory; using Newtonsoft.Json; using SJZY.Track.Domain.Extensions; namespace SJZY.Track.Application.Caching; /// /// 容忍短暂的数据不一致,固定周期内强制刷新(本地缓存过期时强制刷新) /// 适合存字典数据, /// 注意:缓存过多的大对象有可能OOM,尽量避免 /// 注意:redis内数据只新增/修改,不删!!! /// public abstract class BasicCacheForPeriodicRefresh where TKey : notnull where TValue : class { private readonly IMemoryCache _memoryCache; private readonly IRedisClient _redis; protected BasicCacheForPeriodicRefresh(IRedisClient redis, IMemoryCache? memoryCache = null) { _redis = redis; _memoryCache = memoryCache ?? new MemoryCache(new MemoryCacheOptions { CompactionPercentage = 0.75 }); } protected abstract string BuildRedisKey(TKey key); protected abstract TimeSpan RedisExpire { get; } protected virtual TimeSpan LocalExpire => RedisExpire.Divide(2); protected abstract TimeSpan MissingKeySetNullRedisExpire { get; } protected virtual TimeSpan MissingKeySetNullLocalExpire => MissingKeySetNullRedisExpire.Divide(2); protected abstract ValueTask LoadAsync(TKey key); protected abstract ValueTask> LoadAsync(HashSet keys); private const string ConcurrentKey = "ConcurrentKey"; private readonly ConcurrentDictionary _semaphoreDict = new() { [ConcurrentKey] = new SemaphoreSlim(10, 10) }; /// /// 安全获取缓存 /// 同一个key最多允许并行1个访问 /// /// /// public async ValueTask SafeGetAsync(TKey key) { var k = key.ToString()!; var semaphore = _semaphoreDict.GetOrAdd(k, _ => new SemaphoreSlim(1, 1)); await semaphore.WaitAsync(); try { return await GetAsync(key); } finally { semaphore.Release(); _semaphoreDict.TryRemove(k, out _); } } /// /// 批量获取缓存 /// 同时最多同时允许10个并发批量缓存访问 /// /// /// public async ValueTask> SafeGetBatchAsync(HashSet keys) { var semaphore = _semaphoreDict[ConcurrentKey]; await semaphore.WaitAsync(); try { return await GetBatchAsync(keys); } finally { semaphore.Release(); } } private async ValueTask GetAsync(TKey key) { // 1.LocalLoad var localEntry = GetLocal(key); if (localEntry != null) { // 1.1 expire refresh for db if (localEntry.HasExpire) return await RefreshForDbAsync(key); // 1.2 not expire return return localEntry.Data; } // 2.RedisLoad var redisEntry = await GetRedisAsync(key); if (redisEntry != null) { SetLocal(key, redisEntry, redisEntry.Data == null ? MissingKeySetNullLocalExpire : LocalExpire ); return redisEntry.Data; } // 3.DbLoad var dbValue = await LoadAsync(key); if (dbValue != null) { SetLocal(key, CacheEntry.Create(dbValue, LocalExpire)); await SetRedisAsync(key, dbValue, LocalExpire); return dbValue; } // db miss set null short expire SetLocal(key, CacheEntry.CreateEmpty(MissingKeySetNullLocalExpire)); await SetRedisAsync(key, null, cacheExpire: MissingKeySetNullRedisExpire); return null; } private async ValueTask RefreshForDbAsync(TKey key) { // 1.从数据库加载 var dbValue = await LoadAsync(key); if (dbValue != null) { SetLocal(key, CacheEntry.Create(dbValue, LocalExpire)); await SetRedisAsync(key, dbValue); } // 2.db加载不到,触发兜底 var redisValue = await GetRedisAsync(key); if (redisValue != null) { // 2.1 设置本地缓存,如果兜底的值为null则需要设置较短过期时间 SetLocal(key, redisValue, redisValue.Data == null ? MissingKeySetNullLocalExpire : LocalExpire ); return redisValue.Data; } // 3.触发兜底也失败了,设置本地缓存和redis SetLocal(key, CacheEntry.CreateEmpty(MissingKeySetNullLocalExpire)); await SetRedisAsync(key, null, cacheExpire: MissingKeySetNullRedisExpire); return null; } private async ValueTask> OnlyLocalMissKeyAsync(List keys) { var resultDict = new Dictionary(); // 1.redis加载 var redisMissKeys = new HashSet(); var localMissKeyArray = keys.ToArray(); var redisValues = await MGetRedisAsync(keys); for (var i = 0; i < redisValues.Length; i++) { var entry = redisValues[i]; if (entry == null) { redisMissKeys.Add(localMissKeyArray[i]); continue; } if (entry.Data != null) resultDict.Add(localMissKeyArray[i], entry.Data); // 设置本地缓存,空值和非空值过期时间不同 SetLocal(localMissKeyArray[i], entry, entry.Data == null ? MissingKeySetNullLocalExpire : LocalExpire ); } if (redisMissKeys.Count == 0) return resultDict; // 2.db加载 var dbValueDict = await LoadAsync(redisMissKeys); if (dbValueDict.Count > 0) { foreach (var kv in dbValueDict) resultDict.Add(kv.Key, kv.Value); MSetLocal(dbValueDict!, LocalExpire); await MSetRedisAsync(dbValueDict!, cacheExpire: RedisExpire); } if (dbValueDict.Count == redisMissKeys.Count) return resultDict; // 3.db加载失败,设置null值 var dbMissKeys = redisMissKeys.Except(dbValueDict.Keys).ToDictionary(x => x, _ => CacheEntry.CreateEmpty(MissingKeySetNullLocalExpire)); MSetLocal(dbMissKeys, MissingKeySetNullLocalExpire); await MSetRedisAsync(dbMissKeys, cacheExpire: MissingKeySetNullRedisExpire); return resultDict; } private async ValueTask> OnlyLocalExpireKeyAsync(List keys) { var resultDict = new Dictionary(); // 1.DbLoad var dbValueDict = await LoadAsync(keys.ToHashSet()); foreach (var kv in dbValueDict) resultDict.Add(kv.Key, kv.Value); if (dbValueDict.Count == keys.Count) { // 刷新本地缓存,还有redis缓存 MSetLocal(dbValueDict!, LocalExpire); await MSetRedisAsync(dbValueDict!, RedisExpire); return resultDict; } var nullValueSetDict = new Dictionary(); // 2.Redis兜底 var dbMissKeys = keys.Except(dbValueDict.Keys).ToList(); var dbMissKeyArray = dbMissKeys.ToArray(); var redisValues = await MGetRedisAsync(dbMissKeys); for (var i = 0; i < redisValues.Length; i++) { var entry = redisValues[i]; if (entry == null) // 触发兜底也失败了 { nullValueSetDict.Add(dbMissKeyArray[i], CacheEntry.CreateEmpty(MissingKeySetNullLocalExpire)); continue; } if (entry.Data != null) resultDict.Add(dbMissKeyArray[i], entry.Data); // 加载成功设置本地缓存 SetLocal(dbMissKeyArray[i], entry, entry.Data == null ? MissingKeySetNullLocalExpire : LocalExpire ); } // 缓存null值防止缓存穿透 if (nullValueSetDict.Count > 0) { MSetLocal(nullValueSetDict, MissingKeySetNullLocalExpire); await MSetRedisAsync(nullValueSetDict, cacheExpire: MissingKeySetNullRedisExpire); } return resultDict; } private async ValueTask> GetBatchAsync(HashSet keys) { if (keys.Count == 0) return Empty.Dict; // 1.LocalLoad var localEntries = MGetLocal(keys); // 选出过期的key和未命中的key var expireKeys = localEntries.Where(x => x.Value.HasExpire).Select(x => x.Key).ToList(); var localMissKeys = keys.Except(localEntries.Keys).ToList(); // 选出未过期并且不为空的值作为返回的基本组成 var resultDict = localEntries.Where(x => !x.Value.HasExpire && x.Value.Data != null).ToDictionary(x => x.Key, y => y.Value.Data!); // 不存在过期的也不存在未命中的直接返回即可 if (expireKeys.Count == 0 && localMissKeys.Count == 0) { return resultDict; } // 只存在未命中的key if (expireKeys.Count == 0 && localMissKeys.Count > 0) { var loadDict = await OnlyLocalMissKeyAsync(localMissKeys); foreach (var kv in loadDict) resultDict.Add(kv.Key, kv.Value); return resultDict; } // 只存在过期的key if (expireKeys.Count > 0 && localMissKeys.Count == 0) { var loadDict = await OnlyLocalExpireKeyAsync(expireKeys); foreach (var kv in loadDict) resultDict.Add(kv.Key, kv.Value); return resultDict; } // 既存在未命中的key,也存在过期的key // 2.RedisLoad 仅对missKeys尝试进行redis加载 var redisMissKeys = new List(); var localMissKeyArray = localMissKeys.ToArray(); var redisValues = await MGetRedisAsync(localMissKeys); for (var i = 0; i < redisValues.Length; i++) { var entry = redisValues[i]; if (entry == null) { redisMissKeys.Add(localMissKeyArray[i]); continue; } if (entry.Data != null) resultDict.Add(localMissKeyArray[i], entry.Data); // 加载成功设置本地缓存 SetLocal(localMissKeyArray[i], entry, entry.Data == null ? MissingKeySetNullLocalExpire : LocalExpire ); } // 3.DbLoad redis未命中和过期的key var unionKeys = new HashSet(redisMissKeys.Count + expireKeys.Count); unionKeys.UnionWith(redisMissKeys); unionKeys.UnionWith(expireKeys); var dbValueDict = await LoadAsync(unionKeys); // 比较新鲜的数据,设置redis缓存+本地缓存 if (dbValueDict.Count > 0) { foreach (var kv in dbValueDict) resultDict.Add(kv.Key, kv.Value); MSetLocal(dbValueDict!, LocalExpire); await MSetRedisAsync(dbValueDict!, RedisExpire); } // 全部加载成功 if (dbValueDict.Count == unionKeys.Count) { return resultDict; } var nullValueSetDict = new Dictionary(); // 本地未命中的key,从redis和db加载都失败,稍后将这些key设置一个null值,以防缓存穿透 var dbMissingKeys = redisMissKeys.Except(dbValueDict.Keys).ToList(); if (dbMissingKeys.Count > 0) { foreach (var key in dbMissingKeys) nullValueSetDict.Add(key, CacheEntry.CreateEmpty(MissingKeySetNullLocalExpire)); } // 本地过期的key,从db加载失败,触发兜底策略,兜底也失败则将这些key设置一个null值,以防缓存穿透 var expireKeysNotInDb = expireKeys.Except(dbValueDict.Keys).ToList(); if (expireKeysNotInDb.Count > 0) { redisValues = await MGetRedisAsync(expireKeysNotInDb); for (var i = 0; i < redisValues.Length; i++) { var entry = redisValues[i]; if (entry == null) // 触发兜底也失败了 { nullValueSetDict.Add(expireKeysNotInDb[i], CacheEntry.CreateEmpty(MissingKeySetNullLocalExpire)); continue; } if (entry.Data != null) resultDict.Add(localMissKeyArray[i], entry.Data); // 加载成功设置本地缓存 SetLocal(localMissKeyArray[i], entry, entry.Data == null ? MissingKeySetNullLocalExpire : LocalExpire ); } } // 缓存null值防止缓存穿透 if (nullValueSetDict.Count > 0) { MSetLocal(nullValueSetDict, MissingKeySetNullLocalExpire); await MSetRedisAsync(nullValueSetDict, MissingKeySetNullRedisExpire); } return resultDict; } #region 缓存操作方法 private async ValueTask GetRedisAsync(TKey key) { return await _redis.GetAsync(BuildRedisKey(key)); } private async ValueTask MGetRedisAsync(List keys) { var redisKeys = keys.Select(BuildRedisKey).ToArray(); return await _redis.MGetAsync(redisKeys); } private CacheEntry? GetLocal(TKey key) { return _memoryCache.Get(key); } private Dictionary MGetLocal(HashSet keys) { var result = new Dictionary(); foreach (var k in keys) { if (_memoryCache.TryGetValue(k, out var entry)) result.Add(k, entry!); } return result; } private Task MSetRedisAsync(Dictionary dict, TimeSpan? expire = null, TimeSpan? cacheExpire = null) { return MSetRedisAsync(dict.ToDictionary(x => x.Key, y => CacheEntry.Create(y.Value, expire)), cacheExpire); } private async Task MSetRedisAsync(Dictionary dict, TimeSpan? cacheExpire = null) { var setDict = dict.ToDictionary(x => BuildRedisKey(x.Key), y => y.Value); await _redis.MSetAsync(setDict); if (!cacheExpire.HasValue || cacheExpire.Value.TotalSeconds <= 0.0d) return; // 设置过期时间 foreach (var k in setDict.Keys) await _redis.ExpireAsync(k, cacheExpire.Value); } private Task SetRedisAsync(TKey key, TValue? value, TimeSpan? expire = null, TimeSpan? cacheExpire = null) { cacheExpire = cacheExpire?.TotalSeconds > 0.0 ? cacheExpire : TimeSpan.FromSeconds(-1); return _redis.SetAsync(BuildRedisKey(key), CacheEntry.Create(value, expire), cacheExpire.Value); } private void MSetLocal(Dictionary dict, TimeSpan? expire = null, TimeSpan? cacheExpire = null) { MSetLocal(dict.ToDictionary(x => x.Key, y => CacheEntry.Create(y.Value, expire)), null, cacheExpire); } private void MSetLocal(Dictionary dict, TimeSpan? expire = null, TimeSpan? cacheExpire = null) { foreach (var kv in dict) SetLocal(kv.Key, kv.Value, expire, cacheExpire); } private void SetLocal(TKey key, TValue? value, TimeSpan? expire = null, TimeSpan? cacheExpire = null) { SetLocal(key, CacheEntry.Create(value, expire), null, cacheExpire); } private void SetLocal(TKey key, CacheEntry entry, TimeSpan? expire = null, TimeSpan? cacheExpire = null) { if (expire.HasValue) { entry.SetExpire(expire.Value); } _memoryCache.Set(key, entry, cacheExpire?.TotalSeconds > 0.0d ? new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheExpire } : null ); } #endregion private class CacheEntry { private static readonly DateTime NoExpire = DateTime.MaxValue; public TValue? Data { get; set; } [System.Text.Json.Serialization.JsonIgnore] [JsonIgnore] public DateTime Expire { get; set; } [System.Text.Json.Serialization.JsonIgnore] [JsonIgnore] public bool HasExpire => Expire < DateTime.Now; public static CacheEntry Create(TValue? data, TimeSpan? expire = null) { return new CacheEntry { Data = data, Expire = expire.HasValue ? DateTime.Now.Add(expire.Value) : NoExpire }; } public static CacheEntry CreateEmpty(TimeSpan expire) { return new CacheEntry { Data = null, Expire = DateTime.Now.Add(expire) }; } public void SetExpire(TimeSpan expire) { Expire = DateTime.Now.Add(expire); } } }