缓存和线程安全性

3
我正在通过System.Web.Caching.Cache类在ASP.NET网站中缓存数据,因为检索数据非常耗时,且只有在内容人员更改后端数据时才会偶尔更改。

因此,在Application_Start中创建数据并将其存储在Cache中,到期时间为1天。

在访问数据时(在网站的许多页面上都会发生),我现在使用一个静态的CachedData类,类似于以下代码:

public static List<Kategorie> GetKategorieTitelListe(Cache appCache)
{
    // get Data out of Cache
    List<Kategorie> katList = appCache[CachedData.NaviDataKey] as List<Kategorie>;
    // Cache expired, retrieve and store again
    if (katList == null)
    {
            katList = DataTools.BuildKategorienTitelListe();
            appCache.Insert(CachedData.NaviDataKey, katList, null, DateTime.Now.AddDays(1d), Cache.NoSlidingExpiration);
    }
    return katList;
}

我看到这段代码存在的问题是不具有线程安全性。

如果两个用户同时打开这两个页面并且缓存刚好过期,那么就有可能会多次检索到数据。

但是如果我锁定方法体,性能就会遇到麻烦,因为只有一个用户可以获取数据列表。

有没有一种简单的方法来防止这种情况?像这种情况下最佳实践是什么?

3个回答

4

您说得对,您的代码不是线程安全的。

// this must be class level variable!!!
private static readonly object locker = new object();

    public static List<Kategorie> GetKategorieTitelListe(Cache appCache)
    {
        // get Data out of Cache
        List<Kategorie> katList = appCache[CachedData.NaviDataKey] as List<Kategorie>;

        // Cache expired, retrieve and store again
        if (katList == null)
        {
            lock (locker)
            {
                katList = appCache[CachedData.NaviDataKey] as List<Kategorie>;

                if (katlist == null)  // make sure that waiting thread is not executing second time
                {
                    katList = DataTools.BuildKategorienTitelListe();
                    appCache.Insert(CachedData.NaviDataKey, katList, null, DateTime.Now.AddDays(1d), Cache.NoSlidingExpiration);
                }
            }
        }
        return katList;
    }

如果两个线程在数据为空时同时进入该方法,则第一个线程将进入锁定状态。第二个线程将等待锁定“解锁”,然后再进入。在这种情况下,katlist仍将为空,因此将调用DataTools.BuildKetegorienTiteListe两次。因此,我认为lock语句必须包含该方法的所有内容,除了return - Johnny5
2
实际上不是这样的。在您的情况下,当两个线程进入锁时,只有一个线程会进入锁定状态,而第二个线程将等待。因此,当第一个线程加载缓存并退出(解锁)时,第二个线程将进入,这就是为什么您有第二个if(katlist == null)的原因,因此由于第一个线程已经更新了它,它将不会为空,第二个线程也不会重新加载缓存。这是同步的最安全方式。这被称为双重检查锁定。请在http://en.wikipedia.org/wiki/Double-checked_locking上阅读更多信息。 - Vlad Bezden
2
在您从维基百科中提供的示例中,双重检查是在静态布尔值上完成的。在您提供的代码中,“katList”只是一个本地变量。如果在进入锁之前它为null,那么在进入锁时它仍将为null。 - Johnny5
1
好的,我没有意识到这是局部变量,而不是类或全局变量。给你加1。我会修改我的代码。 - Vlad Bezden
这个有性能问题吗?或者第一个用户获取缓存后,所有后续用户都被释放到它上面。这种设置会导致网站崩溃吗? - Mike Flynn

0

MSDN 文档指出,ASP.NET Cache类是线程安全的,这意味着它们的内容可以被应用程序域中的任何线程自由访问(例如读/写将是原子性的)。

只需记住,随着缓存大小的增长,同步的成本也会增加。您可能需要查看帖子

通过添加一个私有对象来锁定,您应该能够安全地运行您的方法,以便其他线程不会干扰。

private static readonly myLockObject = new object();

public static List<Kategorie> GetKategorieTitelListe(Cache appCache)
{
    // get Data out of Cache
    List<Kategorie> katList = appCache[CachedData.NaviDataKey] as List<Kategorie>;

    lock (myLockObject)
    {
        // Cache expired, retrieve and store again
        if (katList == null)
        {
            katList = DataTools.BuildKategorienTitelListe();
            appCache.Insert(CachedData.NaviDataKey, katList, null, DateTime.Now.AddDays(1d), Cache.NoSlidingExpiration);
        }
        return katList;
    }
}

0

我看不到除了锁定之外的其他解决方案。

private static readonly object _locker = new object ();

public static List<Kategorie> GetKategorieTitelListe(Cache appCache)
{
    List<Kategorie> katList;

    lock (_locker)
    {
        // get Data out of Cache
        katList = appCache[CachedData.NaviDataKey] as List<Kategorie>;
        // Cache expired, retrieve and store again
        if (katList == null)
        {
                katList = DataTools.BuildKategorienTitelListe();
                appCache.Insert(CachedData.NaviDataKey, katList, null, DateTime.Now.AddDays(1d), Cache.NoSlidingExpiration);
        }
    }
    return katList;
}

一旦数据存储在缓存中,同时进行的线程只需等待将数据取出的时间,即这行代码:

katList = appCache[CachedData.NaviDataKey] as List<Kategorie>;

因此性能成本不会太高。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接