如何在MVC应用程序中缓存数据

260

我阅读了很多有关MVC应用程序中页面缓存和部分页面缓存的信息。然而,我想知道你如何缓存数据。

在我的情况下,我将使用LINQ to Entities(实体框架)。在第一次调用GetNames(或者其他方法)时,我想从数据库中获取数据。我希望将结果保存在缓存中,并在第二次调用时如果存在缓存版本则使用它。

是否可以给出一个示例说明如何实现,应该在哪里实现(模型?),以及是否有效。

我看到过这种做法在传统的ASP.NET应用程序中完成,通常用于非常静态的数据。


1
在审查以下答案时,请确保考虑您是否希望控制器具有数据访问和缓存问题的知识和责任。通常,您希望将此分开。请参阅存储库模式以了解一个很好的方法:http://deviq.com/repository-pattern/ - ssmith
14个回答

411

这是一个我使用的简单而实用的缓存帮助类/服务:

using System.Runtime.Caching;  

public class InMemoryCache: ICacheService
{
    public T GetOrSet<T>(string cacheKey, Func<T> getItemCallback) where T : class
    {
        T item = MemoryCache.Default.Get(cacheKey) as T;
        if (item == null)
        {
            item = getItemCallback();
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(10));
        }
        return item;
    }
}

interface ICacheService
{
    T GetOrSet<T>(string cacheKey, Func<T> getItemCallback) where T : class;
}

使用方法:

cacheProvider.GetOrSet("cache key", (delegate method if cache is empty));

缓存提供者会检查缓存中是否存在名为“cache id”的内容,如果不存在,则会调用委托方法来获取数据并将其存储在缓存中。

示例:

var products=cacheService.GetOrSet("catalog.products", ()=>productRepository.GetAll())

3
我已经进行了适应性修改,以便通过使用HttpContext.Current.Session使缓存机制在每个用户会话中使用。我还在我的BaseController类上放置了一个Cache属性,以便于访问,并更新了构造函数以允许进行单元测试的DI。希望这有所帮助。 - WestDiscGolf
5
这个类不应该依赖于HttpContext,我在这里只是举例简化了它。缓存对象必须通过构造函数插入 - 然后可以用其他缓存机制来替换它。这一切都可以通过IoC / DI实现,同时使用静态(单例)生命周期。 - Hrvoje Hudo
3
更糟的是,它在缓存键中使用了魔术字符串,而不是从方法名和参数中推断出来。 - ssmith
1
问题:这个程序为什么使用 HttpRuntime.Cache 查询一个项目,而使用 HttpContext.Current.Cache 设置它呢? - pettys
5
这是一个非常棒的低级解决方案。正如其他人所指出的,你会想要将其包装在类型安全、领域特定的类中。如果直接在控制器中访问它,由于魔术字符串,这将成为维护噩梦。 - Josh Noe
显示剩余25条评论

79

在您的模型中引用 System.Web dll,并使用 System.Web.Caching.Cache

    public string[] GetNames()
    {
      string[] names = Cache["names"] as string[];
      if(names == null) //not in cache
      {
        names = DB.GetNames();
        Cache["names"] = names;
      }
      return names;
    }

有点简化,但我认为会有效。这不是MVC特定的,我一直使用这种方法缓存数据。


