Реализация кэширования средствами Unity

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

Наступил момент, когда и я при разработке такого высоконагруженного приложения столкнулся с этой проблемой. И решил её достаточно просто и безболезненно с помощью Unity Application Block.

Изначально Unity в проекте использовался по своему прямому назначению т.е. для внедрения зависимостей. Но когда встала задача кэширования, я вспомнил, что Unity позволяет осуществлять перехват вызова методов, а значит есть возможность реализовать парадигму аспекто-ориентированного программирования, что для данной задачи подходит как нельзя лучше.

Создание атрибута

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

    /// 
    /// Атрибут для пометки кэшируемого метода
    /// 
    [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
    public class CachedAttribute : HandlerAttribute
    {
        /// 
        /// Максимальное время хранения результатов выполнения метода в кэше (сек.)
        /// 
        public int CacheTimeSeconds { get; set; }

        /// 
        /// Максимальное время хранения результатов выполнения метода в кэше (мин.)
        /// 
        public int CacheTimeMinutes
        {
            get { return CacheTimeSeconds / 60; }
            set { CacheTimeSeconds = value * 60; }
        }
        
        /// 
        /// Максимальное время хранения результатов выполнения метода в кэше (часов)
        /// 
        public int CacheTimeHours
        {
            get { return CacheTimeSeconds / 3600; }
            set { CacheTimeSeconds = value * 3600; }
        }

        /// 
        /// Конструктор по умолчанию, время хранения по умолчанию = 10 минут
        /// 
        public CachedAttribute()
        {
            CacheTimeMinutes = 10;
        }

        /// 
        /// Конструктор с указанием времени хранения результатов выполнения метода в кэше в секундах
        /// 
        /// 
        public CachedAttribute(int cacheTimeSeconds)
        {
            CacheTimeSeconds = cacheTimeSeconds;
        }

        public override ICallHandler CreateHandler(IUnityContainer container)
        {
            return new CacheAttributeHandler(CacheTimeSeconds);
        }
    }

Код атрибута в общем-то стандартный за исключением наследования специального класса HandlerAttribute.

Далее нам нужно создать собственно перехватчик события вызова метода. Вот его код.

private class CacheAttributeHandler : ICallHandler
    {
        private readonly int _maxCacheTime;

        /// 
        /// Конструктор
        /// 
        /// 
        public CacheAttributeHandler(int maxCacheTime)
        {
            _maxCacheTime = maxCacheTime;
        }

        #region ICallHandler Members

        public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
        {
            object cachedResult = CacheContainer.CheckCache(input);
            if (cachedResult != null)
                return input.CreateMethodReturn(cachedResult);

            IMethodReturn mr = getNext()(input, getNext);
            CacheContainer.AddToCache(mr, input, _maxCacheTime);
            return mr;
        }

        public int Order { get; set;}

        #endregion
    }

Как видим, этот атрибут наследует интерфейс ICallHandler и реализует метод Invoke этого интерфейса. Invoke вызывается непосредственно перед вызовом самого метода.

Разберём метод Invoke подробнее. Сначала идёт проверка кэша на наличие в нём результата выполнения метода с данным именем и данными аргументами и их значениями. Если значение найдено в кэше — оно возвращается из кэша. Иначе запускается выполнение метода и результат перед выходом из процедуры сохраняется в кэш, чтобы быть использованным при повторном вызове.

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

Собственно кэш

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

Сделал я это из соображений производительности, а точнее скорости поиска по контейнеру.

        static class CacheContainer
    {
        private static IDictionary _methodsCache;
        
        /// 
        /// Кэш для кэшей методов
        /// 
        internal static IDictionary MethodsCache
        {
            get { return _methodsCache ?? (_methodsCache = CreateCache()); }
        }

        static IDictionary CreateCache()
        {
            return Hashtable.Synchronized(new Hashtable());
        }

        static CacheContainer()
        {
            CacheCleanup.Init();
        }

        private static string GetCacheKey(IMethodInvocation methodInvocation)
        {
            string argumentString = "arguments#";
            for (int i = 0; i < methodInvocation.Arguments.Count; i++)
            {
                string argumentName = methodInvocation.Arguments.ParameterName(i);
                object value = methodInvocation.Arguments[i];
                string argumentValue = value == null ? string.Empty : value.ToString();
                argumentString += string.Format("{0}:{1};", argumentName, argumentValue);
            }
            return argumentString;
        }
        
        private static string GetMethodCacheKey(IMethodInvocation methodInvocation)
        {
            string key = methodInvocation.MethodBase.Name;
            return key;
        }

        /// 
        /// Проверить существует ли закэшированное значение. Если да - вернуть его
        /// 
        /// 
        /// 
        internal static object CheckCache(IMethodInvocation methodInvocation)
        {
            // Ключ - имя метода
            string methodKey = GetMethodCacheKey(methodInvocation);
            // Находим нужный кэш результатов по имени метода
            object methodsCacheItem = MethodsCache[methodKey];
            if (methodsCacheItem != null)
            {
                var cache = (IDictionary)methodsCacheItem;
                // ключ - имена аргументов и их значения
                string cacheKey = GetCacheKey(methodInvocation);
                // Находим кэшированный результат выполнения метода
                object cachedItem = cache[cacheKey];
                if (cachedItem != null)
                {
                    var item = cachedItem as CachedItem;
                    if (item != null && !item.IsExpired)
                        return item.MethodResult;
                }
            }
            return null;
        }

        /// 
        /// Добавить результат выполнения метода в кэш
        /// 
        /// 
        /// 
        /// 
        internal static void AddToCache(IMethodReturn mr, IMethodInvocation methodInvocation, int maxCacheTime)
        {
            // Если метод вернул исключение - в кэш ничего не помещаем
            if(mr.Exception != null)
                return;
            
            string methodKey = GetMethodCacheKey(methodInvocation);
            lock (MethodsCache.SyncRoot)
            {
                // если в кэше методов не было кэша для результатов конкретного метода - создаём
                object cache = MethodsCache[methodKey] ?? CreateCache();
                
                string cachekey = GetCacheKey(methodInvocation);
                
                lock (((IDictionary)cache).SyncRoot)
                {
                    if (((IDictionary)cache).Contains(cachekey) == false)
                    {
                        var cachedItem = new CachedItem
                                             {
                                                 MaxCacheTime = DateTime.Now.AddSeconds(maxCacheTime),
                                                 MethodResult = mr.ReturnValue
                                             };
                        ((IDictionary) cache).Add(cachekey, cachedItem);
                        MethodsCache[methodKey] = cache;
                    }
                }
            }
        }
    }

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

public static class CacheCleanup
    {

        /// 
        /// Время последней очистки кэша
        /// 
        public static DateTime LastCleanupTime { get; private set; }

        /// 
        /// Количество очисток кэша
        /// 
        public static int WorkTimes { get; private set; }

        /// 
        /// Время, затраченное на последнюю очистку кэша
        /// 
        public static TimeSpan WorkTime { get; private set; }

        /// 
        /// Количество устаревших объектов в кэше
        /// 
        public static int ObjectsExpired { get; private set; }

        /// 
        /// Количество объектов в кэше, оставшихся после очистки
        /// 
        public static int ObjectsInCache { get; private set; }
        
        static Timer _timer;
        static readonly object SyncTimer = new object();

        internal static void Init()
        {
            // когда выгружается домен приложения - останавливаем таймер
            AppDomain.CurrentDomain.DomainUnload += CurrentDomainDomainUnload;
            Start();
        }

        static void CurrentDomainDomainUnload(object sender, EventArgs e)
        {
            Stop();
        }

        private static void Start()
        {
            if (_timer == null)
                lock (SyncTimer)
                    if (_timer == null)
                    {
                        TimeSpan interval = TimeSpan.FromSeconds(60);
                        _timer = new Timer(Cleanup, null, interval, interval);
                    }
        }

        private static void Stop()
        {
            if (_timer != null)
                lock (SyncTimer)
                    if (_timer != null)
                    {
                        _timer.Dispose();
                        _timer = null;
                    }
        }

        // Очистка кэша
        private static void Cleanup(object state)
        {
            if (!Monitor.TryEnter(CacheContainer.MethodsCache.SyncRoot, 10))
            {
               // The Cache is busy, skip this turn.
               return;
            }

            DateTime start = DateTime.Now;
            LastCleanupTime = start;
            try
            {
                WorkTimes++;

                var list = new List();

                int objectsInCache = 0,
                objectsExpired = 0;
                        
                foreach (DictionaryEntry de in CacheContainer.MethodsCache)
                {
                    var cache = (IDictionary) de.Value;
                    lock (cache.SyncRoot)
                    {
                        foreach (DictionaryEntry entry in cache)
                        {
                            if (((CachedItem) entry.Value).IsExpired)
                                list.Add(entry);
                        }
                        foreach (DictionaryEntry toRemove in list)
                        {
                            cache.Remove(toRemove.Key);
                            objectsExpired++;
                        }
                    }
                    list.Clear();
                    objectsInCache += cache.Count;
                }

                ObjectsExpired = objectsExpired;
                ObjectsInCache = objectsInCache;
            }
            catch(Exception ex)
            {
                Trace.WriteLine(ex);
            }
            finally
            {
                WorkTime += DateTime.Now - start;
                Monitor.Exit(CacheContainer.MethodsCache.SyncRoot);
            }
        }
    }

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

Рейтинг
( Пока оценок нет )
Загрузка ...