Deprecated: Non-static method DbSimple_Generic::connect() should not be called statically, assuming $this from incompatible context in /home/u36306/netcoder.ru/www/classes/modules/sys_database/Database.class.php on line 66

Deprecated: Non-static method DbSimple_Generic::parseDSN() should not be called statically, assuming $this from incompatible context in /home/u36306/netcoder.ru/www/classes/lib/external/DbSimple/Generic.php on line 89

Deprecated: Non-static method DbSimple_Generic::parseDSN() should not be called statically, assuming $this from incompatible context in /home/u36306/netcoder.ru/www/classes/lib/external/DbSimple/Mysql.php on line 35
Реализация кэширования средствами Unity / C# / Netcoder.ru

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

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

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

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

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

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

        /// <summary>
        /// Конструктор с указанием времени хранения результатов выполнения метода в кэше в секундах
        /// </summary>
        /// <param name="cacheTimeSeconds"></param>
        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;

        /// <summary>
        /// Конструктор
        /// </summary>
        /// <param name="maxCacheTime"></param>
        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;
        
        /// <summary>
        /// Кэш для кэшей методов
        /// </summary>
        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;
        }

        /// <summary>
        /// Проверить существует ли закэшированное значение. Если да - вернуть его
        /// </summary>
        /// <param name="methodInvocation"></param>
        /// <returns></returns>
        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;
        }

        /// <summary>
        /// Добавить результат выполнения метода в кэш
        /// </summary>
        /// <param name="mr"></param>
        /// <param name="methodInvocation"></param>
        /// <param name="maxCacheTime"></param>
        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
    {

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

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

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

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

        /// <summary>
        /// Количество объектов в кэше, оставшихся после очистки
        /// </summary>
	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<DictionaryEntry>();

		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 чтобы быстро и безболезненно внедрить кэширование в вашем приложении, даже если вы изначально об этом и не задумывались. Результаты внедрения кстати превзошли самые смелые ожидания. Время выполнения некоторых методов, которые к тому же вызывались достаточно часто и создавали видимую нагрузку на БД, сократилось в сотни раз.
В комментариях готов ответить на все возникшие вопросы =)
  • +2
  • 1 июня 2010, 22:29
  • tonage

Комментарии (2)

RSS свернуть / развернуть
+
0
Молодец, Антоха!
Все грамотно расписал :)
avatar

NetCoder

  • 2 июня 2010, 08:15
+
0
старался =)
avatar

tonage

  • 2 июня 2010, 09:24

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.