我目前正在试用.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()
已经破坏了引用,但似乎仍然存在更改。我敢肯定这很明显,但有人能发现我出错的地方吗?
答案 0 :(得分: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);
}
}
}
}
答案 1 :(得分:0)
在内存中缓存的对象存储在与缓存客户端进程相同的进程空间中。当缓存客户端请求缓存对象时,客户端将接收对本地缓存对象的引用,而不是副本。
获取对象的干净副本的唯一方法是实现自定义克隆机制(ICloneable,Serialization,Automapping,...)。使用该副本,您将能够在不更改父对象的情况下更改新对象。
根据您的使用情况,通常不建议更新缓存中的对象。
答案 2 :(得分:0)
为什么不直接存储为json或字符串?这些不是通过引用传递的,当你离开缓存时,你将获得一个新副本:)我在这里受到挑战,因为那就是我在做什么!
答案 3 :(得分:0)
如果再次反序列化并序列化并获取缓存对象,可以更轻松地执行此操作&#34;按值&#34;。
您可以使用Newtonsoft lib(只需从NuGet获取)
这样做var cacheObj = HttpRuntime.Cache.Get(CACHEKEY);
var json = JsonConvert.SerializeObject(cacheObj);
var byValueObj = JsonConvert.DeserializeObject<List<string>>(json);
return byValueObj;
答案 4 :(得分:0)
序列化/反序列化将解决问题,但同时它会破坏在内存中拥有对象的作用。缓存的作用是提供对存储对象的快速访问,我们在此处添加反序列化开销。由于需要反序列化,我建议将缓存作为服务,例如redis缓存,它将集中在一起,因此您不必拥有每个工作进程的内存对象副本,反正无需反序列化。
在这种情况下,您选择了快速序列化/反序列化选项的关键。