我该如何在ASP.NET MVC中缓存对象?

53
我想在ASP.NET MVC中缓存对象。我有一个名为BaseController的控制器,我希望所有控制器都从它继承。在BaseController中有一个User属性,它可以简单地从数据库中获取用户数据,以便我可以在控制器内使用它或将其传递给视图。
我想要缓存这些信息。由于每个页面都会用到这些信息,所以没有必要在每次页面请求时都去访问数据库。
我想要实现类似以下的功能:
if(_user is null)
  GrabFromDatabase
  StuffIntoCache
return CachedObject as User

我该如何在ASP.NET MVC中实现简单的缓存?

8个回答

76

您仍然可以使用缓存(在所有响应之间共享)和会话(每个用户唯一)进行存储。

我喜欢以下“尝试从缓存中获取/创建并存储”模式(类似于C#的伪代码):

public static class CacheExtensions
{
  public static T GetOrStore<T>(this Cache cache, string key, Func<T> generator)
  {
    var result = cache[key];
    if(result == null)
    {
      result = generator();
      cache[key] = result;
    }
    return (T)result;
  }
}

您可以这样使用:

var user = HttpRuntime
              .Cache
              .GetOrStore<User>(
                 $"User{_userId}", 
                 () => Repository.GetUser(_userId));

你可以将此模式适应于Session、ViewState(呃)或任何其他缓存机制。你还可以扩展ControllerContext.HttpContext(我认为这是System.Web.Extensions中的包装器之一),或创建一个新类来实现它,有些空间用于模拟缓存。

14
不要使用 cache[key]=result,而是使用 Cache.Insert(...),因为在 Insert 中可以设置依赖项、过期策略等内容。 - Andrei Rînea
为什么我没有想到使用Session,这超出了我的理解。我认为那会很好地解决这个问题。 - rball
+1 给安德烈。那是“类似C#的伪代码”,我的意思是我是凭记忆编写的,不一定是最好的代码或没有错误。 - user1228
我也使用了这种方法,但我发现每次请求时都会调用 Repository.GetUser(_userId)(即使数据已经在缓存中存在)。这是正常的吗? - Chase Florell
@rock 你需要调试并逐步执行代码,找出为什么生成器方法被调用。我建议在var result = cache[key];这一行上设置断点,并检查缓存中的值。如果你在那里找不到它,就逐步执行并观察缓存中的值被设置。然后再次检查缓存是否存在该值。如果没有,则说明你有一个严重的问题。如果有,可能是由于系统资源不足而导致缓存被清空。请参考Andrei的评论以了解如何更改缓存过期值。 - user1228
@rock,如果你使用我下面的实现,你仍然会在每个请求中看到对Repository.GetUser方法的调用吗? - njappboy

59

我采用了Will的答案并进行了修改,使CacheExtensions类变为静态,并建议略微修改以处理可能出现的Func<T>null的情况:

public static class CacheExtensions
{

    private static object sync = new object();
    public const int DefaultCacheExpiration = 20;

    /// <summary>
    /// Allows Caching of typed data
    /// </summary>
    /// <example><![CDATA[
    /// var user = HttpRuntime
    ///   .Cache
    ///   .GetOrStore<User>(
    ///      string.Format("User{0}", _userId), 
    ///      () => Repository.GetUser(_userId));
    ///
    /// ]]></example>
    /// <typeparam name="T"></typeparam>
    /// <param name="cache">calling object</param>
    /// <param name="key">Cache key</param>
    /// <param name="generator">Func that returns the object to store in cache</param>
    /// <returns></returns>
    /// <remarks>Uses a default cache expiration period as defined in <see cref="CacheExtensions.DefaultCacheExpiration"/></remarks>
    public static T GetOrStore<T>( this Cache cache, string key, Func<T> generator ) {
        return cache.GetOrStore( key, (cache[key] == null && generator != null) ? generator() : default( T ), DefaultCacheExpiration );
    }


    /// <summary>
    /// Allows Caching of typed data
    /// </summary>
    /// <example><![CDATA[
    /// var user = HttpRuntime
    ///   .Cache
    ///   .GetOrStore<User>(
    ///      string.Format("User{0}", _userId), 
    ///      () => Repository.GetUser(_userId));
    ///
    /// ]]></example>
    /// <typeparam name="T"></typeparam>
    /// <param name="cache">calling object</param>
    /// <param name="key">Cache key</param>
    /// <param name="generator">Func that returns the object to store in cache</param>
    /// <param name="expireInMinutes">Time to expire cache in minutes</param>
    /// <returns></returns>
    public static T GetOrStore<T>( this Cache cache, string key, Func<T> generator, double expireInMinutes ) {
        return cache.GetOrStore( key,  (cache[key] == null && generator != null) ? generator() : default( T ), expireInMinutes );
    }


    /// <summary>
    /// Allows Caching of typed data
    /// </summary>
    /// <example><![CDATA[
    /// var user = HttpRuntime
    ///   .Cache
    ///   .GetOrStore<User>(
    ///      string.Format("User{0}", _userId),_userId));
    ///
    /// ]]></example>
    /// <typeparam name="T"></typeparam>
    /// <param name="cache">calling object</param>
    /// <param name="key">Cache key</param>
    /// <param name="obj">Object to store in cache</param>
    /// <returns></returns>
    /// <remarks>Uses a default cache expiration period as defined in <see cref="CacheExtensions.DefaultCacheExpiration"/></remarks>
    public static T GetOrStore<T>( this Cache cache, string key, T obj ) {
        return cache.GetOrStore( key, obj, DefaultCacheExpiration );
    }

    /// <summary>
    /// Allows Caching of typed data
    /// </summary>
    /// <example><![CDATA[
    /// var user = HttpRuntime
    ///   .Cache
    ///   .GetOrStore<User>(
    ///      string.Format("User{0}", _userId), 
    ///      () => Repository.GetUser(_userId));
    ///
    /// ]]></example>
    /// <typeparam name="T"></typeparam>
    /// <param name="cache">calling object</param>
    /// <param name="key">Cache key</param>
    /// <param name="obj">Object to store in cache</param>
    /// <param name="expireInMinutes">Time to expire cache in minutes</param>
    /// <returns></returns>
    public static T GetOrStore<T>( this Cache cache, string key, T obj, double expireInMinutes ) {
        var result = cache[key];

        if ( result == null ) {

            lock ( sync ) {
                result = cache[key];
                if ( result == null ) {
                    result = obj != null ? obj : default( T );
                    cache.Insert( key, result, null, DateTime.Now.AddMinutes( expireInMinutes ), Cache.NoSlidingExpiration );
                }
            }
        }

        return (T)result;

    }

}

我也会考虑进一步实现一个可测试的会话解决方案,扩展System.Web.HttpSessionStateBase抽象类。
public static class SessionExtension
{
    /// <summary>
    /// 
    /// </summary>
    /// <example><![CDATA[
    /// var user = HttpContext
    ///   .Session
    ///   .GetOrStore<User>(
    ///      string.Format("User{0}", _userId), 
    ///      () => Repository.GetUser(_userId));
    ///
    /// ]]></example>
    /// <typeparam name="T"></typeparam>
    /// <param name="cache"></param>
    /// <param name="key"></param>
    /// <param name="generator"></param>
    /// <returns></returns>
    public static T GetOrStore<T>( this HttpSessionStateBase session, string name, Func<T> generator ) {

        var result = session[name];
        if ( result != null )
            return (T)result;

        result = generator != null ? generator() : default( T );
        session.Add( name, result );
        return (T)result;
    }

}

2
我认为你的代码有一个 bug。即使缓存存在,该函数仍然被调用。将 generator != null 更改为 (cache[key] == null && generator != null) 可以修复该 bug。 - TeYoU
1
你知道吗?只需编写 lock(typeof(CacheExtensions)),就可以创建全局锁。不需要 sync 对象。 - Mathias Lykkegaard Lorenzen
+1 Mathias,我之前不知道锁可以在类型级别上全局运行。 - njappboy
1
锁里面有个bug。你正在检查result == null,但是还没有将其设置为result = cache[key];!此外,锁定似乎毫无意义,因为generator()也将被多个线程调用,因为它们在锁之外。 - DigitalDan
如果“generator”可以合法地是“null”值,则在方法签名中将其设置为默认值。如果不应传递“null”,则抛出“ArgumentNullException”更好。 - Greg Burghardt

6
如果您希望在请求期间将其缓存,可以将以下内容放入您的控制器基类中:
public User User {
    get {
        User _user = ControllerContext.HttpContext.Items["user"] as User;

        if (_user == null) {
            _user = _repository.Get<User>(id);
            ControllerContext.HttpContext.Items["user"] = _user;
        }

        return _user;
    }
}

如果您希望缓存更长时间,可以将ControllerContext调用替换为Cache[]。如果您选择使用Cache对象进行更长时间的缓存,您需要使用唯一的缓存键,因为它将在请求/用户之间共享。

2
我回家后会试一下。谢谢! - rball
3
好久不见了,你已经回家了吗? :) - Piotr Kula

