При разработке любого высоконагруженного приложения рано или поздно встаёт вопрос кэширования данных. Ведь практически всегда есть часть информации которая изменяется достаточно редко и используется в качестве справочной. Постоянно запрашивать такую информацию из БД достаточно накладно, да и по большому счёту бессмысленно.
Наступил момент, когда и я при разработке такого высоконагруженного приложения столкнулся с этой проблемой. И решил её достаточно просто и безболезненно с помощью Unity Application Block.
Изначально Unity в проекте использовался по своему прямому назначению т.е. для внедрения зависимостей. Но когда встала задача кэширования, я вспомнил, что Unity позволяет осуществлять перехват вызова методов, а значит есть возможность реализовать парадигму аспекто-ориентированного программирования, что для данной задачи подходит как нельзя лучше.
Создание атрибута
Создадим специальный атрибут, которым будем помечать методы, результаты выполнения которых будут храниться в кэше приложения. Необходимо помнить, что разные данные устаревают с разной скоростью и для этого в атрибут добавлено время хранения данных в кэше, по истечении которого эти данные считаются устаревшими и удаляются из кэша автоматически.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
/// <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.
Далее нам нужно создать собственно перехватчик события вызова метода. Вот его код.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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, которая в свою очередь содержит в качестве ключа список аргументов и их значений, а в качестве значения — результат выполнения этих методов.
Сделал я это из соображений производительности, а точнее скорости поиска по контейнеру.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
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; } } } } } |
Ниже класс, который производит очистку кэша, а также используется для сбора некоторой статистики его использования. При его создании запускается таймер, раз в минуту удаляющий устаревшие объекты из кэша. При очистке сначала идёт попытка получить доступ к контейнеру, и если это не удаётся — очередная очистка просто пропускается.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
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 чтобы быстро и безболезненно внедрить кэширование в вашем приложении, даже если вы изначально об этом и не задумывались. Результаты внедрения кстати превзошли самые смелые ожидания. Время выполнения некоторых методов, которые к тому же вызывались достаточно часто и создавали видимую нагрузку на БД, сократилось в сотни раз.
Похожие записи: