如何分离 MemoryCache 上的对象引用

Posted

技术标签:

【中文标题】如何分离 MemoryCache 上的对象引用【英文标题】:How can I detach the object reference on MemoryCache 【发布时间】:2012-12-05 18:17:09 【问题描述】:

我目前正在 .Net 4 中试用新的 MemoryCache,以便在我们的一个应用程序中缓存一些数据。我遇到的问题是对象已更新,并且缓存似乎正在保留更改,例如

public IEnumerable<SomeObject> GetFromDatabase()
    const string _cacheKeyGetDisplayTree = "SomeKey"; 
    ObjectCache _cache = MemoryCache.Default;
    var objectInCache = _cache.Get(_cacheKeyGetDisplayTree) as IEnumerable<SomeObject>;
    if (objectInCache != null)
        return objectInCache.ToList();

    // Do something to get the items
    _cache.Add(_cacheKeyGetDisplayTree, categories, new DateTimeOffset(DateTime.UtcNow.AddHours(1)));

    return categories.ToList();


public IEnumerable<SomeObject> GetWithIndentation()
    var categories = GetFromDatabase();

    foreach (var c in categories)
    
        c.Name = "-" + c.Name;
    

    return categories;

如果我先调用GetWithIndentation(),然后再调用GetFromDatabase(),我希望它会返回SomeObject 的原始列表,但它会返回修改后的项目(名称前带有“-”前缀)。

我以为ToList() 破坏了引用,但它似乎仍然保留了更改。我敢肯定这很明显,但谁能发现我哪里出错了?

【问题讨论】:

您正在制作集合的副本,而不是其中的对象。 啊,好地方,谢谢。现在我想知道解决这个问题的最佳方法是进行深层复制还是有办法告诉 MemoryCache 忽略后续更改? 要么在缓存之前修改它,要么从不修改。除此之外,这是一个微不足道的更改,它可能应该在您的表示层中完成。 出于解释的原因,我已经简化了代码,GetWithIndentation() 中还有很多内容,并且可以从多个地方访问。我想我可以缓存GetWithIndentation() 的输出,但GetFromDatabase() 是从其他方法访问的,因此我觉得它是缓存数据库数据的好地方。 好吧,在这种情况下,您要么必须维护 2 个缓存,要么 GetWithIndentation 需要克隆类别对象并在那里应用更改。 【参考方案1】:

我创建了一个 ReadonlyMemoryCache 类来解决这个问题。它继承自 .NET 4.0 MemoryCache,但对象以只读方式(按值)存储且无法修改。我在使用二进制序列化存储之前对对象进行深度复制。

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Runtime.Caching;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading.Tasks;


namespace ReadOnlyCache

    class Program
    

        static void Main()
        
            Start();
            Console.ReadLine();
        

        private static async void Start() 
            while (true)
            
                TestMemoryCache();
                await Task.Delay(TimeSpan.FromSeconds(1));
            
        

        private static void TestMemoryCache() 
            List<Item> items = null;
            string cacheIdentifier = "items";

            var cache = ReadonlyMemoryCache.Default;

            //change to MemoryCache to understand the problem
            //var cache = MemoryCache.Default;

            if (cache.Contains(cacheIdentifier))
            
                items = cache.Get(cacheIdentifier) as List<Item>;
                Console.WriteLine("Got 0 items from cache: 1", items.Count, string.Join(", ", items));

                //modify after getting from cache, cached items will remain unchanged
                items[0].Value = DateTime.Now.Millisecond.ToString();

            
            if (items == null)
            
                items = new List<Item>()  new Item()  Value = "Steve" , new Item()  Value = "Lisa" , new Item()  Value = "Bob"  ;
                Console.WriteLine("Reading 0 items from disk and caching", items.Count);

                //cache for x seconds
                var policy = new CacheItemPolicy()  AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddSeconds(5)) ;
                cache.Add(cacheIdentifier, items, policy);

                //modify after writing to cache, cached items will remain unchanged
                items[1].Value = DateTime.Now.Millisecond.ToString();
            
        
    

    //cached items must be serializable

    [Serializable]
    class Item 
        public string Value  get; set; 
        public override string ToString()  return Value; 
    

    /// <summary>
    /// Readonly version of MemoryCache. Objects will always be returned in-value, via a deep copy.
    /// Objects requrements: [Serializable] and sometimes have a deserialization constructor (see http://***.com/a/5017346/2440) 
    /// </summary>
    public class ReadonlyMemoryCache : MemoryCache
    

        public ReadonlyMemoryCache(string name, NameValueCollection config = null) : base(name, config) 
        

        private static ReadonlyMemoryCache def = new ReadonlyMemoryCache("readonlydefault");

        public new static ReadonlyMemoryCache Default 
            get
            
                if (def == null)
                    def = new ReadonlyMemoryCache("readonlydefault");
                return def;
            
        

        //we must run deepcopy when adding, otherwise items can be changed after the add() but before the get()

        public new bool Add(CacheItem item, CacheItemPolicy policy)
        
            return base.Add(item.DeepCopy(), policy);
        

        public new object AddOrGetExisting(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null)
        
            return base.AddOrGetExisting(key, value.DeepCopy(), absoluteExpiration, regionName);
        

        public new CacheItem AddOrGetExisting(CacheItem item, CacheItemPolicy policy)
        
            return base.AddOrGetExisting(item.DeepCopy(), policy);
        

        public new object AddOrGetExisting(string key, object value, CacheItemPolicy policy, string regionName = null)
        
            return base.AddOrGetExisting(key, value.DeepCopy(), policy, regionName);
        

        //methods from ObjectCache

        public new bool Add(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null)
        
            return base.Add(key, value.DeepCopy(), absoluteExpiration, regionName);
        

        public new bool Add(string key, object value, CacheItemPolicy policy, string regionName = null)
        
            return base.Add(key, value.DeepCopy(), policy, regionName);
        

        //for unknown reasons, we also need deepcopy when GETTING values, even though we run deepcopy on all (??) set methods.

        public new object Get(string key, string regionName = null)
        
            var item = base.Get(key, regionName);
            return item.DeepCopy();
        

        public new CacheItem GetCacheItem(string key, string regionName = null)
        
            var item = base.GetCacheItem(key, regionName);
            return item.DeepCopy();
        

    


    public static class DeepCopyExtentionMethods
    
        /// <summary>
        /// Creates a deep copy of an object. Must be [Serializable] and sometimes have a deserialization constructor (see http://***.com/a/5017346/2440) 
        /// </summary>
        public static T DeepCopy<T>(this T obj)
        
            using (var ms = new MemoryStream())
            
                var formatter = new BinaryFormatter();
                formatter.Serialize(ms, obj);
                ms.Position = 0;

                return (T)formatter.Deserialize(ms);
            
        
    




