【问题标题】:Caching best practices - Single object or multiple entries?缓存最佳实践 - 单个对象还是多个条目?
【发布时间】:2009-07-21 06:37:46
【问题描述】:

在 C# ASP.net 应用程序中缓存数据时,是否有人对哪种方法更好有任何建议?

我目前正在使用两种方法的组合,一些数据(列表、字典、通常的特定于域的信息)直接放入缓存并在需要时装箱,一些数据保存在 globaldata 类中,并且通过该类检索(即 GlobalData 类被缓存,它的属性是实际数据)。

这两种方法都更可取吗?

我觉得从并发的角度来看,单独缓存每个项目会更明智,但是从长远来看,它会产生更多的工作,更多的功能纯粹是处理从缓存位置中获取数据的功能。实用程序类。

建议将不胜感激。

【问题讨论】:

  • 您可以在企业库的缓存应用程序块中看到 Microsoft 最佳实践。您可以阅读简介here

标签: c# asp.net caching


【解决方案1】:

通常缓存的性能比底层源(例如数据库)要好得多,因此缓存的性能不是问题。主要目标是获得尽可能高的缓存命中率(除非您正在大规模开发,因为这样优化缓存也有回报)。

为了实现这一点,我通常会尽量让开发人员使用缓存尽可能简单(这样我们就不会因为开发人员懒得使用缓存而错过任何缓存命中的机会)。在某些项目中,我们使用了 Microsoft 企业库中提供的 CacheHandler 的修改版本。

使用 CacheHandler(它使用 Policy Injection),您可以通过向其添加属性轻松地使方法“可缓存”。比如这个:

[CacheHandler(0, 30, 0)]
public Object GetData(Object input)
{
}

将使对该方法的所有调用缓存 30 分钟。所有调用都会根据输入数据和方法名称获得一个唯一的缓存键,因此如果您使用不同的输入调用该方法两次,则它不会被缓存,但如果您在超时间隔内使用相同的输入调用它 >1 次,则方法只执行一次。

我们修改后的版本如下所示:

using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.Remoting.Contexts;
using System.Text;
using System.Web;
using System.Web.Caching;
using System.Web.UI;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using Microsoft.Practices.Unity.InterceptionExtension;


namespace Middleware.Cache
{
    /// <summary>
    /// An <see cref="ICallHandler"/> that implements caching of the return values of
    /// methods. This handler stores the return value in the ASP.NET cache or the Items object of the current request.
    /// </summary>
    [ConfigurationElementType(typeof (CacheHandler)), Synchronization]
    public class CacheHandler : ICallHandler
    {
        /// <summary>
        /// The default expiration time for the cached entries: 5 minutes
        /// </summary>
        public static readonly TimeSpan DefaultExpirationTime = new TimeSpan(0, 5, 0);

        private readonly object cachedData;

        private readonly DefaultCacheKeyGenerator keyGenerator;
        private readonly bool storeOnlyForThisRequest = true;
        private TimeSpan expirationTime;
        private GetNextHandlerDelegate getNext;
        private IMethodInvocation input;


        public CacheHandler(TimeSpan expirationTime, bool storeOnlyForThisRequest)
        {
            keyGenerator = new DefaultCacheKeyGenerator();
            this.expirationTime = expirationTime;
            this.storeOnlyForThisRequest = storeOnlyForThisRequest;
        }

        /// <summary>
        /// This constructor is used when we wrap cached data in a CacheHandler so that 
        /// we can reload the object after it has been removed from the cache.
        /// </summary>
        /// <param name="expirationTime"></param>
        /// <param name="storeOnlyForThisRequest"></param>
        /// <param name="input"></param>
        /// <param name="getNext"></param>
        /// <param name="cachedData"></param>
        public CacheHandler(TimeSpan expirationTime, bool storeOnlyForThisRequest,
                            IMethodInvocation input, GetNextHandlerDelegate getNext,
                            object cachedData)
            : this(expirationTime, storeOnlyForThisRequest)
        {
            this.input = input;
            this.getNext = getNext;
            this.cachedData = cachedData;
        }


        /// <summary>
        /// Gets or sets the expiration time for cache data.
        /// </summary>
        /// <value>The expiration time.</value>
        public TimeSpan ExpirationTime
        {
            get { return expirationTime; }
            set { expirationTime = value; }
        }

        #region ICallHandler Members

