使用C#中的多个键优化缓存-删除对象重复项

时间:2018-07-12 14:28:23

标签: c# caching asp.net-core

我在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<string, object>,因此它并不那么复杂,只需从此字典中存储和检索数据即可。在我的一项服务中,我有一种如下方法:

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<List<string>, object>

有人知道解决此问题的好方法,即不复制缓存中的对象吗?

编辑1:

我主要担心的是,当我从Web服务中检索List<MyModel>时,因为它们可能有80%的对象具有相同的数据,这将大大增加内存的大小。但这也与简单情况有关。 恐怕我有这样的东西:

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

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

_cache.Invalidate("key2");

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

6 个答案:

答案 0 :(得分:7)

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

  1. 为每个组合执行呼叫,并且
  2. 存储相同结果的n倍,浪费了大量内存

对于1,我不知道如何防止它,因为在执行之前我们不知道是否要在此设置中获取重复项。我们将需要更多基于这些值何时变化(可能或不可能)的信息。

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

答案 1 :(得分:4)

此答案专门用于返回List<TItem>,而不仅仅是返回单个TItem,并且避免了任何TItem和任何List<T>的重复。它使用数组,因为您正试图节省内存,并且数组使用的空间少于List

请注意,要使此(以及所有实际的解决方案)正常工作,您必须覆盖Equals上的GetHashCodeTItem,以便它知道重复的项目。 (除非数据提供者每次都返回相同的对象,这不太可能。)如果您没有TItem的控制权,但是可以自己确定两个TItem是否相等,则可以使用IEqualityComparer可以做到这一点,但是下面的解决方案需要做一些很小的修改才能做到这一点。

通过以下基本测试来查看解决方案: 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;
    }
}

在我看来,如果ReaderWriterLockSlim导致性能下降,那么lock(ListList)会比lock更好,但是它要复杂得多。

答案 2 :(得分: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的示例可以轻松地使用复合键。我还使用了ValueTuple来简化相等比较。

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

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

答案 3 :(得分:1)

如果您可以控制完整的解决方案,则可以执行以下操作。

  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. 现在将对象存储在Cache中时。你做两件事。

    • 存储两种方式的东西。就像一个将ObjectId存储到Key的缓存一样。
    • 另一个将包含对象的ObjectId。

    • 总体思路是,当您获得对象时。您在第一个缓存中搜索,然后看到想要的键针对ObjectId。如果是,则无需采取进一步措施,否则您必须在“第一缓存”中为ObjectId到Key Map创建新条目。

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

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

答案 4 :(得分:1)

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

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

第二个字典将是ConcurrentDictionary<int, object>ConcurrentDictionary<int, T>,其中将包含通过其唯一键索引的唯一大对象。

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

要检索对象,您需要像现在一样构建密钥,从第一个字典中检索哈希码值,然后使用该值从第二个字典中查找实际对象。

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

答案 5 :(得分:1)

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

我已经创建了一个如下的模态

enter image description here

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

使用此模型,您可以满足要求。这利用实体ID来指定对象,不需要哈希来指定重复项。我已经实现了set方法,这是非常基本的。调用密钥非常相似。您必须编写确保线程安全的代码

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>();
            });
        }
    }