Feel Good.

10 марта 2011

Caching в WCF сервисе

Кэширование данных неотъемлемая часть высоконагруженных и ресурсоемких проектов. В этой статье я опишу процесс организации оптимального механизма кэширования в WCF сервисе. Сформулирую задачу: имея WCF сервис, нужно организовать кеширование результатов работы любого сервис-метода в зависимости от входных параметров (Caching Support for WCF Services). Существует множество решений поставленной задачи, и самое простое и очевидное, это организовать кэширование прямо в сервис-методе:

public DateTime GetDate()

{

    ICache cache = ...; // получим экземпляр кэша

 

    string key = "somekey";

    object value;

 

    // Попробуем достать данные из кэша

    if (!cache.TryGet(key, out value))

    {

        // Данные в кэше отсутствуют,

        // получим новые данные

        value = DateTime.Now;

        // и поместим их в кэш на 5 секунд

        cache.Add(key, value, TimeSpan.FromSeconds(5));

    }

    return (DateTime)value;

}


Из примера видно, что сервис-метод GetDate перегружен логикой кэширования, и с появлением еще одного аналогичного сервис-метода, логика кэширования будет просто скопирована в него. Очевидно, что такой подход приведет к тому, что со временем такой код станет труднее поддерживать при изменении схемы кэширования, и естественно возникает вопрос об организации функционала сквозного кэширования. Организовать такую функциональность можно через AOP или в случае с WCF можно воспользоваться стандартным механизмом перехвата вызова через специальные интерфейсы (WCF Interceptor Interfaces), а точнее через интерфейс IOperationInvoker.
И, предже чем приступить к реализации наметим основные шаги:
  1. Для начала создадим простой WCF сервис
  2. Объявим интерфейс кэша ICache
  3. Реализуем свой перехватчик (IOperationInvoker)
  4. И закончим реализацией IOperationBehavior атрибута для нашего перехватчика

Шаг 1. Создадим новый WCF проект, назовем его MyService, с контрактом в виде интерфейса:

[ServiceContract]

public interface IMyService

{

    [OperationContract]

    DateTime GetDate();

}


Сервис метод GetDate() будет возвращать текущее время на сервере (этот метод и будем кэшировать).

public class MyService : IMyService

{

    public DateTime GetDate()

    {

        return DateTime.Now;

    }

}


Шаг 2. Наш WCF сервис будет работать с кэшем, но пока у нас нет конкретной реализации кэша, поэтому определим только интерфейс:

public interface ICache

{

    bool TryGet(string key, out object value);

    void Add(string key, object value, TimeSpan timeout);

}


Для простоты, в интерфейсе только два метода, для извлечения из кэша (TryGet) и помещение в кэш (Add).
Шаг 3. Теперь самое интересное. Когда мы делаем вызов любого сервис-метода из клиентского приложения, мы посылаем WCF сервису сообщение (SOAP), которое содержит: название метода, входные параметры для метода и прочее. Далее WCF сервис, получив запрос от клиента в виде сообщения (SOAP), наравляет его на обработку в свой Dispatcher - некий конвеер по обработке запроса, который можно разбить на два последовательных участка: DispatchRuntime и DispatchOperation. Нас будет интересовать последний, так как именно там (а более точнее в экземпляре объекта типа IOperationInvoker) происходит непосредственный вызов сервис метода с передачей ему уже десериализованных входных параметров и получение результатов выполнения, которые мы и собираемся кэшировать. Стандартный IOperationInvoker не умеет кэшировать, поэтому придется написать свой:

public class CacheOperationInvoker : IOperationInvoker

{

    IOperationInvoker _invoker;

    ICache _cache;

    TimeSpan _timeout;

 

    public CacheOperationInvoker(IOperationInvoker invoker, ICache cache, TimeSpan timeout)

    {

        _invoker = invoker; // стандартный OperationInvoker

        _cache = cache;     // наш объект кэша

        _timeout = timeout; // как долго объект будет находиться в кэше

    }

 

    public object Invoke(object instance, object[] inputs, out object[] outputs)

    {

        // Хэш ключ.

        // Разумно вычислять его на основе

        // входных параметров и названия кешируемого метода.

        // Но мы для простоты обойдемся константой.

        string key = "somekey";

 

        Tuple<object, object[]> result;

        object cacheItem;

        // Попробуем достать данные из кэша

        if (!_cache.TryGet(key, out cacheItem))

        {

            // Данные в кэше отсутствуют,

            // получим новые данные, вызовем базовый Invoke

            object returnValue = _invoker.Invoke(instance, inputs, out outputs);

 

            // Подготовим новый элемент кэша (упакуем returnValue и outputs)

            result = Tuple.Create<object, object[]>(returnValue, outputs);

 

            // Выполним кэширование на указанный timeout

            _cache.Add(key, result, _timeout);

        }

        else

        {

            // Данные нашлись в кэше

            result = cacheItem as Tuple<object, object[]>;

        }

        outputs = result.Item2;

        return result.Item1;

    }

 

    public object[] AllocateInputs()

    {

        // просто пробрасываем вызов

        return _invoker.AllocateInputs();

    }

 

    public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback callback, object state)

    {

        // просто пробрасываем вызов

        return _invoker.InvokeBegin(instance, inputs, callback, state);

    }

 

    public object InvokeEnd(object instance, out object[] outputs, IAsyncResult result)

    {

        // просто пробрасываем вызов

        return _invoker.InvokeEnd(instance, out outputs, result);

    }

 

    public bool IsSynchronous

    {

        // просто пробрасываем вызов

        get { return _invoker.IsSynchronous; }

    }

}


Шаг 4. Вся основная работа уже сделана, осталось как то наделить наш севис метод данным поведением. Для этого надо реализовать атрибут, который к тому же еще и IOperationBehavior:

public class CacheBehaviorAttribute : Attribute, IOperationBehavior

{

    TimeSpan _timeout;

 

    public CacheBehaviorAttribute(int seconds)

    {

        _timeout = TimeSpan.FromSeconds(seconds);

    }

 

    public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)

    {

        // Стандартный OperationInvoker

        IOperationInvoker invoker = dispatchOperation.Invoker;

 

        // Наш кэш, разумно сделать его singleton

        // и получать из DI/IoC-контейнера

        ICache cache = new MyCache();

 

        // Подменим стандартный OperationInvoker расширенным.

        dispatchOperation.Invoker = new CacheOperationInvoker(invoker, cache, _timeout);

    }

 

    public void Validate(OperationDescription operationDescription)

    {

    }

 

    public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)

    {

    }

 

    public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)

    {

    }

}


и не забываем пометить этим атрибутом севис-метод в сервис-интерфейсе:

[ServiceContract]

public interface IMyService

{

    [OperationContract]

    [CacheBehavior(5)] // Кэшируем результат на 5 секунд

    DateTime GetDate();

}



PS: Если Вы реализуете кэширование для WCF Web HTTP Services, то нужно использовать стандартный механизм кэширования ASP.net. Пример, приведенный в статье, демонстрирует общий подход к решению проблемы.

Ссылки:
  1. Extending WCF with Custom Behaviors

Комментариев нет:

Отправка комментария