Упрощенный распределенный кэш
В данной статье рассмотрены два запроса - передача запросов от клиента к серверу и от сервера приложений к серверу баз данных.
Ища в поисковой машине Google или читая рекламу на технических сайтах или в журналах, можно легко найти много изощренных решений для кэширования.
Например: MemCached, NCache, ScaleOut StateServer,
Shared Cache(совместная кэш-память) и даже реализация Microsoft под названием Velocity(скорость).
Иногда эти навороченные решения прекрасно подходят для проекта. Они были написаны профессионалами, тщательно изучившими различные ситуации (или состояния) кэширования. Некоторые из решений уже зарекомендовали себя в производственной среде. Так зачем изобретать колесо? Но…
Основы кэширования весьма простые, так почему бы не взять полный контроль на себя?
Механизм кэширования является одной из основных инфраструктур в любом среднем и большом проекте, в веб-приложении или в приложении winForm.
Главная цель кэширования – избавиться от передачи запросов туда-обратно: от клиента – к серверу, от сервера приложений – к серверу баз данных, и порой от веб-сервера – к серверу приложений.
Последний запрос (от веб-сервера – к серверу приложений) преимущественно используется в веб-сайтах для сохранения HTML-результата общих запросов вместо неоднократного перенаправления их приложению. В данной статье рассмотрены остальные два запроса.
В каждом приложении есть таблицы поиска (для заполнения комбинированных списков списком значений), таблицы решений и другие статические или полустатические таблицы. Эти таблицы читаются повторно всегда, когда открывается форма в приложении, содержащем их, и/или когда потоку приложения нужны его значения - эти передачи запросов туда-обратно, от клиента к серверу и от сервера приложений к базе данных, являются расточительством ресурсов, которого можно было бы избежать, если бы значения и таблицы были сохранены в памяти клиента и сервера приложений.
На первом шаге надо решить, насколько далеко можно зайти с этим.
1. Какие статические таблицы часто используются?
2. Насколько они велики? (память на сервере и клиенте не бесконечна).
3. Какой вид доступа к этим таблицам требуется? (если в таблицах ищется много с помощью соединений, невыгодно кэшировать их).
4. Если эти таблицы может обновлять пользователь, насколько важно обновлять их, и если да, то какой частоты обновления достаточно?
Ответы на эти вопросы отличаются в каждом приложении, но основными принципами являются: кэшировать большинство (а то и все) маленьких статических таблиц, используемых в приложении, и вообще не кэшировать большие таблицы, часто обновляемые таблицы или таблицы, старый снимок реального состояния которых запрашивать недопустимо (речь о давности в несколько секунд).
Остается выяснить, каков предел средних таблиц (по размеру) и что значит частое обновление – как сказано ранее, это меняется от приложения к приложению.
Разработка начинается с базы данных:
1. таблица sysApplicationServers: Эта таблица будет служить таблицей регистрации, каждый сервер приложений при загрузке будет регистрировать себя в ней и будет разрегистрировать себя при выгрузке. Столбцы: IpAddress, FromDate.
2. Таблица CacheItemQueue: Эта таблица будет содержать таблицы, необходимые для обновления.
3. trig[имя таблицы] триггер кэша: Каждая динамическая и кэшированная таблица будет иметь триггер, этот триггер будет добавлять строку в CacheItemQueue для каждой строки sysApplicationServers (при insert, update и delete).
Например: Имеется два сервера приложений, зарегистрированных в sysApplicationServers. При обновлении таблицы пользователей результатом триггера является простая вставка с результатом:
Table, IpAddress, FromDate
-------------------------------------------
Users, appServer1IP, now
Users, appServer2IP, now
Данный простой механизм будет сообщать об изменении таблицы, чтобы можно было обновить ее в снимке памяти (вставка вставит новую строку, только если не существует строки для такого же сочетания таблица+сервер).
Далее будет написан поток, проверяющий таблицу CacheItemQueue в требуемом интервале, этот поток будет работать в бесконечном цикле с загрузки приложения.
Когда он находит новую таблицу для загрузки, он загружает и удаляет эту строку из cacheItemQueue.
...
CacheListenerThread cacheListenerThread = new CacheListenerThread();
thread = new Thread(cacheListenerThread.RunListener);
thread.Start();
while (true)
{
Thread.Sleep(Convert.ToInt32
(AppConfigManager.GetAppSettingsValue("CacheRefreshInterval")));
RefreshCache();
}
public void RefreshCache()
{
string ipAddress = BasicUtil.GetLocalIp();
SqlCommand command = new SqlCommand("spCache_AsyncTablesLoader");
DatabaseManager.AddSqlParameter(command, "@ipAddress", ipAddress);
RefreshCacheInnerImpl(command);
}
...
Фактический кэш можно создать с помощью ASP.NET, имеющего хорошую реализацию. Была выбрана корпоративная библиотека Microsoft, чтобы обеспечить работу кэша на сервере веб-приложений (в данном случае служба Windows).
Интерфейс менеджера кэша объясняет сам себя:
public interface ICacheManager
{
object Get(string key);
bool Add(string key, object value);
bool Contains(string key);
void LoadDataTable(string tableName);
}
Метод LoadDataTable позволит сохранить таблицы кэша, загружаемые только при первом использовании или перезагружаемые при необходимости.
Данная структура в основном используется для хранения всевозможных dataTables, но, как видно, она построена, чтобы содержать любой объект и начинку кэша с датой или временем истечения срока действия.
Серверная реализация менеджера кэша:
..
using Microsoft.Practices.EnterpriseLibrary.Caching;
using Microsoft.Practices.EnterpriseLibrary.Caching.Expirations;
..
public class ServerCache : ICacheManager
{
public delegate bool LoadDataTableDelegate(string tableName);
private static ServerCache serverCacheManager;
private CacheManager cacheManager;
private event LoadDataTableDelegate loadDataTableEvent;
private ServerCache()
{
try
{
cacheManager = CacheFactory.GetCacheManager();
}
catch (Exception ex)
{
throw new ApplicationException("Failed to initilize cache manager", ex);
}
}
public static void InitCache(LoadDataTableDelegate loadDataTable)
{
serverCacheManager = new ServerCache();
serverCacheManager.loadDataTableEvent += loadDataTable;
}
/// <span class="code-SummaryComment"><summary>
</span> /// отложено получить кэш сервера.
/// <span class="code-SummaryComment"></summary>
</span> /// <span class="code-SummaryComment"><returns></returns>
</span> public static ServerCache GetServerCache()
{
if (serverCacheManager == null)
{
string message = "cache was not loaded (should call InitCache)";
throw new ApplicationException(message);
}
return serverCacheManager;
}
/// <span class="code-SummaryComment"><summary>
</span> /// получить значение из кэша по заданному ключу
/// <span class="code-SummaryComment"></summary>
</span> /// <span class="code-SummaryComment"><param name=""key""></param>
</span> /// <span class="code-SummaryComment"><returns></returns>
</span> public object Get(string key)
{
return cacheManager.GetData(key);
}
/// <span class="code-SummaryComment"><summary>
</span> /// проверить, существует ли в кэше объект с заданным ключом
/// <span class="code-SummaryComment"></summary>
</span> /// <span class="code-SummaryComment"><param name=""key""></param>
</span> /// <span class="code-SummaryComment"><returns></returns>
</span> public bool Contains(string key)
{
return cacheManager.Contains(key);
}
/// <span class="code-SummaryComment"><summary>
</span> /// добавить элемент в кэш
/// <span class="code-SummaryComment"></summary>
</span> /// <span class="code-SummaryComment"><param name=""key""></param>
</span> /// <span class="code-SummaryComment"><param name=""value""></param>
</span> /// <span class="code-SummaryComment"><returns>истина, если ключ был переопределен</returns>
</span> public bool Add(string key, object value)
{
//если ключ уже существует – просмотреть значение и вернуть ложь
bool result = (cacheManager.Contains(key));
cacheManager.Add(key, value);
return result;
}
/// <span class="code-SummaryComment"><summary>
</span> /// добавить элемент в кэш с временем ожидания
/// <span class="code-SummaryComment"></summary>
</span> /// <span class="code-SummaryComment"><param name=""key""></param>
</span> /// <span class="code-SummaryComment"><param name=""value""></param>
</span> /// <span class="code-SummaryComment"><param name=""expirationTime""></param>
</span> /// <span class="code-SummaryComment"><returns>истина, если ключ был переопределен</returns>
</span> public bool Add(string key, object value, TimeSpan expirationTime)
{
//проверить наличие объекта, уже кэшированного с таким же ключом
bool result = (cacheManager.Contains(key));
cacheManager.Add(key, value, CacheItemPriority.Normal,
null, new SlidingTime(expirationTime));
return result;
}
public void LoadDataTable(string tableName)
{
loadDataTableEvent(tableName);
}
}
Клиентская реализация ICacheManager еще проще, он содержит статический словарь объектов, метод LoadDataTable может указать на делегат шлюза сервера или может быть оставлен в покое, если на сторону клиента загружаются только статические таблицы.
public class ClientCache : ICacheManager
{
private static ClientCache clientCacheManager;
private static Dictionary<string,> cacheMap;
private ClientCache()
{
cacheMap = new Dictionary<string,>();
}
public static ClientCache GetClientCache()
{
if (clientCacheManager == null)
{
clientCacheManager = new ClientCache();
}
return clientCacheManager;
}
public object Get(string key)
{
object result;
cacheMap.TryGetValue(key, out result);
return result;
}
/// <span class="code-SummaryComment"><summary>
</span> /// проверить, существует ли в кэше объект с заданным ключом
/// <span class="code-SummaryComment"></summary>
</span> /// <span class="code-SummaryComment"><param name=""key""></param>
</span> /// <span class="code-SummaryComment"><returns></returns>
</span> public bool Contains(string key)
{
return cacheMap.ContainsKey(key);
}
public bool Add(string key, object value)
{
bool overrideKey = cacheMap.ContainsKey(key);
if (overrideKey)
{
lock (cacheMap)
{
cacheMap.Remove(key);
cacheMap.Add(key, value);
}
}
else
{
cacheMap.Add(key, value);
}
return overrideKey;
}
public void LoadDataTable(string tableName)
{
string message =
string.Format("table {0} was not loaded to client cache", tableName);
throw new ValidationException(message);
}
}
Чтобы обеспечить конфигурирование кэша, за исключением интервала просмотра cacheItemQueue, используется простой XML, содержащий список кэшируемых таблиц.
Каждый элемент в XML содержит три атрибута (за исключением имени таблицы, конечно):
1. loadOnStart: загрузить при загрузке приложения или при первом вызове.
2. loadToClient: включить таблицу в ответ на клиентский метод "getCache" при загрузке клиента.
3. refreshOnUpdate: true, если таблица кэша обновляемая (чтобы снабдить все таблицы с "refreshOnUpdate" триггерами, применяется утилита развертывания, с помощью того же XML автоматически создающая триггеры, и при загрузке приложения проверяется, что это совпадает).
Обобщение основной идеи:
1. При загрузке приложения данные кэша извлекаются в память сервера приложений, сервер регистрирует себя, чтобы получить обновления, и запускает поток, проверяющий наличие обновлений.
2. Каждая динамическая таблица в кэше имеет триггер, служащий для уведомления сервера приложений об обновлении и принуждения его к обновлению. Можно обновить целую таблицу (обычно маленькие таблицы с малым числом записей), или с помощью столбца метки времени можно определить, какие строки обновились, и выборочно обновить кэш.
3. Каждый клиент при загрузке извлекает снимок статических кэшированных таблиц, чтобы избавиться от передачи запросов туда-обратно к серверу.