        /// <summary>
        /// Implements the caching behavior of this handler.
        /// </summary>
        /// <param name="input"><see cref="IMethodInvocation"/> object describing the current call.</param>
        /// <param name="getNext">delegate used to get the next handler in the current pipeline.</param>
        /// <returns>Return value from target method, or cached result if previous inputs have been seen.</returns>
        public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
        {
            lock (input.MethodBase)
            {
                this.input = input;
                this.getNext = getNext;

                return loadUsingCache();
            }
        }

        public int Order
        {
            get { return 0; }
            set { }
        }

        #endregion

        private IMethodReturn loadUsingCache()
        {
            //We need to synchronize calls to the CacheHandler on method level
            //to prevent duplicate calls to methods that could be cached.
            lock (input.MethodBase)
            {
                if (TargetMethodReturnsVoid(input) || HttpContext.Current == null)
                {
                    return getNext()(input, getNext);
                }

                var inputs = new object[input.Inputs.Count];
                for (int i = 0; i < inputs.Length; ++i)
                {
                    inputs[i] = input.Inputs[i];
                }

                string cacheKey = keyGenerator.CreateCacheKey(input.MethodBase, inputs);
                object cachedResult = getCachedResult(cacheKey);

                if (cachedResult == null)
                {
                    var stopWatch = Stopwatch.StartNew();
                    var realReturn = getNext()(input, getNext);
                    stopWatch.Stop();
                    if (realReturn.Exception == null && realReturn.ReturnValue != null)
                    {
                        AddToCache(cacheKey, realReturn.ReturnValue);
                    }
                    return realReturn;
                }

                var cachedReturn = input.CreateMethodReturn(cachedResult, input.Arguments);

                return cachedReturn;
            }
        }

        private object getCachedResult(string cacheKey)
        {
            //When the method uses input that is not serializable 
            //we cannot create a cache key and can therefore not 
            //cache the data.
            if (cacheKey == null)
            {
                return null;
            }

            object cachedValue = !storeOnlyForThisRequest ? HttpRuntime.Cache.Get(cacheKey) : HttpContext.Current.Items[cacheKey];
            var cachedValueCast = cachedValue as CacheHandler;
            if (cachedValueCast != null)
            {
                //This is an object that is reloaded when it is being removed.
                //It is therefore wrapped in a CacheHandler-object and we must
                //unwrap it before returning it.
                return cachedValueCast.cachedData;
            }
            return cachedValue;
        }

        private static bool TargetMethodReturnsVoid(IMethodInvocation input)
        {
            var targetMethod = input.MethodBase as MethodInfo;
            return targetMethod != null && targetMethod.ReturnType == typeof (void);
        }

        private void AddToCache(string key, object valueToCache)
        {
            if (key == null)
            {
                //When the method uses input that is not serializable 
                //we cannot create a cache key and can therefore not 
                //cache the data.
                return;
            }

            if (!storeOnlyForThisRequest)
            {
                HttpRuntime.Cache.Insert(
                    key,
                    valueToCache,
                    null,
                    System.Web.Caching.Cache.NoAbsoluteExpiration,
                    expirationTime,
                    CacheItemPriority.Normal, null);
            }
            else
            {
                HttpContext.Current.Items[key] = valueToCache;
            }
        }
    }

    /// <summary>
    /// This interface describes classes that can be used to generate cache key strings
    /// for the <see cref="CacheHandler"/>.
    /// </summary>
    public interface ICacheKeyGenerator
    {
        /// <summary>
        /// Creates a cache key for the given method and set of input arguments.
        /// </summary>
        /// <param name="method">Method being called.</param>
        /// <param name="inputs">Input arguments.</param>
        /// <returns>A (hopefully) unique string to be used as a cache key.</returns>
        string CreateCacheKey(MethodBase method, object[] inputs);
    }

    /// <summary>
    /// The default <see cref="ICacheKeyGenerator"/> used by the <see cref="CacheHandler"/>.
    /// </summary>
    public class DefaultCacheKeyGenerator : ICacheKeyGenerator
    {
        private readonly LosFormatter serializer = new LosFormatter(false, "");

        #region ICacheKeyGenerator Members

