C# 缓存和线程安全

C# 缓存和线程安全,c#,asp.net,caching,thread-safety,C#,Asp.net,Caching,Thread Safety,我通过System.Web.caching.Cache-Class在ASP.NET网站中缓存数据,因为检索数据的成本非常高,而且当我们的内容人员在后端更改数据时,数据只会偶尔更改一次 所以我在Application_Start中创建数据并将其存储在缓存中,过期时间为1天 当访问数据(发生在网站的许多页面上)时,我现在在一个静态CachedData类中有如下内容: public static List<Kategorie> GetKategorieTitelListe(Cache ap

我通过System.Web.caching.Cache-Class在ASP.NET网站中缓存数据,因为检索数据的成本非常高,而且当我们的内容人员在后端更改数据时,数据只会偶尔更改一次

所以我在Application_Start中创建数据并将其存储在缓存中,过期时间为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;
}
public静态列表GetKategorieTitelListe(Cache-appCache)
{
//从缓存中获取数据
List katList=appCache[CachedData.NaviDataKey]作为列表;
//缓存已过期,请重新检索和存储
if(katList==null)
{
katList=DataTools.buildkategorientitellite();
Insert(CachedData.NaviDataKey,katList,null,DateTime.Now.AddDays(1d),Cache.NoSlidingExpiration);
}
返回列表;
}
我看到这段代码的问题是它不是线程安全的。 如果两个用户同时打开其中两个页面,而缓存刚刚用完,则存在数据被多次检索的风险

但是如果我锁定方法体,我将遇到性能问题,因为一次只有一个用户可以获取数据列表


有没有一个简单的方法来防止这种情况?对于这样的情况,什么是最佳实践?

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

// 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;
    }
//这必须是类级变量!!!
私有静态只读对象锁定器=新对象();
公共静态列表GetKategorieTitelListe(缓存appCache)
{
//从缓存中获取数据
List katList=appCache[CachedData.NaviDataKey]作为列表;
//缓存已过期,请重新检索和存储
if(katList==null)
{
锁(储物柜)
{
katList=appCache[CachedData.NaviDataKey]作为列表;
if(katlist==null)//确保等待的线程没有第二次执行
{
katList=DataTools.buildkategorientitellite();
Insert(CachedData.NaviDataKey,katList,null,DateTime.Now.AddDays(1d),Cache.NoSlidingExpiration);
}
}
}
返回列表;
}
MSDN 声明ASP.NET缓存类是线程安全的——这意味着它们的内容可以由AppDomain中的任何线程自由访问(例如,读/写将是原子的)

请记住,随着缓存大小的增加,同步的成本也会增加。你可能想看看这篇文章

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

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;
    }
}
private static readonly myLockObject=new object();
公共静态列表GetKategorieTitelListe(缓存appCache)
{
//从缓存中获取数据
List katList=appCache[CachedData.NaviDataKey]作为列表;
锁定(myLockObject)
{
//缓存已过期,请重新检索和存储
if(katList==null)
{
katList=DataTools.buildkategorientitellite();
Insert(CachedData.NaviDataKey,katList,null,DateTime.Now.AddDays(1d),Cache.NoSlidingExpiration);
}
返回列表;
}
}

除了锁定,我看不到其他解决方案

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;
}
私有静态只读对象_locker=new object();
公共静态列表GetKategorieTitelListe(缓存appCache)
{
名单;
锁(储物柜)
{
//从缓存中获取数据
katList=appCache[CachedData.NaviDataKey]作为列表;
//缓存已过期,请重新检索和存储
if(katList==null)
{
katList=DataTools.buildkategorientitellite();
Insert(CachedData.NaviDataKey,katList,null,DateTime.Now.AddDays(1d),Cache.NoSlidingExpiration);
}
}
返回列表;
}
一旦数据在缓存中,并发线程将只等待取出数据的时间,即这行代码:

katList = appCache[CachedData.NaviDataKey] as List<Kategorie>;
katList=appCache[CachedData.NaviDataKey]作为列表;

因此,性能成本不会太大。

如果两个线程在数据为null时同时进入该方法,则第一个线程将进入锁。第二个将等待锁“解锁”,然后进入
katlist
在他的情况下仍然是空的,因此将调用两次
DataTools.BuildKeteGorientiteList
。所以基本上,我认为
lock
语句必须包含方法的所有内容,除了
return
。实际上不是。在您的场景中,当两个线程进入锁时,只有一个线程将进入锁,第二个线程将等待。因此,当第一个线程加载缓存并退出(解锁锁)时,第二个线程将进入,这就是为什么您有第二个if(katlist==null),所以由于第一个线程更新了它,它将不会为null,第二个线程将不会重新加载缓存。这是进行同步的最安全的方法。这称为双重检查锁定。在您的维基百科示例中了解更多信息,双重检查是在静态bool上完成的。在您提供的代码中,
katList
只是一个局部变量。如果在进入锁之前它是空的,那么在进入锁时它仍然是空的。好的一点,我没有意识到这是局部变量,而不是类或全局变量+祝你好运。我将更改我的代码。这是否存在性能问题,或者在第一个用户获得缓存后,以下所有用户都将被释放到缓存中。使用此设置使站点崩溃是否存在任何问题。