【问题标题】:Optimize cache with multiple keys in c# - remove duplication of objects在 C# 中使用多个键优化缓存 - 删除对象的重复
【发布时间】:2018-07-27 20:27:48
【问题描述】:

我在 Asp.Net Core 中有一个项目。这个项目有一个 ICacheService 如下:

public interface ICacheService
{
    T Get<T>(string key);
    T Get<T>(string key, Func<T> getdata);
    Task<T> Get<T>(string key, Func<Task<T>> getdata); 
    void AddOrUpdate(string key, object value);
} 

实现只是基于ConcurrentDictionary&lt;string, object&gt;,所以它并不复杂,只是从这个字典存储和检索数据。在我的一项服务中,我有一种方法如下:

public async Task<List<LanguageInfoModel>> GetLanguagesAsync(string frontendId, string languageId, string accessId) 
{
    async Task<List<LanguageInfoModel>> GetLanguageInfoModel()
    {
        var data = await _commonServiceProxy.GetLanguages(frontendId, languageId, accessId);
        return data;
    }

    _scheduler.ScheduleAsync($"{CacheKeys.Jobs.LanguagesJob}_{frontendId}_{languageId}_{accessId}", async () =>
    {
        _cacheService.AddOrUpdate($"{CacheKeys.Languages}_{frontendId}_{languageId}_{accessId}", await GetLanguageInfoModel());
        return JobStatus.Success;
    }, TimeSpan.FromMinutes(5.0));

    return await _cacheService.Get($"{CacheKeys.Languages}_{frontendId}_{languageId}_{accessId}", async () => await GetLanguageInfoModel());
}

问题是我在这个方法中有三个参数用作缓存键。这很好用,但问题是三个参数的组合非常高,所以缓存中会有很多重复的对象。我正在考虑创建一个没有重复的缓存,如下所示:

拥有一个以列表为键的缓存,我可以为一个对象存储多个键。因此,当我获得新元素时,我将检查它们中的每一个是否在缓存中,如果它在缓存中,我只会在键列表中添加一个键,否则在缓存中插入一个新元素。这里的问题是测试一个对象是否在缓存中是一个大问题。我认为它会消耗大量资源,并且需要将一些序列化为特定形式以使比较成为可能,这将再次使比较消耗大量资源。 缓存可能看起来像这样 CustomDictionary&lt;List&lt;string&gt;, object&gt;

有没有人知道解决这个问题的好方法,不重复缓存中的对象?

编辑 1:

我主要担心的是当我从我的 web 服务中检索 List&lt;MyModel&gt; 时,因为它们可能有 80% 的对象具有相同的数据,这将大大增加内存的大小。但这也适用于简单的情况。 免得假设我有这样的事情:

MyClass o1 = new MyObject();
_cache.Set("key1", o1);
_cashe.Set("key2", o1);

在这种情况下,当尝试添加两次相同的对象时,我不想复制它,而是让key2 以某种方式指向与key1 相同的对象。如果实现了,那么使它们无效将是一个问题,但我希望有这样的东西:

_cache.Invalidate("key2");

这将检查是否有另一个键指向同一个对象。如果是这样,它只会删除密钥,否则会破坏对象本身。

【问题讨论】:

  • 为什么不使用内置的内存缓存?
  • 因为对我的需要 ConcurrentDictionary 已经足够了,另一个原因是因为我使用作业来更新它,据我所知,它对我来说是一个很好的解决方案。如果内置内存缓存对我的问题有一些优势,我会轻松切换到它
  • 如果我理解正确,您只想在缓存中为$"{CacheKeys.Languages}_{frontendId}_{languageId}_{accessId}" 的所有组合存储一个对象,因此它们都具有相同的数据。那么为什么不直接使用 cons 字符串作为缓存呢?如果值相同,为什么它需要不同?
  • 你的意思是 GetLanguageInfoModel() for frontendid=1, languageid=2 and accessid=3 可以为三个参数的完全不同的值生成相同的模型?如果是这样,GetLanguageInfoModel() 与三个参数的关系如何?缓存键以独特的方式表示对象状态......可能键生成对于当前情况不是很正确?
  • @RajmondBurgaj 在 c# 中存储引用与在 c++ 中指向同一个对象完全相同。 .net 核心中的 IMemoryCache 可以满足您的所有需求。在存储时,您也可以存储现有对象的引用,IMemoryCache 可能只是错误地计算了它的内存占用,但您不会有重复的对象。当您为 key1 和 key2 存储 o1 时,谁告诉您它们是重复的?在这种情况下,它只是存储对原始 o1 的引用,而不是复制。