6

这里的其他答案没有涉及以下内容:

  • 缓存惊群(cache stampede)
  • 双重检查锁(double check lock)

这可能会导致生成器(可能需要很长时间)在不同的线程中运行多次。

这是我的版本,不应该遇到这个问题:

// using System;
// using System.Web.Caching;

// https://dev59.com/8nRB5IYBdhLWcg3w-8Ho#42443437
// Usage: HttpRuntime.Cache.GetOrStore("myKey", () => GetSomethingToCache());

public static class CacheExtensions
{
    private static readonly object sync = new object();
    private static TimeSpan defaultExpire = TimeSpan.FromMinutes(20);

    public static T GetOrStore<T>(this Cache cache, string key, Func<T> generator) =>
        cache.GetOrStore(key, generator, defaultExpire);

    public static T GetOrStore<T>(this Cache cache, string key, Func<T> generator, TimeSpan expire)
    {
        var result = cache[key];
        if (result == null)
        {
            lock (sync)
            {
                result = cache[key];
                if (result == null)
                {
                    result = generator();
                    cache.Insert(key, result, null, DateTime.UtcNow.AddMinutes(expire.TotalMinutes), Cache.NoSlidingExpiration);
                }
            }
        }
        return (T)result;
    }
}

3
我喜欢隐藏数据在仓库中的缓存事实。 您可以通过HttpContext.Current.Cache属性访问缓存,并使用“User”+ id.ToString()作为键来存储用户信息。
这意味着从仓库访问用户数据时,如果有可用的缓存数据,则使用缓存数据,而不需要对模型,控制器或视图进行任何代码更改。
我已经使用了这种方法来纠正一个系统的严重性能问题,该系统对每个用户属性查询数据库,并将页面加载时间从几分钟减少到几秒钟。

