具有已知密钥列表的延迟加载缓存的多线程实现

时间:2012-09-18 14:13:39

标签: c# c#-3.0

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

1 个答案:

答案 0 :(得分:0)

鉴于:

  • 您加载所有密钥一次,永远不会更改它们
  • 您创建一个字典并且永远不会更改
  • CacheItem.Load是线程安全的,因为它只使用新的完全初始化列表替换私有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;