Подтвердить что ты не робот

Недействительность кэша в приложении CQRS

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

Некоторые запросы используются очень часто (для нескольких выпадающих страниц на страницах), и имеет смысл кэшировать результат их выполнения. Итак, у нас есть декоратор вокруг 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" и т.д.).

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

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

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

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

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

4b9b3361

Ответ 1

Мне интересно, нужно ли вообще делать кэширование здесь, поскольку 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 только для того, чтобы заблокировать кеш для GetCitiesByCountryQuery. IQueryCache здесь - абстракция, специально созданная для кэширования и аннулирования результатов запроса. InvalidateGetCitiesByCountryQueryCache явно создает запрос, результаты которого должны быть недействительными. Этот метод Invalidate может не использовать интерфейс ICachedQuery для определения его ключа и недействительности результатов (если они есть).

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

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

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

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).

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

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

Ответ 2

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