我正在使用.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),但仅对于请求的键,记录列表不为空。请让我知道此解决方案是否存在线程安全问题或任何错误。非常感谢。