        /// <summary>
        /// Create a cache key for the given method and set of input arguments.
        /// </summary>
        /// <param name="method">Method being called.</param>
        /// <param name="inputs">Input arguments.</param>
        /// <returns>A (hopefully) unique string to be used as a cache key.</returns>
        public string CreateCacheKey(MethodBase method, params object[] inputs)
        {
            try
            {
                var sb = new StringBuilder();

                if (method.DeclaringType != null)
                {
                    sb.Append(method.DeclaringType.FullName);
                }
                sb.Append(':');
                sb.Append(method.Name);

                TextWriter writer = new StringWriter(sb);

                if (inputs != null)
                {
                    foreach (var input in inputs)
                    {
                        sb.Append(':');
                        if (input != null)
                        {
                            //Diffrerent instances of DateTime which represents the same value
                            //sometimes serialize differently due to some internal variables which are different.
                            //We therefore serialize it using Ticks instead. instead.
                            var inputDateTime = input as DateTime?;
                            if (inputDateTime.HasValue)
                            {
                                sb.Append(inputDateTime.Value.Ticks);
                            }
                            else
                            {
                                //Serialize the input and write it to the key StringBuilder.
                                serializer.Serialize(writer, input);
                            }
                        }
                    }
                }

                return sb.ToString();
            }
            catch
            {
                //Something went wrong when generating the key (probably an input-value was not serializble.
                //Return a null key.
                return null;
            }
        }

        #endregion
    }
}

Microsoft 最值得称赞的是这段代码。我们只添加了诸如在请求级别而不是跨请求缓存之类的东西(比您想象的更有用)并修复了一些错误(例如,相等的 DateTime 对象序列化为不同的值)。

【讨论】:

    【解决方案2】:

    在什么情况下需要使缓存失效?应该存储对象,以便在它们失效时重新填充缓存只需要重新缓存失效的项目。

    例如,如果您缓存了一个 Customer 对象,该对象包含订单的交货详细信息以及购物篮。由于添加或删除了商品而使购物篮无效还需要重新填充不必要的交货详细信息。

    (注意:这是一个老生常谈的例子,我并不是为了证明原理而提倡这样做,而且我今天的想象力有点偏离)。

    【讨论】:

      【解决方案3】:

      Ed,我假设这些列表和字典包含几乎静态的数据,过期的可能性很小。然后是经常被点击但也更频繁地更改的数据,因此您使用 HttpRuntime 缓存对其进行缓存。

      现在,您应该考虑所有这些数据以及不同类型之间的所有依赖关系。如果您在逻辑上发现 HttpRuntime 缓存的数据以某种方式依赖于您的 GlobalData 项,则应将其移入缓存并在其中设置适当的依赖项,以便您从“级联过期”中受益。

      即使您确实使用了自定义缓存机制,您仍然必须提供所有同步,因此您不会通过避免另一个来节省这一点。

      如果您需要(预定)具有非常低频率更改的项目列表,您仍然可以通过使用 HttpRuntime 缓存来实现。因此,您可以只缓存一个字典,然后使用它来列出您的项目或通过您的自定义键来索引和访问。

      【讨论】:

      • 您好!这是一个非常晚的问题,但是这里的“级联过期”是什么意思?我从来没有听说过这个术语。谢谢!
      【解决方案4】:

      两全其美的(最差的?)怎么样?

      globaldata 类在内部管理所有缓存访问。然后,您的其余代码可以只使用globaldata,这意味着它根本不需要缓存感知。

      您可以根据需要更改缓存实现,只需更新globaldata,其余代码将不知道也不关心内部发生了什么。

      【讨论】:

      • 是的,我有一个实用程序类来处理所有的缓存,所以一段普通的代码只需要调用 GetCachedWhatever() 来获取任何东西,只是一些东西还包含列表和字典,并且我想知道对此的普遍共识是什么
      • @Ed:为什么不只拥有一个 GetWhatever() 方法,而让它担心缓存 ifs 和 whens 以及 hows 呢?你的普通代码只需要给出一个“随便”,并不关心它是否来自缓存、数据库、文件系统、网络服务等。
      • 重要的是有一个区别,例如,根据情况,有些东西需要从缓存中访问以提高速度,而从数据库中访问以提高可靠性。
      【解决方案5】:

      在构建缓存策略时,要考虑的远不止这些。将您的缓存存储视为您的内存数据库。因此,请仔细处理存储在其中的每种类型的依赖关系和过期策略。使用什么缓存并不重要(system.web、其他商业解决方案、滚动您自己的...)。

      不过,我会尝试将其集中起来,并使用某种可插入的架构。让您的数据消费者通过通用 API(公开它的抽象缓存)访问它,并在运行时插入您的缓存层(比如说 asp.net 缓存)。

      在缓存数据时,您应该真正采用自上而下的方法以避免任何类型的数据完整性问题(如我所说的正确依赖关系),然后注意提供同步。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-06-10
        • 1970-01-01
        • 1970-01-01
        • 2015-12-02
        • 1970-01-01
        • 2015-05-31
        • 1970-01-01
        • 2019-11-23
        相关资源
        最近更新 更多