看了园子里很多介绍Caching的文章,多数都只介绍基本机制,对于Cache更新和依赖部分,更是只简单的实现ICacheItemRefreshAction接口,这在实际项目中是远远不够的。实际项目中,至少应该考虑以下3点:
接下去分别展开。
0、前言
下面的讨论分成2大块,一是环境的搭建,包括用Unity提供服务实例+用Unity提供Interception。经过这一块,就可以AOP的方式实现Caching了,这部分园子里的文章很多,我简略的过一下。二是Caching的实现,分别从外部数据缓存+内部缓存+内部触发更新3部分来讨论。
1、环境的搭建
1.1、用Unity提供服务实例
这部分其实就是用Unity实现IInstanceProvider,再配合IEndpointBehavior之类的,来实现WCF与Unity的集成。核心代码如下:
1 public class UnityInstanceProvider : IInstanceProvider{ 2 public object GetInstance(InstanceContext context, Message msg){ 3 return UnityWrapper.Instance.Resolve(serviceType); 4 } 5 }
具体可参见Artech的这篇《WCF后续之旅(7):通过WCF Extension实现和Enterprise Library Unity Container的集成》。
1.2、用Unity提供Interception(即PIAB,PolicyInjection)
这里要先吐槽一下EntLib,向后的兼容性做的太差了,完全不符合微软一贯的风格。每出一个大版本就一堆Breaking Changes和Deprecated,真心有点郁闷。当然估计他们也有苦衷,比如EntLib6里的Caching,被包含到Net40里了,确实也只能放弃。好吧,回归正题。好在EntLib自带的帮助文档写的非常详尽,有问题应该优先去Manual里查看相关内容。
EntLib4.1的PIAB模块,到EntLib5里就完全基于Unity的Interception来实现了,到EntLib6里就干脆取消了。这里我们采用EntLib5里的配置来实现策略注入,核心配置如下:
1 <unity> 2 <sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration"/> 3 <assembly name="Service"/> 4 <namespace name="Service"/> 5 <alias alias="CachingCallHandler" type="Service.Unity.CachingCallHandler, Service"/> 6 <container name="UnityContainer"> 7 <extension type="Interception" /> 8 <interception> 9 <policy name="caching"> 10 <matchingRule name="allServiceMatch" type="NamespaceMatchingRule"> 11 <constructor> 12 <param name="namespaceName" value="Service"/> 13 </constructor> 14 </matchingRule> 15 <callHandler name="cachingService" type="CachingCallHandler"/> 16 </policy> 17 </interception> 18 <register type="IService1"> 19 <interceptor type="TransparentProxyInterceptor" /> 20 <policyInjection /> 21 </register> 22 </container> 23 </unity>
这里有个细节,为了方便配置,1是可以用EntLib里自带的EntLibConfig.exe,2是在vs菜单->XML->架构里,添加UnityConfiguration20.xsd或者EnterpriseLibrary.Configuration.xsd,这样就有智能提示了。如果有EntLib的安装目录里找不到.xsd文件,可以用Everything搜一下。
2、Caching的实现
2.1、外部数据缓存
外部数据是我们不可控的数据,什么时候更新不确定。我们采取定时更新的策略,更新的频率主要取决于外部数据的变化频率,比如我们项目里依赖是外部的基础数据,变化较少,所以只需1小时甚至半天更新一次。
原本我以为AbsoluteTime是我想要的(注:SlidingTime是用户第一次命中Cache后,再滑动一段时间才过期),但通过ILSpy查看内部代码才知道,EntLib不是在一到期就自动触发ICacheItemRefreshAction接口的,而是在用户访问Cache的时候,再根据时间判断是否过期,如果过期再触发RefreshAction。代码如下:
1 namespace Microsoft.Practices.EnterpriseLibrary.Caching{ 2 public class Cache{ 3 public object GetData(string key){ 4 if(cacheItem.HasExpired()){ 5 this.inMemoryCache.Remove(key); 6 RefreshActionInvoker.InvokeRefreshAction(cacheItem, CacheItemRemovedReason.Expired, this.instrumentationProvider); 7 this.instrumentationProvider.FireCacheExpired(1L); 8 }}}}
这样的用户体验当然差了些,不能让用户享受到Caching的全部好处,我们改用Timer自动更新Cache,注意请使用System.Threading.Timer,而不是System.Timers.Timer。代码如下://我们用自定义的TimerCacheAttribute来标识这类方法
1 var atts = input.MethodBase.GetCustomAttributes(typeof(TimerCacheAttribute), false); 2 if(atts.Length > 0){ 3 RefreshTimerUtil.Instance[key] = new Timer(e => { 4 object[] args = e as object[]; 5 if(args == null || args.Length != 2) return; 6 7 var tmpInput = args[0] as IMethodInvocation; 8 var tmpNext = args[1] as GetNextHandlerDelegate; 9 var tmpReturn = tmpNext()(tmpInput, tmpNext); 10 if(tmpReturn.Exception == null) { 11 CacheWrapper.Instance.Add(key, tmpReturn.ReturnValue); 12 } 13 }, 14 new object[] { input, getNext }, 15 TimeSpan.Zero, 16 new TimeSpan(1, 0, 0)); 17 }
2.2、内部数据缓存
内部数据可以直截了当的插入Cache,然后就静静的等待增删改操作来把它置为过期。
1 CacheWrapper.Instance.Add(key, methodReturn.ReturnValue, 2 CacheItemPriority.Normal, 3 new CacheRefreshAction { Input = input, GetNext = getNext });
当然,要实现ICacheItemRefreshAction接口,代码如下:
1 [Serializable] 2 public class CacheRefreshAction : ICacheItemRefreshAction { 3 public IMethodInvocation Input {get; set;} 4 public GetNextHandlerDelegate GetNext {get; set;} 5 public void Refresh(string removedKey, object expiredValue, CacheItemRemovedReason removalReason) { 6 if( Input == null || GetNext == null) return; 7 var methodReturn = GetNext()(Input, GetNext); 8 if(methodReturn.Exception == null) { 9 CacheWrapper.Instance.Add(removedKey, methodReturn.ReturnValue, 10 CacheItemPriority.Normal, 11 new CacheRefreshAction { Input = Input, GetNext = GetNext }); 12 } 13 } 14 }
据说MemoryCache是不需要实现ISerializable的,而其他Caching要么实现ISerializable、要么打[Serializable]标签。不过这种实现方式,IMethodInvocation和GetNextHandlerDelegate目测序列化都略麻烦,好在目前我们只用到MemoryCache,可以先不管,以后再突破。
2.3、内部数据触发更新
最后就剩触发更新了,如何实现类似Add/Delete/Update[User][s]->Get[User][s]的效果呢?大致思路是:1)判断方法名里是否StartWith关键字(Add/Delete/Update);2)截取出目标关键字User、并生成对应的复数形式Users;3)与Get等动作拼接成CacheKey,把所有命中的CacheKey置为过期(CacheManager.Remove),即可触发对应的RefreshAction。代码如下:
1 var entity = string.Empty; 2 foreach (var act in ConfigUtil.Action){ 3 if(methodName.StartsWith(act)) { 4 entity = methodName.Substring(act.Length); 5 break; 6 }} 7 8 var entities = PluralUtil.Pluralize(entity); 9 if(entities == entity) entities = PluralUtil.Singularize(entity); 10 foreach (var prefix in ConfigUtil.Prefix) { 11 CacheWrapper.Instance.RemoveStartWith(prefix + entity); 12 CacheWrapper.Instance.RemoveStartWith(prefix + entities); 13 }
这里的产生单复数的代码是从EntityFramework里借来的,具体请查看EnglishPluralizationService。
如果有不正确的地方,欢迎批评指正!