我正在使用.Net 3.5开发一个多线程应用程序,它从存储在数据库中的不同表中读取记录。读数非常频繁,因此需要延迟加载缓存实现。每个表都映射到一个C#类,并且有一个字符串列,可以用作缓存中的键。 此外,还要求定期刷新所有缓存的记录。 我可以在每次读取时使用锁定来实现缓存,以确保线程安全的环境,但后来我想到了另一种解决方案,它依赖于获取所有可能密钥列表的简单事实。
所以这是我写的第一个类,它存储了所有使用双重检查锁模式延迟加载的键的列表。它还有一个方法,用于在静态变量中存储上次请求的刷新的时间戳。
public class Globals
{
private static object _KeysLock = new object();
public static volatile List<string> Keys;
public static void LoadKeys()
{
if (Keys == null)
{
lock (_KeysLock)
{
if (Keys == null)
{
List<string> keys = new List<string>();
// Filling all possible keys from DB
// ...
Keys = keys;
}
}
}
}
private static long refreshTimeStamp = DateTime.Now.ToBinary();
public static DateTime RefreshTimeStamp
{
get { return DateTime.FromBinary(Interlocked.Read(ref refreshTimeStamp)); }
}
public static void NeedRefresh()
{
Interlocked.Exchange(ref refreshTimeStamp, DateTime.Now.ToBinary());
}
}
然后我写了CacheItem<T>
类,它是由键过滤的指定表T的单个缓存项的实现。它具有记录列表延迟加载的Load
方法和存储上次记录加载的时间戳的LoadingTimeStamp
属性。请注意,静态的记录列表会被当地填充的新记录覆盖,然后LoadingTimeStamp
也会被覆盖。
public class CacheItem<T>
{
private List<T> _records;
public List<T> Records
{
get { return _records; }
}
private long loadingTimestampTick;
public DateTime LoadingTimestamp
{
get { return DateTime.FromBinary(Interlocked.Read(ref loadingTimestampTick)); }
set { Interlocked.Exchange(ref loadingTimestampTick, value.ToBinary()); }
}
public void Load(string key)
{
List<T> records = new List<T>();
// Filling records from DB filtered on key
// ...
_records = records;
LoadingTimestamp = DateTime.Now;
}
}
最后这里是Cache<T>
类,它将表T的缓存存储为静态字典。如您所见,Get
方法首先加载缓存中的所有可能密钥(如果尚未完成),然后检查刷新的时间戳(两者都使用双重检查的锁模式完成)。即使存在另一个在锁内部进行刷新的线程,由Get
调用返回的实例中的记录列表也可以安全地被线程读取,因为刷新线程不会修改列表本身但会创建一个新的。
public class Cache<T>
{
private static object _CacheSynch = new object();
private static Dictionary<string, CacheItem<T>> _Cache = new Dictionary<string, CacheItem<T>>();
private static volatile bool _KeysLoaded = false;
public static CacheItem<T> Get(string key)
{
bool checkRefresh = true;
CacheItem<T> item = null;
if (!_KeysLoaded)
{
lock (_CacheSynch)
{
if (!_KeysLoaded)
{
Globals.LoadKeys(); // Checks the lazy loading of the common key list
foreach (var k in Globals.Keys)
{
item = new CacheItem<T>();
if (k == key)
{
// As long as the lock is acquired let's load records for the requested key
item.Load(key);
// then the refresh is no more needed by the current thread
checkRefresh = false;
}
_Cache.Add(k, item);
}
_KeysLoaded = true;
}
}
}
// here the key is certainly contained in the cache
item = _Cache[key];
if (checkRefresh)
{
// let's check the timestamps to know if refresh is needed
DateTime rts = Globals.RefreshTimeStamp;
if (item.LoadingTimestamp < rts)
{
lock (_CacheSynch)
{
if (item.LoadingTimestamp < rts)
{
// refresh is needed
item.Load(key);
}
}
}
}
return item;
}
}
定期调用Globals.NeedRefresh()
以确保记录将被刷新。
此解决方案可以避免每次读取缓存时锁定,因为缓存预先填充了所有可能的密钥:这意味着内存中将存在多个实例,这些实例等于所有可能密钥的数量(大约20个) )对于每个请求的类型T(所有T类型大约为100),但仅对于请求的键,记录列表不为空。
如果此解决方案存在某些线程安全问题或任何错误,请告诉我。
非常感谢你。
答案 0 :(得分:0)
鉴于:
List<T>
字段。你根本不需要任何锁,所以可以简化代码。
锁定的唯一可能需要是防止并发尝试运行CacheItem.Load
。就个人而言,我只是让并发数据库访问运行,但如果你想阻止它,你可以在CacheItem.Load
中实现一个锁。或者从.NET 4中捏Lazy<T>
并按照我对your previous question的回答中的建议使用它。
另一个评论是你的刷新逻辑使用DateTime.Now
,因此在夏令时结束时的时间段和(b)如果系统时钟是更新。
我只想使用一个静态整数值,每次调用NeedRefresh
时都会递增。
来自评论:
例如,如果两个线程...尝试同时加载常见的Globals.Keys会发生什么?“
在应用程序启动时可能会发生这种情况的风险很小,但那又如何呢?这将导致从数据库中读取20个密钥两次,但性能影响可能可以忽略不计。如果你真的想要阻止这种情况,任何锁定都可以封装在像Lazy<T>
这样的类中。
关于使用DateTime.Now的评论实际上是一个兴趣点,但我想也许我可以假设这些事件可以在应用程序未使用时发生。
你可以“假设”,但不能保证。您的机器可能决定随时将它与时间服务器同步。
关于在NeedRefresh中使用整数的建议我不明白如何将它与每个由DateTime表示的记录列表状态进行比较。
据我所知,您只能使用DateTime
检查您的数据是否在最近一次调用NeedRefresh
之前或之后加载。所以你可以用以下方法替换它:
public static class Globals
{
...
public static int Version { get {return _version; } }
private static int _version;
public static void NeedRefresh()
{
Interlocked.Increment(ref _version);
}
}
public class CacheItem<T>
{
public int Version {get; private set; }
...
public void Load(string key)
{
Version = Globals.Version;
List<T> records = new List<T>();
// Filling records from DB filtered on key
// ...
_records = records;
}
}
然后访问缓存时:
item = _Cache[key];
if (item.Version < Globals.Version) item.Load();
**更新2 **
回应评论:
...如果一个线程试图读取字典而另一个线程在锁内部添加项目,则可能存在完整的完整风险,不可能?
现有代码仅在加载全局密钥后立即将所有密钥添加到字典中,然后从不修改字典。所以这是线程安全的,只要在完全构造字典之前不分配_Cache属性:
var dictionary = new Dictionary<string, CacheItem<T>>(Global.Keys.Count);
foreach (var k in Globals.Keys)
{
dictionary.Add(k, new CacheItem<T>());
}
_Cache = dictionary;