Рекомендации по кэшированию - один объект или несколько записей? - PullRequest
9 голосов
/ 29 июня 2009

Кто-нибудь советует, какой метод лучше использовать при кэшировании данных в приложении C # ASP.net?

В настоящее время я использую комбинацию двух подходов, при этом некоторые данные (список, словари, обычная информация, относящаяся к домену) помещаются непосредственно в кэш и, при необходимости, помещаются в коробки, а некоторые данные хранятся в классе globaldata, и извлекается через этот класс (т. е. класс GlobalData кэшируется, а его свойства являются фактическими данными).

Любой подход предпочтительнее?

У меня такое ощущение, что кэширование каждого элемента в отдельности было бы более разумным с точки зрения параллелизма, однако это создает гораздо больше работы в долгосрочной перспективе с большим количеством функций, которые имеют дело исключительно с получением данных из местоположения кэша в Сервисный класс.

Предложения будут оценены.

Ответы [ 5 ]

8 голосов
/ 20 июля 2009

Как правило, производительность кеша намного выше, чем базовый источник (например, БД), поэтому производительность кеша не является проблемой.Основная цель состоит в том, чтобы получить как можно более высокий коэффициент попадания в кэш (если вы не разрабатываете в действительно больших масштабах, потому что тогда это также окупается для оптимизации кеша).

Чтобы достичь этого, я обычно стараюсь сделать так, чтобы разработчик как можно проще использовал кеш (чтобы мы не упустили никаких шансов на попадание в кеш только потому, что разработчик слишком ленив, чтобы использоватькэш).В некоторых проектах мы использовали модифицированную версию CacheHandler, доступную в корпоративной библиотеке Microsoft.

С 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, сериализованные в разные значения).

3 голосов
/ 29 июня 2009

При каких условиях вам нужно сделать ваш кеш недействительным? Объекты должны храниться таким образом, чтобы при их аннулировании повторное заполнение кэша требовало только повторного кэширования тех элементов, которые были признаны недействительными.

Например, если вы кэшировали, скажем, объект Customer, который содержит детали доставки для заказа вместе с корзиной покупок. Признание недействительными корзины покупок, поскольку они добавили или удалили товар, также потребовало бы повторного заполнения реквизитов доставки.

(ПРИМЕЧАНИЕ. Это тупой пример, и я не защищаю его, просто пытаюсь продемонстрировать принцип, и мое воображение сегодня немного не в порядке).

2 голосов
/ 21 июля 2009

Эд, я предполагаю, что эти списки и словари содержат почти статические данные с малыми шансами истечения. Кроме того, есть данные, которые часто посещаются, но также изменяются чаще, поэтому вы кешируете их, используя кеш HttpRuntime.

Теперь вы должны подумать обо всех этих данных и всех зависимостях между различными типами. Если вы логически обнаружите, что кэшированные данные HttpRuntime каким-то образом зависят от ваших элементов GlobalData, вы должны переместить их в кеш и установить там соответствующие зависимости, чтобы вы могли воспользоваться «каскадным истечением».

Даже если вы используете свой собственный механизм кэширования, вам все равно придется обеспечивать всю синхронизацию, поэтому вы не сэкономите на этом, избегая другого.

Если вам нужны (предварительно упорядоченные) списки элементов с очень низким изменением частоты, вы все равно можете сделать это, используя кеш HttpRuntime. Таким образом, вы можете просто кэшировать словарь и использовать его для составления списка ваших предметов или индексирования и доступа по вашему пользовательскому ключу.

2 голосов
/ 29 июня 2009

Как насчет лучшего (худшего?) Обоих миров?

Класс globaldata должен управлять всем доступом к кешу изнутри. Остальная часть вашего кода может затем просто использовать globaldata, что означает, что он вообще не нуждается в кэшировании.

Вы можете изменить реализацию кэша как / когда захотите, просто обновив globaldata, и остальная часть вашего кода не будет знать или беспокоиться о том, что происходит внутри.

1 голос
/ 08 июля 2009

При разработке стратегии кэширования нужно учитывать гораздо больше. Думайте о вашем хранилище кеш-памяти, как о базе данных в памяти. Поэтому тщательно обрабатывайте зависимости и политику истечения срока действия для каждого сохраненного там типа. На самом деле не имеет значения, что вы используете для кэширования (system.web, другое коммерческое решение, сворачивание собственного ...).

Я бы попытался централизовать его, а также использовать какую-то подключаемую архитектуру. Сделайте так, чтобы ваши потребители данных обращались к нему через общий API (абстрактный кеш, который его обнажает) и подключили ваш уровень кэширования во время выполнения (скажем, кеш asp.net).

При кэшировании данных следует действительно использовать нисходящий подход, чтобы избежать любых проблем с целостностью данных (правильные зависимости, как я уже говорил), а затем позаботиться о предоставлении синхронизации.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...