91
我不推荐使用这个解决方案:在返回时,你可能会再次得到一个空对象,因为它是从缓存中重新读取的,而它可能已经从缓存中删除了。 我更愿意使用以下代码: public string[] GetNames() { string[] names = Cache["names"]; if(names == null) { names = DB.GetNames(); Cache["names"] = names; } return (names); } 该代码将先检查缓存是否存有数据,如无则调用DB.GetNames()方法并将结果存入缓存,最后返回结果。 - Oli
1
这是否适用于延迟查询的 DB.GetNames().AsQueryable 方法? - Chase Florell
14
如果您不设置过期时间,缓存会在默认情况下何时过期? - Chaka
你能不能像这样做if ((names = Cache["names"]) == null) { . . . - BenR
它可以像这样吗?. . . Cache["names"] = names = DB.GetNames(); . . . - stenlytw
显示剩余3条评论

43

我参考了TT的帖子并建议采用以下方法:

在您的模型中引用System.Web DLL并使用System.Web.Caching.Cache

public string[] GetNames()
{ 
    var noms = Cache["names"];
    if(noms == null) 
    {    
        noms = DB.GetNames();
        Cache["names"] = noms; 
    }

    return ((string[])noms);
}

不应该返回从缓存中重新读取的值,因为您永远不会知道在那个特定时刻它是否仍然在缓存中。即使您在之前的语句中插入了它,它可能已经丢失或从未添加到缓存中,您只是不知道。

因此,您应该添加从数据库中读取的数据并直接返回它,而不是从缓存中重新读取。


但是 Cache["names"] = noms; 这行代码不是将数据放入缓存中吗? - Omar
2
@Baddie是的。但是此示例与Oli所提到的第一个示例不同,因为他不再访问缓存-问题在于仅执行以下操作:return(string[])Cache["names"]; 可能会导致返回null值,因为它可能已过期。这不太可能,但确实可能发生。此示例更好,因为我们将从数据库返回的实际值存储在内存中,缓存该值,然后返回该值,而不是从缓存重新读取的值。 - jamiebarrow
或者...如果缓存中仍存在该值(!= null),则从缓存重新读取。这就是缓存的全部意义。这只是为了确保对空值进行双重检查,并在必要时读取数据库。非常聪明,谢谢Oli! - Sean Kendle

42

对于.NET 4.5+框架

添加引用:System.Runtime.Caching

添加using语句:using System.Runtime.Caching;

public string[] GetNames()
{ 
    var noms = System.Runtime.Caching.MemoryCache.Default["names"];
    if(noms == null) 
    {    
        noms = DB.GetNames();
        System.Runtime.Caching.MemoryCache.Default["names"] = noms; 
    }

    return ((string[])noms);
}

在.NET Framework 3.5及之前的版本中,ASP.NET在System.Web.Caching命名空间中提供了一个内存缓存实现。在以前的.NET Framework版本中,缓存仅在System.Web命名空间中可用,因此需要依赖于ASP.NET类。在.NET Framework 4中,System.Runtime.Caching命名空间包含为Web和非Web应用程序设计的API。
更多信息:
- https://msdn.microsoft.com/en-us/library/dd997357(v=vs.110).aspx - https://learn.microsoft.com/en-us/dotnet/framework/performance/caching-in-net-framework-applications

Db.GetNames() 是从哪里来的? - Junior
DB.GetNames只是DAL中的一个方法,用于从数据库中获取一些名称。这就是您通常要检索的内容。 - juFo
这应该放在顶部,因为它具有当前相关的解决方案。 - BYISHIMO Audace
2
谢谢,还需要添加System.Runtime.Caching nuget包(v4.5)。 - Steve Greene

26

Steve Smith撰写了两篇博客文章,演示如何在ASP.NET MVC中使用他的CachedRepository模式。这个模式有效地使用了存储库模式,允许您获得缓存而不必更改现有的代码。

http://ardalis.com/Introducing-the-CachedRepository-Pattern

http://ardalis.com/building-a-cachedrepository-via-strategy-pattern

在这两篇文章中,他向您展示如何设置这种模式,并解释了它的有用性。使用这个模式,您可以获得缓存,但您的现有代码不会看到任何缓存逻辑。基本上,您可以像使用任何其他存储库一样使用缓存存储库。


链接已于2013年08月31日失效。 - CBono
3
内容已经移动。http://ardalis.com/Introducing-the-CachedRepository-Pattern 和 http://ardalis.com/building-a-cachedrepository-via-strategy-pattern - Uchitha

8

我已经以这种方式使用它并且对我起作用。 https://msdn.microsoft.com/zh-cn/library/system.web.caching.cache.add(v=vs.110).aspx 系统.web.caching.cache.add的参数信息。

public string GetInfo()
{
     string name = string.Empty;
     if(System.Web.HttpContext.Current.Cache["KeyName"] == null)
     {
         name = GetNameMethod();
         System.Web.HttpContext.Current.Cache.Add("KeyName", name, null, DateTime.Noew.AddMinutes(5), Cache.NoSlidingExpiration, CacheitemPriority.AboveNormal, null);
     }
     else
     {
         name = System.Web.HttpContext.Current.Cache["KeyName"] as string;
     }

      return name;

}

只翻译文本内容:额外的赞成票将会给予完全限定名称的资格! - Ninjanoel
1
DateTime.Now在答案中。请小心。 - L0uis

4

AppFabric Caching 是一种分布式的、基于内存的缓存技术,它使用多台服务器上的物理内存存储数据的键值对。AppFabric 为 .NET Framework 应用程序提供了性能和可伸缩性的提升。 概念和架构


这是针对Azure的特定内容,不是针对ASP.NET MVC一般性的。 - Henry C

3

延伸 @Hrvoje Hudo 的答案...

代码:

using System;
using System.Runtime.Caching;

public class InMemoryCache : ICacheService
{
    public TValue Get<TValue>(string cacheKey, int durationInMinutes, Func<TValue> getItemCallback) where TValue : class
    {
        TValue item = MemoryCache.Default.Get(cacheKey) as TValue;
        if (item == null)
        {
            item = getItemCallback();
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(durationInMinutes));
        }
        return item;
    }

    public TValue Get<TValue, TId>(string cacheKeyFormat, TId id, int durationInMinutes, Func<TId, TValue> getItemCallback) where TValue : class
    {
        string cacheKey = string.Format(cacheKeyFormat, id);
        TValue item = MemoryCache.Default.Get(cacheKey) as TValue;
        if (item == null)
        {
            item = getItemCallback(id);
            MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(durationInMinutes));
        }
        return item;
    }
}

