我们在我们的应用程序中练习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;
}
}
关于我们是否应该在查询或属性上设置接口存在争议。当前使用接口是因为您可以根据缓存的内容以编程方式更改缓存键。即你可以添加实体&#39; id进入缓存键(即具有&#34; person_55&#34;,&#34; person_56&#34;等等)。
问题当然是缓存失效(命名和缓存失效,嗯?)。问题在于查询与命令或实体一对一不匹配。并且执行单个命令(即修改人员记录)应该呈现无效的多个缓存记录:人员记录和下载人员&#39;名。
目前,我有几位候选人参与解决方案:
ICacheRelated
并将所有这些键作为此接口的一部分返回。当EntityFramework更新/创建记录时,获取这些缓存密钥并使其无效。 (哈克!)ICacheInvalidatingCommand
应该返回应该失效的缓存键列表。并且在ICommandHandler
上有一个装饰器,它会在执行命令时使缓存失效。我不喜欢任何选项(可能除了4号)。但我认为选项2是我要放弃的选择。问题是,缓存密钥生成变得混乱,我需要在知道如何生成密钥的命令和查询之间有一个共同点。另一个问题是,它太容易添加另一个缓存查询并错过命令的无效部分(或者不是所有应该无效的命令都将失效)。
有更好的建议吗?
答案 0 :(得分:11)
我想知道你是否应该真的在这里进行缓存,因为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 new 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循环中存活,您需要确保在应用程序重新启动时获得完全相同的密钥(这意味着序列化属性的排序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 :(得分:1)
您使用的是单独的读写模型吗?如果是这样,也许您的“投影”类(处理来自写模型的事件并在读模型上执行CRUD的类)可能同时使相应的缓存条目无效。