From f0ab2f2bd94d7049481bac0ec779a72111c26031 Mon Sep 17 00:00:00 2001 From: lqc <15342622+aq982@user.noreply.gitee.com> Date: Mon, 10 Mar 2025 01:17:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dservice=5FMid.cs.vm=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=B8=AD=E9=97=B4=E4=BB=B6=E8=B7=A8=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8=E5=BC=82=E5=BA=93=E5=9B=A0=E4=B8=BB=E5=BA=93=E6=9D=83?= =?UTF-8?q?=E9=99=90=E6=97=A0=E6=B3=95=E8=AF=BB=E5=8F=96=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=9B=20=E6=96=B0=E5=A2=9E=E7=BC=93=E5=AD=98=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E8=AE=BE=E7=BD=AE=E5=92=8C=E8=8E=B7=E5=8F=96=E7=9A=84?= =?UTF-8?q?=E6=89=A9=E5=B1=95=EF=BC=88=E5=8F=AA=E5=AF=B9=E9=9B=AA=E8=8A=B1?= =?UTF-8?q?Id=E6=9C=89=E6=95=88=EF=BC=89=EF=BC=8C=E9=80=82=E7=94=A8?= =?UTF-8?q?=E5=AE=9E=E4=BD=93=E5=A4=A7=E6=95=B0=E6=8D=AE=E9=87=8F=E7=A2=8E?= =?UTF-8?q?=E7=89=87=E5=8C=96=E5=AD=98=E5=82=A8=E7=BC=93=E5=AD=98=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=EF=BC=8C=E8=87=AA=E5=8A=A8=E5=88=86=E6=89=B9=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Service/Cache/SysCacheService.cs | 309 +++++++++++++++++- .../wwwroot/template/service_Mid.cs.vm | 8 +- 2 files changed, 303 insertions(+), 14 deletions(-) diff --git a/Admin.NET/Admin.NET.Core/Service/Cache/SysCacheService.cs b/Admin.NET/Admin.NET.Core/Service/Cache/SysCacheService.cs index eca5bb72..f4589562 100644 --- a/Admin.NET/Admin.NET.Core/Service/Cache/SysCacheService.cs +++ b/Admin.NET/Admin.NET.Core/Service/Cache/SysCacheService.cs @@ -4,6 +4,9 @@ // // 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! +using NewLife; +using Newtonsoft.Json; + namespace Admin.NET.Core.Service; /// @@ -143,7 +146,7 @@ public class SysCacheService : IDynamicApiController, ISingleton private static string KeySingle(object t) { - return t.GetType().IsClass && !t.GetType().IsPrimitive ? JSON.Serialize(t) : t.ToString(); + return t.GetType().IsClass && !t.GetType().IsPrimitive ? JsonConvert.SerializeObject(t) : t.ToString(); } /// @@ -152,7 +155,7 @@ public class SysCacheService : IDynamicApiController, ISingleton /// /// [NonAction] - public static TimeSpan GetExpire(string key) + public TimeSpan GetExpire(string key) { return _cacheProvider.Cache.GetExpire(key); } @@ -168,7 +171,250 @@ public class SysCacheService : IDynamicApiController, ISingleton { return _cacheProvider.Cache.Get($"{_cacheOptions.Prefix}{key}"); } + /// + /// 批量获取缓存值(普通键值结构)🔖 + /// + /// 值类型 + /// 缓存键集合 + /// 与键顺序对应的值列表 + [NonAction] + public List GetBatch(IEnumerable keys) + { + var prefixedKeys = keys.Select(k => $"{_cacheOptions.Prefix}{k}"); + return prefixedKeys.Select(k => _cacheProvider.Cache.Get(k)).ToList(); + } + /// + /// 批量设置缓存项(兼容现有键规则)❄️,方法只对雪花Id有效 + /// + /// 实体类型(需包含Id属性) + /// 待缓存数据集合 + /// 统一过期时间 + /// 批次大小(默认500) + [NonAction] + public void SetList(IEnumerable items, TimeSpan? expire = null, int batchSize = 500) where T : class + { + if (items == null) return; + var itemList = items.ToList(); + if (itemList.Count == 0) return; + + // 获取雪花ID属性 + var idProperty = typeof(T).GetProperty("Id") + ?? throw new ArgumentException("实体必须包含Id属性"); + + // 分批次处理 + foreach (var batch in itemList.Batch(batchSize)) + { + var dic = batch.ToDictionary( + item => $"{_cacheOptions.Prefix}{idProperty.GetValue(item)}", + item => item + ); + + if (_cacheProvider.Cache is Redis redis) + { + // Redis管道批量设置 + redis.StartPipeline(); + try + { + foreach (var kv in dic) + { + redis.Set(kv.Key, kv.Value, expire ?? TimeSpan.Zero); + } + } + finally + { + redis.StopPipeline(true); + } + } + else + { + // 通用缓存实现 + foreach (var kv in dic) + { + _cacheProvider.Cache.Set(kv.Key, kv.Value, expire ?? TimeSpan.Zero); + } + } + } + } + + /// + /// 异步批量获取(当前为同步实现,未来可升级) + /// 实体类型 + /// 雪花ID集合 + /// 数据加载方法 + /// 是否缓存空值(防穿透) + /// 空值缓存时间(默认永久) + /// + [NonAction] + public async Task> GetListAsync( + IEnumerable ids, + Func, Task>> loadFromDb, // 改为异步委托 + bool cacheNull = true, + TimeSpan? nullExpire = null +) where T : class + { + var idList = ids.Distinct().ToList(); + if (idList.Count == 0) return new List(); + + // 1. 批量获取缓存(保持同步,假设缓存操作快速) + var cachedItems = GetFromCache(idList); + + // 2. 识别未命中ID + var missedIds = new List(); + var resultDict = new Dictionary(); + + for (int i = 0; i < idList.Count; i++) + { + if (cachedItems[i] != null) + { + resultDict[idList[i]] = cachedItems[i]; + } + else + { + missedIds.Add(idList[i]); + } + } + + // 3. 异步加载缺失数据 + if (missedIds.Count > 0) + { + var dbItems = await loadFromDb(missedIds).ConfigureAwait(false); // 异步等待 + var dbDict = dbItems.ToDictionary(GetId); + + // 4. 缓存回填 + var toCache = new List(); + foreach (var id in missedIds) + { + if (dbDict.TryGetValue(id, out var item)) + { + resultDict[id] = item; + toCache.Add(item); + } + //else if (cacheNull) + //{ + // // 使用 default(T) 作为空值标记 + // toCache.Add(default(T)); + //} + } + + if (toCache.Count > 0) SetList(toCache, cacheNull ? nullExpire : null); // 保持同步缓存写入 + } + + // 5. 按原始顺序返回 + return idList.Select(id => resultDict.TryGetValue(id, out var item) + ? (item ==null ? null : item) + : null).ToList(); + } + /// + /// 批量获取(自动加载缺失数据+缓存回填)🔁 + /// + /// 实体类型 + /// 雪花ID集合 + /// 数据加载方法 + /// 是否缓存空值(防穿透) + /// 空值缓存时间(默认永久) + [NonAction] + public List GetList( + IEnumerable ids, + Func, List> loadFromDb, + bool cacheNull = true, + TimeSpan? nullExpire = null + ) where T : class + { + var idList = ids.Distinct().ToList(); + if (idList.Count == 0) return new List(); + + // 1. 批量获取缓存 + var cachedItems = GetFromCache(idList); + + // 2. 识别未命中ID + var missedIds = new List(); + var resultDict = new Dictionary(); + + for (int i = 0; i < idList.Count; i++) + { + if (cachedItems[i] != null) + { + resultDict[idList[i]] = cachedItems[i]; + } + else + { + missedIds.Add(idList[i]); + } + } + + // 3. 加载缺失数据 + if (missedIds.Count > 0) + { + var dbItems = loadFromDb(missedIds); + var dbDict = dbItems.ToDictionary(GetId); + + // 4. 缓存回填 + var toCache = new List(); + foreach (var id in missedIds) + { + if (dbDict.TryGetValue(id, out var item)) + { + resultDict[id] = item; + toCache.Add(item); + } + //else if (cacheNull) + //{ + // // 缓存空值标记 + // toCache.Add(default(T)); + //} + } + + //SetList(toCache, cacheNull ? (nullExpire ?? TimeSpan.FromMinutes(5)) : null); + // 将默认过期时间改为null(一直存储) + + if(toCache.Count>0) SetList(toCache, cacheNull ? nullExpire : null); + } + + // 5. 按原始顺序返回 + return idList.Select(id => resultDict.TryGetValue(id, out var item) + ? (item ==null ? null : item) + : null).ToList(); + } + + private long GetId(T item) + { + var prop = typeof(T).GetProperty("Id"); + return (long)prop.GetValue(item); + } + + /// + /// 基础方法:仅从缓存获取数据 + /// + [NonAction] + public List GetFromCache(List ids) where T : class + { + if (ids == null || ids.Count == 0) + return new List(); + + var keys = ids.Select(id => $"{_cacheOptions.Prefix}{id}").ToList(); + + if (_cacheProvider.Cache is FullRedis redis) + { + var result = redis.GetAll(keys); + return keys.Select(k => result.TryGetValue(k, out var val) ? val : null).ToList(); + } + + return keys.Select(k => _cacheProvider.Cache.Get(k)).ToList(); + } + // + /// 批量获取哈希缓存字段值(哈希结构)🔖 + /// + /// 值类型 + /// 哈希键名 + /// 要获取的字段集合 + /// 与字段顺序对应的值列表 + [NonAction] + public List HashGetBatch(string key, IEnumerable fields) + { + var hash = GetHashMap($"{_cacheOptions.Prefix}{key}"); + return fields.Select(f => hash.TryGetValue(f, out T val) ? val : default).ToList(); + } /// /// 删除缓存 🔖 /// @@ -272,7 +518,7 @@ public class SysCacheService : IDynamicApiController, ISingleton /// /// [NonAction] - public static IDictionary GetHashMap(string key) + public IDictionary GetHashMap(string key) { return _cacheProvider.Cache.GetDictionary(key); } @@ -285,7 +531,7 @@ public class SysCacheService : IDynamicApiController, ISingleton /// /// [NonAction] - public static bool HashSet(string key, Dictionary dic) + public bool HashSet(string key, Dictionary dic) { var hash = GetHashMap(key); foreach (var v in dic) @@ -303,7 +549,7 @@ public class SysCacheService : IDynamicApiController, ISingleton /// /// [NonAction] - public static void HashAdd(string key, string hashKey, T value) + public void HashAdd(string key, string hashKey, T value) { var hash = GetHashMap(key); hash.Add(hashKey, value); @@ -317,7 +563,7 @@ public class SysCacheService : IDynamicApiController, ISingleton /// /// [NonAction] - public static void HashAddOrUpdate(string key, string hashKey, T value) + public void HashAddOrUpdate(string key, string hashKey, T value) { var hash = GetHashMap(key); if (hash.ContainsKey(hashKey)) @@ -334,7 +580,7 @@ public class SysCacheService : IDynamicApiController, ISingleton /// /// [NonAction] - public static List HashGet(string key, params string[] fields) + public List HashGet(string key, params string[] fields) { var hash = GetHashMap(key); return hash.Where(t => fields.Any(c => t.Key == c)).Select(t => t.Value).ToList(); @@ -348,12 +594,50 @@ public class SysCacheService : IDynamicApiController, ISingleton /// /// [NonAction] - public static T HashGetOne(string key, string field) + public T HashGetOne(string key, string field) { var hash = GetHashMap(key); return hash.TryGetValue(field, out T value) ? value : default; } + // 新增方法:获取哈希表所有键 + public List HashGetAllKeys(string key) + { + var hash = GetHashMap(key); // 假设值为任意类型 + return hash.Keys.ToList(); + } + // 增强的哈希设置方法(带过期时间) + public bool HashSet(string key, Dictionary items, TimeSpan? expiry = null) + { + var hash = GetHashMap(key); + foreach (var item in items) + { + hash[item.Key] = item.Value; + } + + if (expiry.HasValue) + { + _cacheProvider.Cache.SetExpire(key, expiry.Value); + } + return true; + } + // 异步批量设置哈希,目前没有,先保留扩展 + public async Task HashSetAsync(string key, Dictionary items, TimeSpan? expiry = null) + { + var hash = GetHashMap(key); + foreach (var item in items) + { + hash[item.Key] = item.Value; + } + if (expiry.HasValue) + { + _cacheProvider.Cache.SetExpire(key, expiry.Value); + } + return true; + } + + // 异步设置过期时间 + /// /// 根据KEY获取所有HASH /// @@ -361,7 +645,7 @@ public class SysCacheService : IDynamicApiController, ISingleton /// /// [NonAction] - public static IDictionary HashGetAll(string key) + public IDictionary HashGetAll(string key) { var hash = GetHashMap(key); return hash; @@ -375,7 +659,7 @@ public class SysCacheService : IDynamicApiController, ISingleton /// /// [NonAction] - public static int HashDel(string key, params string[] fields) + public int HashDel(string key, params string[] fields) { var hash = GetHashMap(key); fields.ToList().ForEach(t => hash.Remove(t)); @@ -410,4 +694,9 @@ public class SysCacheService : IDynamicApiController, ISingleton // var hash = GetHashMap(key); // return hash.Search(pattern, count).ToList(); //} +} +public class CacheItem +{ + public T Value { get; set; } + public bool IsNull { get; set; } } \ No newline at end of file diff --git a/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/service_Mid.cs.vm b/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/service_Mid.cs.vm index 621a81b9..c48c0480 100644 --- a/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/service_Mid.cs.vm +++ b/Admin.NET/Admin.NET.Web.Entry/wwwroot/template/service_Mid.cs.vm @@ -81,11 +81,11 @@ public partial class @(@Model.ClassName)Mid @: var key = $"@(@column.FkEntityName)_{t.@(@column.PropertyName)}"; @: if (!sysCacheService.ExistKey(key)) @: { - @: var m = db.CopyNew().GetSimpleClient<@(@column.FkEntityName)>().GetFirst(f => f.@(@column.FkLinkColumnName) == t.@(@column.PropertyName)); + @: var m = db.Queryable<@(@column.FkEntityName)>().FirstAsync(f => f.@(@column.FkLinkColumnName) == t.@(@column.PropertyName)); @: if (m != null) sysCacheService.Set(key, m); @: } @: t.@(@column.PropertyName)@(@column.FkColumnName) = sysCacheService.Get<@(@column.FkEntityName)>(key)?.@(@column.FkColumnName); - @: //t.@(@column.PropertyName)@(@column.FkColumnName)=db.CopyNew().GetSimpleClient<@(@column.FkEntityName)>().GetFirst(f => f.@(@column.FkLinkColumnName) == t.@(@column.PropertyName))).@(@column.FkColumnName);// + @: //t.@(@column.PropertyName)@(@column.FkColumnName)=db.Queryable<@(@column.FkEntityName)>().FirstAsync(f => f.@(@column.FkLinkColumnName) == t.@(@column.PropertyName))).@(@column.FkColumnName);// @:}) } else if(@column.EffectType == "ApiTreeSelector"){ @@ -95,11 +95,11 @@ public partial class @(@Model.ClassName)Mid @: var key = $"@(@column.FkEntityName)_{t.@(@column.PropertyName)}"; @: if (!sysCacheService.ExistKey(key)) @: { - @: var m = db.CopyNew().GetSimpleClient<@(@column.FkEntityName)>().GetFirst(f => f.@(@column.ValueColumn) == t.@(@column.PropertyName)); + @: var m = db.Queryable<@(@column.FkEntityName)>().FirstAsync(f => f.@(@column.ValueColumn) == t.@(@column.PropertyName)); @: if (m != null) sysCacheService.Set(key, m); @: } @: t.@(@column.PropertyName)@(@column.DisplayColumn) = sysCacheService.Get<@(@column.FkEntityName)>(key)?.@(@column.DisplayColumn); - @: //t.@(@column.PropertyName)@(@column.FkColumnName)=db.CopyNew().GetSimpleClient<@(@column.FkEntityName)>().GetFirst(f => f.@(@column.FkLinkColumnName) == t.@(@column.PropertyName))).@(@column.FkColumnName);// + @: //t.@(@column.PropertyName)@(@column.FkColumnName)=db.Queryable<@(@column.FkEntityName)>().FirstAsync(f => f.@(@column.FkLinkColumnName) == t.@(@column.PropertyName))).@(@column.FkColumnName);// @:}) } }