interface ICacheService
{
    TValue Get<TValue>(string cacheKey, Func<TValue> getItemCallback) where TValue : class;
    TValue Get<TValue, TId>(string cacheKeyFormat, TId id, Func<TId, TValue> getItemCallback) where TValue : class;
}

示例

单个项目缓存(当每个项目基于其ID进行缓存,因为对于该项目类型缓存整个目录将过于密集)。

Product product = cache.Get("product_{0}", productId, 10, productData.getProductById);

缓存所有内容
IEnumerable<Categories> categories = cache.Get("categories", 20, categoryData.getCategories);

TId的原因

第二个辅助器尤其好用,因为大多数数据键都不是复合键。如果您经常使用复合键,可以添加其他方法。这样可以避免做各种字符串连接或字符串格式化来获取要传递给缓存辅助程序的键。它还使得传递数据访问方法更加容易,因为您不需要将ID传递到包装器方法中...整个过程对于大多数用例来说非常简洁和一致。


1
你的接口定义缺少“durationInMinutes”参数。;-) - Tech0

3
public sealed class CacheManager
{
    private static volatile CacheManager instance;
    private static object syncRoot = new Object();
    private ObjectCache cache = null;
    private CacheItemPolicy defaultCacheItemPolicy = null;

    private CacheEntryRemovedCallback callback = null;
    private bool allowCache = true;

