15

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

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()破坏了参考,但它似乎仍然保留了更改。我敢肯定这很明显,但谁能发现我哪里出错了?

4

5 回答 5

7

我创建了一个 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://stackoverflow.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://stackoverflow.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);
            }
        }
    }



}
于 2014-04-14T11:14:01.830 回答
1

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

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

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

于 2013-01-11T12:45:10.380 回答
1

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

您可以使用 Newtonsoft lib 执行此操作(只需从 NuGet 获取)

var cacheObj = HttpRuntime.Cache.Get(CACHEKEY);
var json = JsonConvert.SerializeObject(cacheObj);
var byValueObj = JsonConvert.DeserializeObject<List<string>>(json);
return byValueObj;
于 2017-04-12T11:31:16.407 回答
0

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

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

于 2018-04-18T04:41:39.363 回答
0

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

于 2016-05-18T15:05:49.493 回答