【讨论】:

这个看起来像个赢家,DeepCopyExtensionMethods 类解决了这个问题。 @GrantWinney 如果您从 Get() 中删除 deepcopy,您会发现它不起作用。我同意这不应该是必要的。【参考方案2】:

在内存中,缓存对象存储在与缓存客户端进程相同的进程空间中。当缓存客户端请求缓存对象时,客户端会收到对本地缓存对象的引用,而不是副本。

获得对象的干净副本的唯一方法是实现自定义克隆机制(ICloneable、序列化、自动映射......)。使用该副本,您将能够在不更改父对象的情况下更改新对象。

根据您的使用情况,通常不建议更新缓存中的对象。

【讨论】:

"根据您的使用情况,通常不建议更新缓存中的对象。"您介意详细说明这是为什么吗?举个简单的例子,假设您有一个存储在缓存中的用户列表。用户很少会更改,但他们会在某些时候更改(例如更新名字/姓氏、地址等) - 这是否适合将用户集合存储在缓存中并在需要时更新它们?【参考方案3】:

如果您再次反序列化和序列化并“按值”获取缓存对象,则可以更轻松。

你可以用 Newtonsoft lib 做这样的事情(只需从 NuGet 获取)

var cacheObj = HttpRuntime.Cache.Get(CACHEKEY);
var json = JsonConvert.SerializeObject(cacheObj);
var byValueObj = JsonConvert.DeserializeObject<List<string>>(json);
return byValueObj;

【讨论】:

【参考方案4】:

为什么不直接存储为 json 或字符串?这些不是通过引用传递的,当您从缓存中取出时,您将获得一个新副本:) 我来这里是为了接受挑战,因为这就是我正在做的 atm!

【讨论】:

因为每次Get都要支付反序列化成本【参考方案5】:

序列化/反序列化将解决问题,但同时它破坏了在内存中拥有对象的提议。缓存的作用是提供对存储对象的快速访问,我们在这里增加了反序列化开销。由于需要反序列化,我建议将缓存作为服务,例如 redis 缓存,它将是集中式的,因此您不必为每个工作进程复制内存对象,并且反序列化无论如何都已完成。

在这种情况下,关键是您选择了快速序列化/反序列化选项。

【讨论】:

以上是关于如何分离 MemoryCache 上的对象引用的主要内容,如果未能解决你的问题,请参考以下文章

一个实体对象不能被多个 IEntityChangeTracker 实例引用,即使我分离了它

如何清除内存缓存?

如何禁用分离实例关系上的 SQLAlchemy 延迟加载?

自定义视图上的数据绑定“无法在空引用对象上引用 .setTag”

一个简单的MemoryCache的实现

MemoryCache类使用记录