    private CacheManager()
    {
        cache = MemoryCache.Default;
        callback = new CacheEntryRemovedCallback(this.CachedItemRemovedCallback);

        defaultCacheItemPolicy = new CacheItemPolicy();
        defaultCacheItemPolicy.AbsoluteExpiration = DateTime.Now.AddHours(1.0);
        defaultCacheItemPolicy.RemovedCallback = callback;
        allowCache = StringUtils.Str2Bool(ConfigurationManager.AppSettings["AllowCache"]); ;
    }
    public static CacheManager Instance
    {
        get
        {
            if (instance == null)
            {
                lock (syncRoot)
                {
                    if (instance == null)
                    {
                        instance = new CacheManager();
                    }
                }
            }

            return instance;
        }
    }

    public IEnumerable GetCache(String Key)
    {
        if (Key == null || !allowCache)
        {
            return null;
        }

        try
        {
            String Key_ = Key;
            if (cache.Contains(Key_))
            {
                return (IEnumerable)cache.Get(Key_);
            }
            else
            {
                return null;
            }
        }
        catch (Exception)
        {
            return null;
        }
    }

    public void ClearCache(string key)
    {
        AddCache(key, null);
    }

    public bool AddCache(String Key, IEnumerable data, CacheItemPolicy cacheItemPolicy = null)
    {
        if (!allowCache) return true;
        try
        {
            if (Key == null)
            {
                return false;
            }

            if (cacheItemPolicy == null)
            {
                cacheItemPolicy = defaultCacheItemPolicy;
            }

            String Key_ = Key;

            lock (Key_)
            {
                return cache.Add(Key_, data, cacheItemPolicy);
            }
        }
        catch (Exception)
        {
            return false;
        }
    }

    private void CachedItemRemovedCallback(CacheEntryRemovedArguments arguments)
    {
        String strLog = String.Concat("Reason: ", arguments.RemovedReason.ToString(), " | Key-Name: ", arguments.CacheItem.Key, " | Value-Object: ", arguments.CacheItem.Value.ToString());
        LogManager.Instance.Info(strLog);
    }
}

4
考虑添加一些说明。 - Mike Debela

3
这是对Hrvoje Hudo答案的改进。这个实现有几个重要的改进:
  • 缓存键自动根据更新数据的函数和指定依赖项的对象创建
  • 传递任何缓存持续时间的时间跨度
  • 使用锁来保证线程安全
请注意,这依赖于Newtonsoft.Json来序列化dependsOn对象,但可以轻松更换为任何其他序列化方法。
ICache.cs
public interface ICache
{
    T GetOrSet<T>(Func<T> getItemCallback, object dependsOn, TimeSpan duration) where T : class;
}

InMemoryCache.cs

using System;
using System.Reflection;
using System.Runtime.Caching;
using Newtonsoft.Json;

public class InMemoryCache : ICache
{
    private static readonly object CacheLockObject = new object();

    public T GetOrSet<T>(Func<T> getItemCallback, object dependsOn, TimeSpan duration) where T : class
    {
        string cacheKey = GetCacheKey(getItemCallback, dependsOn);
        T item = MemoryCache.Default.Get(cacheKey) as T;
        if (item == null)
        {
            lock (CacheLockObject)
            {
                item = getItemCallback();
                MemoryCache.Default.Add(cacheKey, item, DateTime.Now.Add(duration));
            }
        }
        return item;
    }

    private string GetCacheKey<T>(Func<T> itemCallback, object dependsOn) where T: class
    {
        var serializedDependants = JsonConvert.SerializeObject(dependsOn);
        var methodType = itemCallback.GetType();
        return methodType.FullName + serializedDependants;
    }
}

使用方法:

var order = _cache.GetOrSet(
    () => _session.Set<Order>().SingleOrDefault(o => o.Id == orderId)
    , new { id = orderId }
    , new TimeSpan(0, 10, 0)
);

2
if (item == null)应该在锁内。现在,当此if在锁之前时,可能会发生竞争条件。或者更好的做法是,在锁之前保留if,但是在锁内的第一行重新检查缓存是否仍为空。因为如果两个线程同时到达,它们都会更新缓存。您当前的锁没有帮助。 - Al Kepp

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