2014-10-13 3 views
12

Мы используем архитектуру CQRS в нашем приложении, то есть у нас есть ряд классов, реализующих ICommand, и для каждой команды есть обработчики: ICommandHandler<ICommand>. То же самое относится к поиску данных - у нас есть IQUery<TResult> с IQueryHandler<IQuery, TResult>. Довольно часто в эти дни.Недействительность кэша в приложении CQRS

Некоторые запросы используются очень часто (для нескольких выпадающих страниц на страницах), и имеет смысл кэшировать результат их выполнения. Таким образом, у нас есть декоратор вокруг IQueryHandler, который кэширует выполнение запросов.
Запросы реализуют интерфейс ICachedQuery, а декоратор кэширует результаты. Например:

public interface ICachedQuery { 
    String CacheKey { get; } 
    int CacheDurationMinutes { get; } 
} 

public class CachedQueryHandlerDecorator<TQuery, TResult> 
    : IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult> 
{ 
    private IQueryHandler<TQuery, TResult> decorated; 
    private readonly ICacheProvider cacheProvider; 

    public CachedQueryHandlerDecorator(IQueryHandler<TQuery, TResult> decorated, 
     ICacheProvider cacheProvider) { 
     this.decorated = decorated; 
     this.cacheProvider = cacheProvider; 
    } 

    public TResult Handle(TQuery query) { 
     var cachedQuery = query as ICachedQuery; 
     if (cachedQuery == null) 
      return decorated.Handle(query); 

     var cachedResult = (TResult)cacheProvider.Get(cachedQuery.CacheKey); 

     if (cachedResult == null) 
     { 
      cachedResult = decorated.Handle(query); 
      cacheProvider.Set(cachedQuery.CacheKey, cachedResult, 
       cachedQuery.CacheDurationMinutes); 
     } 

     return cachedResult; 
    } 
} 

Было дебаты о том, следует ли иметь интерфейс по запросам или атрибуту. Интерфейс в настоящее время используется, потому что вы можете программно изменить ключ кеша в зависимости от того, что кэшируется. То есть вы можете добавить идентификатор сущности в кэш-ключ (т. е. иметь такие ключи, как «person_55», «person_56» и т. д.).

Вопрос, конечно же, заключается в недействительности кеша (именования и недействительности кэша, а?). Проблема в том, что запросы не совпадают друг с другом с командами или сущностями. И выполнение одной команды (т. Е. Модификации записи человека) должно отображать недействительные множественные записи кеша: запись человека и выпадающие имена людей.

На данный момент у меня есть несколько кандидатов для решения:

  1. есть все ключи кэша, записанные как-то в классе сущностей, отмечающий объект как ICacheRelated и вернуть все эти ключи как часть этого интерфейса. И когда EntityFramework обновляет/создает запись, получите эти ключи кеша и аннулирует их. (Hacky!)
  2. Команды должны аннулировать все кеши. Вернее, ICacheInvalidatingCommand, который должен вернуть список ключей кеша, которые должны быть недействительными. И у вас есть декоратор на ICommandHandler, который приведет к аннулированию кеша при выполнении команды.
  3. Не аннулировать кеши, просто установите короткие периоды времени кеширования (как короткий?)
  4. Магия бобы.

Мне не нравится ни один из вариантов (возможно, кроме номера 4). Но я думаю, что вариант 2 - это тот, который я отдам. Проблема с этим, создание кеш-ключа становится беспорядочным, мне нужно иметь общее место между командами и запросами, которые умеют генерировать ключи. Другая проблема заключалась бы в том, что будет слишком легко добавить другой кешированный запрос и пропустить часть недействительной команды (или не все команды, которые должны быть недействительными, будут аннулированы).

Любые лучшие предложения?

+0

Почему у вашего 'CachedQueryHandlerDecorator' есть ограничение общего типа' где TQuery: ICachedQuery'? – Steven

+0

мм .. никогда, хотя и не уверен, что регистрация Autofac будет изменена. Я попробую – trailmax

+0

Ой, извините. Я не знал, что вы используете Autofac. Неважно, вы не можете сделать это с помощью Autofac. – Steven

ответ

10

Мне интересно, нужно ли вообще делать кэширование здесь, поскольку SQL-сервер довольно хорош в результатах кэширования, поэтому вы должны видеть запросы, которые возвращают фиксированный список выпадающих значений, чтобы быть очень быстрыми.

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

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

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

Например:

public class AddCityCommandHandler : ICommandHandler<AddCityCommand> 
{ 
    private readonly IRepository<City> cityRepository; 
    private readonly IGuidProvider guidProvider; 
    private readonly IDomainEventPublisher eventPublisher; 

    public AddCountryCommandHandler(IRepository<City> cityRepository, 
     IGuidProvider guidProvider, IDomainEventPublisher eventPublisher) { ... } 

    public void Handle(AddCityCommand command) 
    { 
     City city = cityRepository.Create(); 

     city.Id = this.guidProvider.NewGuid(); 
     city.CountryId = command.CountryId; 

     this.eventPublisher.Publish(new CityAdded(city.Id)); 
    } 
} 

