我正在为一个MVC Web应用程序的缓存管理器工作。对于这个应用程序,我有一些非常大的对象,构建成本很高。在应用程序生命周期中,我可能需要根据用户请求创建其中的几个对象。构建时,用户将使用对象中的数据,从而导致许多读取操作。有时,我需要更新缓存对象中的一些次要数据点(创建和替换将花费太多时间)。
下面是我创建的缓存管理器类,以帮助我。除了基本的线程安全之外,我的目标是:
允许缓存存储许多对象,并保持锁定 每个对象(而不是所有对象的一个锁)。
public class CacheManager
{
private static readonly ObjectCache Cache = MemoryCache.Default;
private static readonly ConcurrentDictionary<string, ReaderWriterLockSlim>
Locks = new ConcurrentDictionary<string, ReaderWriterLockSlim>();
private const int CacheLengthInHours = 1;
public object AddOrGetExisting(string key, Func<object> factoryMethod)
{
Locks.GetOrAdd(key, new ReaderWriterLockSlim());
var policy = new CacheItemPolicy
{
AbsoluteExpiration = DateTimeOffset.Now.AddHours(CacheLengthInHours)
};
return Cache.AddOrGetExisting
(key, new Lazy<object>(factoryMethod), policy);
}
public object Get(string key)
{
var targetLock = AcquireLockObject(key);
if (targetLock != null)
{
targetLock.EnterReadLock();
try
{
var cacheItem = Cache.GetCacheItem(key);
if(cacheItem!= null)
return cacheItem.Value;
}
finally
{
targetLock.ExitReadLock();
}
}
return null;
}
public void Update<T>(string key, Func<T, object> updateMethod)
{
var targetLock = AcquireLockObject(key);
var targetItem = (Lazy<object>) Get(key);
if (targetLock == null || key == null) return;
targetLock.EnterWriteLock();
try
{
updateMethod((T)targetItem.Value);
}
finally
{
targetLock.ExitWriteLock();
}
}
private ReaderWriterLockSlim AcquireLockObject(string key)
{
return Locks.ContainsKey(key) ? Locks[key] : null;
}
}
我是否在保持线程安全的同时完成目标?你们都看到了实现目标的更好方法吗?
谢谢!
更新:所以这里的底线是我真的想在一个区域做太多。出于某种原因,我确信在管理缓存的同一个类中管理Get / Update操作是个好主意。看完Groo的解决方案&amp;重新考虑这个问题,我能够进行大量的重构,从而消除了我面临的这个问题。
答案 0 :(得分:1)
好吧,我认为这门课没有你所需要的。
允许对该对象进行多次读取,但在更新请求时锁定所有读取
您可以将所有读取锁定到缓存管理器,但是您没有锁定对实际缓存实例的读取(也不更新)。
确保对象仅在不存在的情况下创建一次(请记住它是一个很长的构建操作)。
我认为你不能确保这一点。在将对象添加到字典时,您没有锁定任何内容(此外,您正在添加一个惰性构造函数,因此您甚至不知道该对象何时将被实例化。)
编辑:此部分成立,我唯一要做的就是让Get
返回Lazy<object>
。在编写我的程序时,我忘了转换它并在返回的值上调用ToString
“”未创建值“。
允许缓存存储许多对象,并保持每个对象的锁定(而不是所有对象的锁定)。
这与第1点相同:您正在锁定字典,而不是对对象的访问。并且您的update
委托有一个奇怪的签名(它接受一个类型化的泛型参数,并返回一个从未使用过的object
)。这意味着您实际上正在修改对象的属性,并且这些更改会立即显示给程序中包含对该对象的引用的任何部分。
如何解决此问题
如果您的对象是可变的(并且我认为它是),则无法确保事务一致性,除非您的每个属性也获得对每个读取访问的锁定。简化这种方法的一种方法是使其不可变(这就是为什么这些在多线程中非常流行)。
或者,您可以考虑将这个大型对象分成小块并分别缓存每个部分,如果需要,使它们不可变。
[编辑]添加了竞争条件示例:
class Program
{
static void Main(string[] args)
{
CacheManager cache = new CacheManager();
cache.AddOrGetExisting("item", () => new Test());
// let one thread modify the item
ThreadPool.QueueUserWorkItem(s =>
{
Thread.Sleep(250);
cache.Update<Test>("item", i =>
{
i.First = "CHANGED";
Thread.Sleep(500);
i.Second = "CHANGED";
return i;
});
});
// let one thread just read the item and print it
ThreadPool.QueueUserWorkItem(s =>
{
var item = ((Lazy<object>)cache.Get("item")).Value;
Log(item.ToString());
Thread.Sleep(500);
Log(item.ToString());
});
Console.Read();
}
class Test
{
private string _first = "Initial value";
public string First
{
get { return _first; }
set { _first = value; Log("First", value); }
}
private string _second = "Initial value";
public string Second
{
get { return _second; }
set { _second = value; Log("Second", value); }
}
public override string ToString()
{
return string.Format("--> PRINTING: First: [{0}], Second: [{1}]", First, Second);
}
}
private static void Log(string message)
{
Console.WriteLine("Thread {0}: {1}", Thread.CurrentThread.ManagedThreadId, message);
}
private static void Log(string property, string value)
{
Console.WriteLine("Thread {0}: {1} property was changed to [{2}]", Thread.CurrentThread.ManagedThreadId, property, value);
}
}
这样的事情应该发生:
t = 0ms : thread A gets the item and prints the initial value
t = 250ms: thread B modifies the first property
t = 500ms: thread A prints the INCONSISTENT value (only the first prop. changed)
t = 750ms: thread B modifies the second property