【问题标题】:How to cache an object with multiple keys in C#如何在 C# 中缓存具有多个键的对象
【发布时间】:2020-03-24 22:59:18
【问题描述】:

我的模型在 2 个或多个属性中是独一无二的。例如,Entity 类的对象在名称和 ID 上都是唯一的。

public class Entity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

我有一个模型存储库:

public class EntityRepository
{
    ...
    public Entity GetById(int id)
    {
        return db.GetById(id);
    }
    public Entity GetByName(string name)
    {
        return db.GetByName(name);
    }
}

缓存对GetById 的调用和使用Microsoft.Extensions.Caching.Memory.IMemoryCacheGetByName 的调用的最佳方法是什么?

目前的解决方案:

public class EntityRepository
{
    ...
    public Entity GetById(int id)
    {
        return Cache.GetOrCreate($"id:{id}", cacheEntry =>
        {
            return db.GetById(id);
        });
    }
    public Entity GetByName(string name)
    {
        return Cache.GetOrCreate($"name:{name}", cacheEntry =>
        {
            return db.GetByName(name);
        });
    }
    public void RemoveById(int id)
    {
        db.RemoveById(id);
        Cache.Remove($"id:{id}");
    }
}

这里的问题是,如果我通过它的 ID 删除一个实体,我可以通过 ID 从缓存中删除它,但它仍然会与另一个键一起存在。更新实体也有类似的问题。

有没有比将对象两次保存在缓存中更好的解决方案?

【问题讨论】:

    标签: c# asp.net-core caching .net-core memorycache


    【解决方案1】:

    在你的情况下,我会以不同的方式缓存,基本上,我会将缓存的条目保存在缓存中的列表中,然后检索列表并在那里搜索。

    如果您认为列表太大而无法搜索,那么出于性能原因,您可能需要进行一些分区。

    一般的例子如下。

    public class EntityRepository
    {
    
        public Entity GetById(int id)
        {
            List<Entity> entities = Cache.GetOrCreate($"entities", cacheEntry =>
            {
                // Create an empty list of entities
                return new List<Entity>();
            });
    
            // Look for the entity
            var entity = entities.Where(e => e.id == id).FirstOrDefault();
            // if not there, then add it to the cached list
            if (entity == null)
            {
                entity = db.GetById(id);
                entities.Add(entity)
                // Update the cache
                Cache.Set($"entities", entities);
            }
            return entity;
        }
        public Entity GetByName(string name)
        {
            // Same thing with id    
        }
        public void RemoveById(int id)
        {
            // load the list, remove item and update cache
        }
    }
    

    在任何其他情况下,您都需要使用某种逻辑来围绕您的实现。也许是多键字典,或者某种数据结构来保留历史记录并进行自定义清理。不过没有什么是开箱即用的。

    您还可以通过将实体列表提取到 getter 和 setter 中来简化代码和重复,如下所示:

    public List<Entity> Entities
    {
        get { return Cache.GetOrCreate($"entities", cacheEntry =>
                     {
                         // Create an empty list of entities
                         return new List<Entity>();
                     }); 
        }
        set { Cache.Set($"entities", value); }
    }
    

    【讨论】:

    • 但是使用这种方法,我如何使缓存的实体过期?如果我为缓存设置了大小限制,当达到限制时,整个列表将被删除,并且无法为单个实体设置过期时间。
    • 确实没有。应在整个列表中设置过期时间。因此,根据您的实施,也许您应该为不同的用户创建不同的列表并为每个用户过期。
    • 这就是为什么我提到分区来处理这些情况。另一种方法是根据它们的参考记录项目并逐个删除。
    • Dictionary&lt;Entity, List&lt;string&gt;&gt; entityMap。当您将某些内容添加到缓存中时,您会将键添加到字符串列表中,例如“entityMap[entity].Add(key)”。删除时,您检索密钥并删除所有内容。
    【解决方案2】:

    内存缓存将缓存项存储为键值对,因此我们可以利用这一点。

    所以不要这样做:

    public Entity GetById(int id)
    {
        return Cache.GetOrCreate($"id:{id}", cacheEntry =>
        {
            return db.GetById(id);
        });
    }
    

    你可以这样做:

    public Entity GetById(int id)
    {
        string _name = string.Empty;
    
        if (!_cache.TryGetValue(id, out _name))
        {
            var _entity = db.GetById(id);
    
            _cache.Set(_entity.Id, _entity.Name);
        }
    
        return _cache.Get<Entity>(id);
    }
    

    在获取时,它会检查缓存是否存在,然后它会从源中获取值并将其存储在缓存中,然后它会返回缓存。

    现在,由于您还需要检查 GetByName 方法上的值而不是键,因此您可以将缓存转换为字典,并使用 Linq 按其值检索键。

        public Entity GetByName(string name)
        {
            int id;
    
            var dic = _cache as IDictionary<int, string>;
    
            if (!dic.Values.Contains(name))
            {
                var _entity = db.GetByName(name);
    
                _cache.Set(_entity.Id, _entity.Name);
    
                id = _entity.Id;
            }
            else
            {
                id = dic.FirstOrDefault(x => x.Value == name).Key;
            }
    
            return _cache.Get<Entity>(id);
        }
    

    现在,当您使用 RemoveById 时,您只需传递 id:

    public void RemoveById(int id)
    {
        db.RemoveById(id);
        _cache.Remove(id);
    }
    

    我没有测试上面的代码,我只是想给你一些见解,可以引导你得到你想要的结果。

    【讨论】:

    • 但是缓存变大不会有问题吗? FirstOrDefaultValues.Contains 执行线性搜索,因此它会比通过键(id)访问要慢得多。
    • @GurGaller Dictionary 被优化为通过键获取价值,因此反向操作会降低您的性能,尤其是在大数据中,因此,如果您正在使用大缓存,我建议构建用于搜索缓存值(而不是键)的自定义解决方案。(也许构建您自己的字典)。
    【解决方案3】:

    不确定是否有比在缓存中存储两次对象更好的解决方案,但有一个简单的删除/更新解决方案。首先,通过Id 获取一个对象,然后通过已知属性值删除其所有缓存条目

    public class EntityRepository
    {
        public Entity GetById(int id)
        {
            return Cache.GetOrCreate($"id:{id}", cacheEntry =>
            {
                return db.GetById(id);
            });
        }
        public Entity GetByName(string name)
        {
            return Cache.GetOrCreate($"name:{name}", cacheEntry =>
            {
                return db.GetByName(name);
            });
        }
        public void RemoveById(int id)
        {
            db.RemoveById(id);
            if (Cache.TryGetValue(id, out Entity entity))
            {
                Cache.Remove($"id:{entity.Id}");
                Cache.Remove($"name:{entity.Name}");
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-08-10
      • 1970-01-01
      • 2011-08-27
      • 1970-01-01
      相关资源
      最近更新 更多