Здесь вы опубликовать CityAdded событие, которое могло бы выглядеть следующим образом:

public class CityAdded : IDomainEvent 
{ 
    public readonly Guid CityId; 

    public CityAdded (Guid cityId) { 
     if (cityId == Guid.Empty) throw ArgumentException(); 
     this.CityId = cityId; 
    } 
} 

Теперь вы можете иметь ноль или более абонентов к этому событию:

public class InvalidateGetCitiesByCountryQueryCache : IEventHandler<CityAdded> 
{ 
    private readonly IQueryCache queryCache; 
    private readonly IRepository<City> cityRepository; 

    public InvalidateGetCitiesByCountryQueryCache(...) { ... } 

    public void Handle(CityAdded e) 
    { 
     Guid countryId = this.cityRepository.GetById(e.CityId).CountryId; 

     this.queryCache.Invalidate(new GetCitiesByCountryQuery(countryId)); 
    } 
} 

Здесь у нас есть специальный обработчик событий, который обрабатывает CityAdded d omain просто для того, чтобы инсталлировать кеш для GetCitiesByCountryQuery. IQueryCache - это абстракция, специально созданная для кэширования и аннулирования результатов запроса. InvalidateGetCitiesByCountryQueryCache явно создает запрос, результаты которого должны быть недействительными. Этот метод Invalidate может использовать интерфейс ICachedQuery для определения его ключа и недействительности результатов (если таковые имеются).

Вместо определения ICachedQuery для определения ключа, однако, я просто сериализую весь запрос на JSON и использую его как ключ. Таким образом, каждый запрос с уникальными параметрами автоматически получит свой собственный ключ и кеш, и вам не нужно реализовывать его в самом запросе. Это очень безопасный механизм. Однако, если ваш кеш должен пережить переработку AppDomain, вам нужно убедиться, что вы получаете точно такой же ключ для перезапуска приложений (что означает упорядочение сериализованных свойств must be guaranteed).

Одна вещь, которую вы должны иметь в виду, однако заключается в том, что этот механизм особенно подходит в случае возможной последовательности. Чтобы взять предыдущий пример, когда вы хотите сделать недействительным кеш? Прежде чем добавить город или после? Если вы ранее не выполнили проверку кеша, возможно, что кеш будет заселен до того, как вы сделаете фиксацию. Конечно, это будет сосать. С другой стороны, если вы это сделаете сразу после этого, возможно, что кто-то все еще наблюдает за старым значением сразу после. Особенно, когда ваши события помещаются в очередь и обрабатываются в фоновом режиме.

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

public class EventProcessorCommandHandlerDecorator<T> : ICommandHandler<T> 
{ 
    private readonly EventPublisherImpl eventPublisher; 
    private readonly IEventProcessor eventProcessor; 
    private readonly ICommandHandler<T> decoratee; 

    public void Handle(T command) 
    { 
     this.decotatee.Handle(command); 

     foreach (IDomainEvent e in this.eventPublisher.GetQueuedEvents()) 
     { 
      this.eventProcessor.Process(e); 
     } 
    } 
} 

Здесь декоратор напрямую зависит от реализации издателя события, чтобы называть GetQueuedEvents() метод, который будет недоступен из интерфейса IDomainEventPublisher. И мы перебираем все события и передаем эти события на медиатор IEventProcessor (который работает только как IQueryProcessor).

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

Этот дизайн может показаться излишним для кеширования, но как только вы начали публиковать события в домене, вы начнете видеть множество вариантов использования для них, что значительно упростит работу с вашей системой.

+1

Мне нравится ваш трюк, чтобы строка с сериализацией Json была кеш-ключом - очень аккуратно! Что касается событий, у нас уже есть статический вызов 'DomainEvents.Raise()' и обработчика событий домена, очень похожий на то, что вы здесь описываете, поэтому не переполняйте. Просто нужно иметь общий запрос ClearCacheEvent (IQuery query), который будет делать недействительность кэша. – trailmax

+0

Что касается кеш-кеширования и кеширования SQL-сервера: мы находимся в Azure, и SQL Server обычно сидит на другой виртуальной машине, возможно, в другом конце центра обработки данных. И время ожидания сети начинается, когда у нас есть более 10 выпадающих списков, даже если выпадающие списки - это просто «выбрать» из ProductTypes' с 30 записями в таблице. – trailmax

+1

В следующий раз, когда я буду заниматься кешированием, я буду реализовывать ваши материалы и расскажу вам, как это работает на производстве. Спасибо за Ваш ответ! – trailmax

1

Вы используете отдельную модель для чтения и записи? Если это так, возможно, ваши «проекционные» классы (те, которые обрабатывают события из модели записи и делают CRUD на модели чтения) могут одновременно аннулировать соответствующие записи кэша.

+0

Это в значительной степени мой вариант номер 2, где команды знают, какие кеши недействительны. – trailmax

Смежные вопросы