标签: c# caching asp.net-core


【解决方案1】:

也许我们可以将这个问题重新表述为两个独立的问题...

  1. 为每个组合执行调用并
  2. 存储 n 次相同的结果,浪费大量内存

对于 1,我不知道如何防止它,因为在执行之前我们不知道是否会在此设置中获取副本。我们需要更多信息,这些信息基于这些值何时发生变化,这可能是也可能是不可能的。

对于 2,一种解决方案是覆盖哈希码,使其基于实际返回的值。一个好的解决方案是通用的并且遍历对象树(这可能很昂贵)。想知道实际上是否有任何预制的解决方案。

【讨论】:

    【解决方案2】:

    此答案专门用于返回List&lt;TItem&gt;s,而不仅仅是单个TItems,它避免了任何TItem 以及任何List&lt;T&gt; 的重复。它使用数组,因为您试图节省内存,而数组将使用少于List

    请注意,要使此(以及任何真正的解决方案)起作用,您必须在 TItem 上覆盖 EqualsGetHashCode,以便它知道什么是重复项。 (除非数据提供者每次都返回相同的对象,这不太可能。)如果您无法控制TItem,但您可以自己确定两个TItems 是否相等,则可以使用@987654332 @ 来做到这一点,但下面的解决方案需要稍微修改一下才能做到这一点。

    通过基本测试查看解决方案: https://dotnetfiddle.net/pKHLQP

    public class DuplicateFreeCache<TKey, TItem> where TItem : class
    {
        private ConcurrentDictionary<TKey, int> Primary { get; } = new ConcurrentDictionary<TKey, int>();
        private List<TItem> ItemList { get; } = new List<TItem>();
        private List<TItem[]> ListList { get; } = new List<TItem[]>();
        private Dictionary<TItem, int> ItemDict { get; } = new Dictionary<TItem, int>();
        private Dictionary<IntArray, int> ListDict { get; } = new Dictionary<IntArray, int>();
    
        public IReadOnlyList<TItem> GetOrAdd(TKey key, Func<TKey, IEnumerable<TItem>> getFunc)
        {
            int index = Primary.GetOrAdd(key, k =>
            {
                var rawList = getFunc(k);
    
                lock (Primary)
                {
                    int[] itemListByIndex = rawList.Select(item =>
                    {
                        if (!ItemDict.TryGetValue(item, out int itemIndex))
                        {
                            itemIndex = ItemList.Count;
                            ItemList.Add(item);
                            ItemDict[item] = itemIndex;
                        }
                        return itemIndex;
                    }).ToArray();
    
                    var intArray = new IntArray(itemListByIndex);
    
                    if (!ListDict.TryGetValue(intArray, out int listIndex))
                    {
                        lock (ListList)
                        {
                            listIndex = ListList.Count;
                            ListList.Add(itemListByIndex.Select(ii => ItemList[ii]).ToArray());
                        }
                        ListDict[intArray] = listIndex;
                    }
    
                    return listIndex;
                }
            });
    
            lock (ListList)
            {
                return ListList[index];
            }
        }
    
    
        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine($"A cache with:");
            sb.AppendLine($"{ItemList.Count} unique Items;");
            sb.AppendLine($"{ListList.Count} unique lists of Items;");
            sb.AppendLine($"{Primary.Count} primary dictionary items;");
            sb.AppendLine($"{ItemDict.Count} item dictionary items;");
            sb.AppendLine($"{ListDict.Count} list dictionary items;");
            return sb.ToString();
        }
    
        //We have this to make Dictionary lookups on int[] find identical arrays.
        //One could also just make an IEqualityComparer, but I felt like doing it this way.
        public class IntArray
        {
            private readonly int _hashCode;
            public int[] Array { get; }
            public IntArray(int[] arr)
            {
                Array = arr;
                unchecked
                {
                    _hashCode = 0;
                    for (int i = 0; i < arr.Length; i++)
                        _hashCode = (_hashCode * 397) ^ arr[i];
                }
            }
    
            protected bool Equals(IntArray other)
            {
                return Array.SequenceEqual(other.Array);
            }
    
            public override bool Equals(object obj)
            {
                if (ReferenceEquals(null, obj)) return false;
                if (ReferenceEquals(this, obj)) return true;
                if (obj.GetType() != this.GetType()) return false;
                return Equals((IntArray)obj);
            }
    
            public override int GetHashCode() => _hashCode;
        }
    }
    

    我想到如果lock 导致性能滞后,ReaderWriterLockSlim 会比lock(ListList) 更好,但它稍微复杂一些。

    【讨论】:

      【解决方案3】:

      与@MineR 类似,此解决方案执行“双重缓存”操作:它缓存键的列表(查找)以及单个对象 - 执行自动重复数据删除。

      这是一个相当简单的解决方案,使用两个ConcurrentDictionaries - 一个充当HashSet,一个充当键控查找。这允许框架处理大部分线程问题。

      您还可以在多个Cachedlookups 之间传递和共享哈希集,从而允许使用不同的键进行查找。

      请注意,需要对象相等或 IEqualityComparer 才能生成任何此类解决方案功能。

      类:

      public class CachedLookup<T, TKey>
      {        
          private readonly ConcurrentDictionary<T, T> _hashSet;
          private readonly ConcurrentDictionary<TKey, List<T>> _lookup = new ConcurrentDictionary<TKey, List<T>>();
      
          public CachedLookup(ConcurrentDictionary<T, T> hashSet)
          {
              _hashSet = hashSet;
          }   
      
          public CachedLookup(IEqualityComparer<T> equalityComparer = default)
          {
              _hashSet = equalityComparer is null ? new ConcurrentDictionary<T, T>() : new ConcurrentDictionary<T, T>(equalityComparer);
          }
      
          public List<T> Get(TKey key) => _lookup.ContainsKey(key) ? _lookup[key] : null;
      
          public List<T> Get(TKey key, Func<TKey, List<T>> getData)
          {
              if (_lookup.ContainsKey(key))
                  return _lookup[key];
      
              var result = DedupeAndCache(getData(key));
      
              _lookup.TryAdd(key, result);
      
              return result;
          }
          public async ValueTask<List<T>> GetAsync(TKey key, Func<TKey, Task<List<T>>> getData)
          {
              if (_lookup.ContainsKey(key))
                  return _lookup[key];
      
              var result = DedupeAndCache(await getData(key));
      
              _lookup.TryAdd(key, result);
      
              return result;
          }
      
          public void Add(T value) => _hashSet.TryAdd(value, value);
      
          public List<T> AddOrUpdate(TKey key, List<T> data)
          {            
              var deduped = DedupeAndCache(data);
      
              _lookup.AddOrUpdate(key, deduped, (k,l)=>deduped);
      
              return deduped;
          }
      
          private List<T> DedupeAndCache(IEnumerable<T> input) => input.Select(v => _hashSet.GetOrAdd(v,v)).ToList();
      }
      

      示例用法:

      public class ExampleUsage
      {
          private readonly CachedLookup<LanguageInfoModel, (string frontendId, string languageId, string accessId)> _lookup 
              = new CachedLookup<LanguageInfoModel, (string frontendId, string languageId, string accessId)>(new LanguageInfoModelComparer());
      
          public ValueTask<List<LanguageInfoModel>> GetLanguagesAsync(string frontendId, string languageId, string accessId)
          {
              return _lookup.GetAsync((frontendId, languageId, accessId), GetLanguagesFromDB(k));
          }
      
          private async Task<List<LanguageInfoModel>> GetLanguagesFromDB((string frontendId, string languageId, string accessId) key) => throw new NotImplementedException();
      }
      
      public class LanguageInfoModel
      {
          public string FrontendId { get; set; }
          public string LanguageId { get; set; }
          public string AccessId { get; set; }
          public string SomeOtherUniqueValue { get; set; }
      }
      
      public class LanguageInfoModelComparer : IEqualityComparer<LanguageInfoModel>
      {
          public bool Equals(LanguageInfoModel x, LanguageInfoModel y)
          {
              return (x?.FrontendId, x?.AccessId, x?.LanguageId, x?.SomeOtherUniqueValue)
                  .Equals((y?.FrontendId, y?.AccessId, y?.LanguageId, y?.SomeOtherUniqueValue));
          }
      
          public int GetHashCode(LanguageInfoModel obj) => 
              (obj.FrontendId, obj.LanguageId, obj.AccessId, obj.SomeOtherUniqueValue).GetHashCode();
      }
      

      注意事项:

      CachedLookup 类在值和键上都是通用的。 ValueTuple 的示例使用使得拥有复合键变得容易。我还使用ValueTuples 来简化相等比较。

      ValueTask 的这种用法非常符合其预期目的,同步返回缓存列表。

      如果您有权访问较低级别的数据访问层,则一项优化是将重复数据删除移动到在对象实例化之前进行(基于属性值相等)。这将减少 GC 上的分配和负载。

      【讨论】:

        【解决方案4】:

        如果您可以控制完整的解决方案,那么您可以这样做。

        1. 能够存储在缓存中的任何对象。你必须确定这一点。 所有此类对象都实现了通用接口。

          public interface ICacheable 
          {
              string ObjectId(); // This will implement logic to calculate each object identity. You can count hash code but you have to add some other value to.
          }
          
        2. 现在,当您将对象存储在缓存中时。你做两件事。

          • 存储两种方式。就像一个缓存存储 ObjectId 到 Key。
          • 另一个将包含 ObjectId 到 Object。

          • 总体思路是当你得到对象时。您在第一个缓存中搜索,并看到您想要的键在 ObjectId 上。如果是,则无需进一步操作,否则您必须在 First Cache 中为 ObjectId 到键映射创建新条目。

          • 如果对象不存在,则必须在两个缓存中创建条目

        注意:您必须克服性能问题。因为您的键是某种列表,所以在搜索时会产生问题。

        【讨论】:

        • 感谢您的信息。如果我想存储 List 怎么办?
        • @RajmondBurgaj - 然后您将在此列表上构建一个包装器并再次实现 ICacheable 接口。请注意,如果您仍然需要 set 和 get 的原子性,使用两个缓存,您可能必须使用 lock 或其他一些同步机制来包装这些方法。
        【解决方案5】:

        听起来好像您需要实现某种索引。假设您的模型相当大,这就是您想要节省内存的原因,那么您可以使用两个并发字典来做到这一点。

        第一个是ConcurrentDictionary&lt;string, int&gt;(或适用于您的模型对象的任何唯一ID)并包含您的键值。根据您的所有组合,每个键显然不同,但您只是为所有对象复制 int 唯一键,而不是整个对象。

        第二个字典是ConcurrentDictionary&lt;int, object&gt;ConcurrentDictionary&lt;int, T&gt;,其中包含通过其唯一键索引的唯一大型对象。

        在构建缓存时,您需要填充两个字典,具体方法取决于您目前的操作方式。

        要检索对象,您可以像现在一样构建键,从第一个字典中检索哈希码值,然后使用它从第二个字典中找到实际对象。

        也可以使一个键无效而不使主对象无效另一个键也在使用它,尽管它确实需要您遍历索引字典以检查是否有任何其他键指向同一个对象。

        【讨论】:

        • 您不能将哈希码用作任何旧对象的唯一标识符。您将遇到哈希冲突,然后这一切都崩溃了。
        • @MineR 公平点,如果有很多很多,它们不能保证是唯一的。将进行编辑以明确该模型应该有一个唯一的 ID。
        【解决方案6】:

        我认为这不是一个缓存问题,其中一个键映射到一个且只有一个数据。在这种情况下,你的不是。您正在尝试将内存中的本地数据存储库作为缓存数据进行操作。 您正在尝试在从远程加载的键和对象之间创建映射器。一把钥匙能够映射到许多对象。一个对象可以被多个Key映射,所以关系是n n

        我已经创建了如下示例模式

        Key、KeyMyModel 和 MyModel 是缓存处理程序的类 RemoteModel 是您从远程服务获得的类

        使用此模型,您可以满足要求。这利用实体 ID 来指定一个对象,不需要散列来指定重复。这是我实现 set 方法的基础。 Invaildate a key 非常相似。您必须编写确保线程安全的代码

        public class MyModel
            {
                public RemoteModel RemoteModel { get; set; }
                public List<KeyMyModel> KeyMyModels { get; set; }
            }
            public class RemoteModel
            {
                public string Id { get; set; } // Identity property this get from remote service
                public string DummyProperty { get; set; } // Some properties returned by remote service
            }
            public class KeyMyModel
            {
                public string Key { get; set; }
                public string MyModelId { get; set; }
            }
            public class Key
            {
                public string KeyStr { get; set; }
                public List<KeyMyModel> KeyMyModels { get; set; }
            }
        
            public interface ICacheService
            {
                List<RemoteModel> Get(string key);
                List<RemoteModel> Get(string key, Func<List<RemoteModel>> getdata);
                Task<List<RemoteModel>> Get(string key, Func<Task<List<RemoteModel>>> getdata);
                void AddOrUpdate(string key, object value);
            }
        
            public class CacheService : ICacheService
            {
                public List<MyModel> MyModels { get; private set; }
                public List<Key> Keys { get; private set; }
                public List<KeyMyModel> KeyMyModels { get; private set; }
        
                public CacheService()
                {
                    MyModels = new List<MyModel>();
                    Keys = new List<Key>();
                    KeyMyModels = new List<KeyMyModel>();
                }
                public List<RemoteModel> Get(string key)
                {
                    return MyModels.Where(s => s.KeyMyModels.Any(t => t.Key == key)).Select(s => s.RemoteModel).ToList();
                }
        
                public List<RemoteModel> Get(string key, Func<List<RemoteModel>> getdata)
                {
                    var remoteData = getdata();
                    Set(key, remoteData);
        
                    return MyModels.Where(s => s.KeyMyModels.Any(t => t.Key == key)).Select(t => t.RemoteModel).ToList();
                }
        
                public Task<List<RemoteModel>> Get(string key, Func<Task<List<RemoteModel>>> getdata)
                {
                    throw new NotImplementedException();
                }
        
                public void AddOrUpdate(string key, object value)
                {
                    throw new NotImplementedException();
                }
        
                public void Invalidate(string key)
                {
        
                }
        
                public void Set(string key, List<RemoteModel> data)
                {
                    var Key = Keys.FirstOrDefault(s => s.KeyStr == key) ?? new Key()
                    {
                        KeyStr = key
                    };
        
                    foreach (var remoteModel in data)
                    {
                        var exist = MyModels.FirstOrDefault(s => s.RemoteModel.Id == remoteModel.Id);
                        if (exist == null)
                        {
                            // add data to the cache
                            var myModel = new MyModel()
                            {
                                RemoteModel = remoteModel
                            };
                            var keyMyModel = new KeyMyModel()
                            {
                                Key = key,
                                MyModelId = remoteModel.Id
                            };
                            myModel.KeyMyModels.Add(keyMyModel);
                            Key.KeyMyModels.Add(keyMyModel);
                            Keys.Add(Key);
                        }
                        else
                        {
                            exist.RemoteModel = remoteModel;
                            var existKeyMyModel =
                                KeyMyModels.FirstOrDefault(s => s.Key == key && s.MyModelId == exist.RemoteModel.Id);
                            if (existKeyMyModel == null)
                            {
                                existKeyMyModel = new KeyMyModel()
                                {
                                    Key = key,
                                    MyModelId = exist.RemoteModel.Id
                                };
                                Key.KeyMyModels.Add(existKeyMyModel);
                                exist.KeyMyModels.Add(existKeyMyModel);
                                KeyMyModels.Add(existKeyMyModel);
                            }
                        }
                    }
        
                    // Remove MyModels if need
                    var remoteIds = data.Select(s => s.Id);
                    var currentIds = KeyMyModels.Where(s => s.Key == key).Select(s => s.MyModelId);
                    var removingIds = currentIds.Except(remoteIds);
                    var removingKeyMyModels = KeyMyModels.Where(s => s.Key == key && removingIds.Any(i => i == s.MyModelId)).ToList();
                    removingKeyMyModels.ForEach(s =>
                    {
                        KeyMyModels.Remove(s);
                        Key.KeyMyModels.Remove(s);
                    });
                }
            }
        
            class CacheConsumer
            {
                private readonly CacheService _cacheService = new CacheService();
        
                public List<RemoteModel> GetMyModels(string frontendId, string languageId, string accessId)
                {
                    var key = $"{frontendId}_{languageId}_{accessId}";
                    return _cacheService.Get(key, () =>
                    {
                        // call to remote service here
                        return new List<RemoteModel>();
                    });
                }
            }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-09-06
          • 1970-01-01
          • 2023-01-08
          • 2011-10-09
          • 2020-03-24
          • 2017-01-13
          • 1970-01-01
          • 2014-06-14
          相关资源
          最近更新 更多