При разработке любого высоконагруженного приложения рано или поздно встаёт вопрос кэширования данных. Ведь практически всегда есть часть информации которая изменяется достаточно редко и используется в качестве справочной. Постоянно запрашивать такую информацию из БД достаточно накладно, да и по большому счёту бессмысленно.
Наступил момент, когда и я при разработке такого высоконагруженного приложения столкнулся с этой проблемой. И решил её достаточно просто и безболезненно с помощью 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 чтобы быстро и безболезненно внедрить кэширование в вашем приложении, даже если вы изначально об этом и не задумывались. Результаты внедрения кстати превзошли самые смелые ожидания. Время выполнения некоторых методов, которые к тому же вызывались достаточно часто и создавали видимую нагрузку на БД, сократилось в сотни раз.