4
+1 或者更好地,使用一个装饰器仓库来缓存真正仓库的结果。 - Richard Szalay

3

@njappboy: 好的实现。我只会推迟Generator( )调用到最后一个负责的时刻,这样你也可以缓存方法调用。

/// <summary>
/// Allows Caching of typed data
/// </summary>
/// <example><![CDATA[
/// var user = HttpRuntime
///   .Cache
///   .GetOrStore<User>(
///      string.Format("User{0}", _userId), 
///      () => Repository.GetUser(_userId));
///
/// ]]></example>
/// <typeparam name="T"></typeparam>
/// <param name="Cache">calling object</param>
/// <param name="Key">Cache key</param>
/// <param name="Generator">Func that returns the object to store in cache</param>
/// <returns></returns>
/// <remarks>Uses a default cache expiration period as defined in <see cref="CacheExtensions.DefaultCacheExpiration"/></remarks>
public static T GetOrStore<T>( this Cache Cache, string Key, Func<T> Generator )
{
    return Cache.GetOrStore( Key, Generator, DefaultCacheExpiration );
}

/// <summary>
/// Allows Caching of typed data
/// </summary>
/// <example><![CDATA[
/// var user = HttpRuntime
///   .Cache
///   .GetOrStore<User>(
///      string.Format("User{0}", _userId), 
///      () => Repository.GetUser(_userId));
///
/// ]]></example>
/// <typeparam name="T"></typeparam>
/// <param name="Cache">calling object</param>
/// <param name="Key">Cache key</param>
/// <param name="Generator">Func that returns the object to store in cache</param>
/// <param name="ExpireInMinutes">Time to expire cache in minutes</param>
/// <returns></returns>
public static T GetOrStore<T>( this Cache Cache, string Key, Func<T> Generator, double ExpireInMinutes )
{
    var Result = Cache [ Key ];

    if( Result == null )
    {
        lock( Sync )
        {
            if( Result == null )
            {
                Result = Generator( );
                Cache.Insert( Key, Result, null, DateTime.Now.AddMinutes( ExpireInMinutes ), Cache.NoSlidingExpiration );
            }
        }
    }

    return ( T ) Result;
}

SDReyes,我喜欢你在这里所做的。我试图保持代码DRY,但这种重载是有意义的。我可以建议将缓存实例命名为除“Cache”之外的其他名称 :) - njappboy
请从此bcode中删除lock()调用。如果键已经存在,Cache.Insert()可以正常工作,无需锁定。 - Andrus
1
和njappboy的回答一样,这个实现也没有正确地实现双重检查锁定。你在锁内缺少了这行代码:Result = Cache [ Key ];。我完全同意你推迟Generator()调用的观点,但是如果其他线程正在等待锁,则当前将多次调用它。 - DigitalDan

2
如果您不需要ASP.NET缓存的特定失效功能,那么静态字段非常好,轻量且易于使用。但是,一旦您需要高级功能,您可以切换到ASP.NET的Cache对象进行存储。
我使用的方法是创建一个属性和一个private字段。如果该字段为null,则该属性将填充它并返回它。我还提供了一个InvalidateCache方法,手动将字段设置为null。这种方法的优点是缓存机制封装在属性中,如果需要,您可以切换到其他方法。

静态字段会在页面视图之间保留吗? - rball
是的,它将在整个AppDomain中持久存在(附注:或者线程,如果您指定了[ThreadStatic],但您不希望这样)。 - Mehrdad Afshari

1

使用最小缓存锁来实现。在缓存中存储的值被封装在容器中。如果值不在缓存中,则值容器被锁定。仅在创建容器期间才锁定缓存。

public static class CacheExtensions
{
    private static object sync = new object();

    private class Container<T>
    {
        public T Value;
    }

    public static TValue GetOrStore<TValue>(this Cache cache, string key, Func<TValue> create, TimeSpan slidingExpiration)
    {
        return cache.GetOrStore(key, create, Cache.NoAbsoluteExpiration, slidingExpiration);
    }

    public static TValue GetOrStore<TValue>(this Cache cache, string key, Func<TValue> create, DateTime absoluteExpiration)
    {
        return cache.GetOrStore(key, create, absoluteExpiration, Cache.NoSlidingExpiration);
    }

    public static TValue GetOrStore<TValue>(this Cache cache, string key, Func<TValue> create, DateTime absoluteExpiration, TimeSpan slidingExpiration)
    {
        return cache.GetOrCreate(key, x => create());
    }

    public static TValue GetOrStore<TValue>(this Cache cache, string key, Func<string, TValue> create, DateTime absoluteExpiration, TimeSpan slidingExpiration)
    {
        var instance = cache.GetOrStoreContainer<TValue>(key, absoluteExpiration, slidingExpiration);
        if (instance.Value == null)
            lock (instance)
                if (instance.Value == null)
                    instance.Value = create(key);

        return instance.Value;
    }

    private static Container<TValue> GetOrStoreContainer<TValue>(this Cache cache, string key, DateTime absoluteExpiration, TimeSpan slidingExpiration)
    {
        var instance = cache[key];
        if (instance == null)
            lock (cache)
            {
                instance = cache[key];
                if (instance == null)
                {
                    instance = new Container<TValue>();

                    cache.Add(key, instance, null, absoluteExpiration, slidingExpiration, CacheItemPriority.Default, null);
                }
            }

        return (Container<TValue>)instance;
    }
}

1
没有 GetOrCreate() 方法? - bbqchickenrobot

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