过滤与缓存
刚说要坚持写博客,这一晃两周过去了...汗啊,今天刚上新版服务,忙里偷闲总结下最近的工作,继续谈谈对缓存的利用吧。
先推荐篇博文,在cnblog上看了篇文章写的不错,关于cache和db之间的关系以及怎么用好两者《Memcache和Mongodb》
http://www.cnblogs.com/lovecindywang/archive/2010/05/19/1739025.html。
这一周大部分工作还是投入在优化缓存设计上面,之前两篇博客分别介绍了我之前对cache的设计由最初的模糊的服务器代理进行分层缓存改进到本地缓存+服务器代理进行分页缓存,但是一直都遗留下一个问题——过滤。
例如对于一个数据源A,Web客户端要能对全量的A进行处理,而Android端只能解析A中一部分数据B,同样IOS端能解析的部分为C,这种情况如何处理?一种方式是写三条查询语句在DAO层控制数据差异,过滤动作由跨表查询代替。当然到这一步对开发在性能和难度上都是可以接受的,然而如果客户端版本迭代,假设Android端的不同版本支持的数据类型也有了差异,老版本支持的数据集为D,新版则支持E,那问题出现了如果还是利用纯sql来处理的话,客户端差异*版本差异将会使DAO层变的更复杂,逻辑层版本和客户端控制就更不必说了。
对于这一问题,我们最终采用了折中的一种方案,客户端差异的区分由SQL跨表查询解决,对于版本迭代差异则通过过滤器来完成。如何实现过滤器以及如何结合缓存,这一问题还是困扰了我一段时间的,下面谈谈我的思路。
之前结合了Attribute和静态解析做了一套缓存代理工具,通过在DAO层接口简单的使用Attribute声明、服务器静态解析利用IL运行期生成代理函数来完成缓存代理(详见上一篇博客http://gkqcz-126-com.iteye.com/admin/blogs/1717882)。这样的设计首要的原则是代理函数不要有任何侵入性,确保代理函数的签名与被代理的函数一致,也正是因为这个原因让我在引入过滤操作时有了顾虑,如果直接在原接口上引入过滤会不会导致接口被污染?起初我的想法是在DAO层之上的Facade层进行过滤操作,Facade层负责过滤逻辑以及结果组装,而DAO负责从DB中获取数据,分别进行cache,对于需要取过滤数据的则从Facade层取,取全量数据则由DAO层取。不过这样却认为的将同样的取数据操作变成了两个层级,甚至两个层级间缓存也产生了依赖(这一点很糟糕),最后我不得不改变了原来的思路,定义了空过滤修改了原接口。
这是利用缓存工具以及结合过滤生成出代理函数的伪代码(这个示例使用了本地缓存、分页缓存以及过滤):
//被代理的函数 public virtual ListObject<DemoEntity> GetDemoEntity(string condition ,int pageIndex ,int pageSize ,Filter filter) { ListObject<DemoEntity> res=DemoEntityDAO.GetDemoEntity(condition)//查询数据库 FillFilterResult()//进行过滤并填充结果,直至满足请求的数据量进行返回, //对与无需过滤的情况,我定义了一个名为NullFilter的过滤器,返回结果恒为true; //取出请求部分的数据并返回 } //动态生成的代理函数 public ListObject<DemoEntity> GetDemoEntity(string condition ,int pageIndex ,int pageSize ,Filter filter) { //根据入参的pageIndex、pageSize,计算其对应的缓存所对应的pageIndex/pageSize并进行替换, //例如假设服务器缓存配置为cache10页每页大小100,若请求pageIndex=3、pageSize=40,则计算后pageIndex=1(对应缓存的起始页),pageSize=100 if (..)//客户端请求的数据范围在缓存范围内 { do{ ListObject<DemoEntity> result; string key=string.concat(new Object{..})//根据函数入参以及过滤器的ID生成出的Cachekey result=cacheManager.GetLocal(key);//查询本地缓存,这类缓存时间设置不要过程,过长会导致各服务器数据不一致 if(result==null) { result=cacheManager.Get(key);//当本地缓存失效时,查询memcache if(result==null) { result=base.GetDemoEntity(condition ,pageIndex ,pageSize ,Filter)//缓存失效,进行数据库查询 if(result!=null) cacheManager.Set(..);//重新写memcache缓存 } if(result!=null) { cacheManager.SetLocal(..)//重新写本地缓存 } } }while(..)//如果客户端请求跨页则获取下一缓存页内容,并填充结果 PageFormatUtil.Format(..);//这里取得的数据都是服务器进行代理后的,最后需要取出真正请求的那一部分数据 } else return base.GetDemoEntity(...)//这类访问比较少,缓存带来的内存开销不如直接查询数据库 }
动态代码其实只是通过了继承完成了对原函数的代理,完成了对调用者的隐藏(当然写到这都只是我个人的一点小思考和实现,要是大家有更好的设计和实现方式望多多指点啊)。到这里只是完成了对工具构造,今天就先写到这里了,下一篇博客谈谈如何使用过滤器进行版本控制以及另外我们采用的一些小技巧。
PS:再附上一点使用ILEmit的一些容易犯得小错误
1.值类型数据需要通过box才能转会为对象类型,例如newobject[]{"str",1},在用IL代码实现时不要忘记先对1进行装箱,同样的在取出去取出值对象的时候也要unbox一下,例如缓存命中返回int类型值时先对返回值unbox后再返回。
2.枚举类型本质上也是值类型哦,这个也要进行box(这个好像是因为枚举值每个对应了从0开始的整数,就是我们定义时候赋的int值)。
欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。