缓存框架之AOP渐进实现
案例描述
系统中常常存在这样的一些逻辑:某些业务方法的执行结果在一段时间内比较稳定,不太容易发生变化,这些执行的结果可以借助缓存框架,通过一些细粒度的缓存过期策略,存放于缓存中,再次执行这些业务方法时可以直接到缓存中命中,从而降低系统开销,提高效率。
?
解决方案之一:第一小步
首先,我们需要一套缓存体系,缓存的过期策略大致有如下几类:
(1)EternalPolicy:永不过期策略
(2)IdlePolicy:空闲过期策略
(3)LivePolicy:
(4)IdleAndLivePolicy:
于是我们抽取出的Cache接口会像下面这样:
?
?其中CachePolicySource是一个缓存过期策略源,负责管理业务方法的过期策略。有了这个拦截器之后就可以借助
Spring的Aop能力提供业务层的缓存能力了,Spring配置文件如下:
?
至此,业务层的缓存能力在AOP的强大力量下,以一种比较优雅的方式得到了完美的解决。咖啡有点冷了,去加点热水……
?
解决方案之三:Annotation篇
?一个优秀的程序员是不能满足于仅仅能用的基础上,还得充分考虑到易用性。拦截篇中提供的配置方式始终有点复杂的感觉,如果能提供Annotation配置的话,易用性就得到了极大的提高。
?
?于是我们的业务层就可以通过如下配置得到缓存的能力:
?
synchronized(lockSource.getLock(key)){
Element element = getFromCache(key);
if(element == null){
element = new Element(key);
putInCache(element);
}
return element;
}
}
public void put(Element element){
synchronized(lockSource.getLock(element.key())){
Element cacheElement = getFromCache(key);
if(cacheElement == null){
putInCache(element);
}else{
if(element.version() >= cacheElement.version()){
putInCache(element);
}else{
throw RefreshException();
}
}
}
}
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
........
Element element = get(cacheKey);
if(element.value() == null){
element.value(methodInvocation.proceed());
try{
put(element);
}catch(RefreshException e){
//donothing
}
}
return element.value();
}</pre>
<p>?至于缓存对象的克隆保护,因为我目前需要一种自动Flush的机制,所以没有提供unmodifed这样的实现。至于其它的数据不太稳定的情况不太适合扔进缓存。其实看了这么多,我还真的没有发现需要非只读缓存的实际场景,大家可以一起探讨一下,有这样的实际场景吗</p>
<p>?</p> 21 楼 whitesock 2010-07-21 潜在问题1:
一旦methodInvocation.proceed()的返回值为null,那么基于element.value() == null便不够健壮。
潜在问题2:
element.value(methodInvocation.proceed()):对value的赋值存在潜在的可见性问题。
潜在问题3:
1 invoke方法中,两个不同的线程以相同的cacheKey调用get(cacheKey)。假设两个线程都得到相同的element引用,并且element.value() 为null。
2 两个线程可能同时调用methodInvocation.proceed()。
3 假设A线程调用methodInvocation.proceed(),返会a;B线程调用methodInvocation.proceed(),返会b;
4 最终不能确定放入缓存到底是a或b。如果最终放入的是a,而b是最新版本的数据,那么导致后续操作得到不正确的值。
建议:
1 看看concurrent包
2 参考一下spring-modules-cache
平常很少在一个帖子上花这么多时间,这么做的原因是笔者曾开发过一个分布式的非只读缓存,深知其深浅。我一再强调缓存的正确性,大概是因为一直从事金融相关的项目(信贷和FX,特别是FX,一个微小的错误都可能造成上千万的损失)的原因。 在这里指出楼主的不足,是因为楼主目前的实现还有long way to go,可以毫不夸张地讲,楼主贴的代码越多,我就会发现越多的问题,但楼主却总是有藉口和理由,我也就没必要再浪费时间了。 22 楼 gigi_112 2010-08-12 whitesock 写道潜在问题1:
一旦methodInvocation.proceed()的返回值为null,那么基于element.value() == null便不够健壮。
潜在问题2:
element.value(methodInvocation.proceed()):对value的赋值存在潜在的可见性问题。
潜在问题3:
1 invoke方法中,两个不同的线程以相同的cacheKey调用get(cacheKey)。假设两个线程都得到相同的element引用,并且element.value() 为null。
2 两个线程可能同时调用methodInvocation.proceed()。
3 假设A线程调用methodInvocation.proceed(),返会a;B线程调用methodInvocation.proceed(),返会b;
4 最终不能确定放入缓存到底是a或b。如果最终放入的是a,而b是最新版本的数据,那么导致后续操作得到不正确的值。
建议:
1 看看concurrent包
2 参考一下spring-modules-cache
平常很少在一个帖子上花这么多时间,这么做的原因是笔者曾开发过一个分布式的非只读缓存,深知其深浅。我一再强调缓存的正确性,大概是因为一直从事金融相关的项目(信贷和FX,特别是FX,一个微小的错误都可能造成上千万的损失)的原因。 在这里指出楼主的不足,是因为楼主目前的实现还有long way to go,可以毫不夸张地讲,楼主贴的代码越多,我就会发现越多的问题,但楼主却总是有藉口和理由,我也就没必要再浪费时间了。
看了各位的讨论,感触很多,我们公司最近在用memcached做业务逻辑的非只读缓存,由于我们的项目是做的网站开发,对于有些业务的一致性强调不是很高,没有发现过数据一致性的问题。
目前,我和楼主的思想是一致的,把setcache,deletecache分别做成切面,切入到业务层中。
但是现在遇到一个问题,就是对于key值的选定,目前使用的是calss+parameters,但是这样造成的问题就是切面不够通用,每次在创建key的时候需要耦合到方法的参数中去,删除key的时候同样还需要对key的创建重新编写相关的逻辑。
不知道大家有兴趣讨论下memcached如何优雅的切入到代码中去么? 23 楼 tedeyang 2010-09-27 gigi_112 写道whitesock 写道潜在问题1:
一旦methodInvocation.proceed()的返回值为null,那么基于element.value() == null便不够健壮。
潜在问题2:
element.value(methodInvocation.proceed()):对value的赋值存在潜在的可见性问题。
潜在问题3:
1 invoke方法中,两个不同的线程以相同的cacheKey调用get(cacheKey)。假设两个线程都得到相同的element引用,并且element.value() 为null。
2 两个线程可能同时调用methodInvocation.proceed()。
3 假设A线程调用methodInvocation.proceed(),返会a;B线程调用methodInvocation.proceed(),返会b;
4 最终不能确定放入缓存到底是a或b。如果最终放入的是a,而b是最新版本的数据,那么导致后续操作得到不正确的值。
建议:
1 看看concurrent包
2 参考一下spring-modules-cache
平常很少在一个帖子上花这么多时间,这么做的原因是笔者曾开发过一个分布式的非只读缓存,深知其深浅。我一再强调缓存的正确性,大概是因为一直从事金融相关的项目(信贷和FX,特别是FX,一个微小的错误都可能造成上千万的损失)的原因。 在这里指出楼主的不足,是因为楼主目前的实现还有long way to go,可以毫不夸张地讲,楼主贴的代码越多,我就会发现越多的问题,但楼主却总是有藉口和理由,我也就没必要再浪费时间了。
看了各位的讨论,感触很多,我们公司最近在用memcached做业务逻辑的非只读缓存,由于我们的项目是做的网站开发,对于有些业务的一致性强调不是很高,没有发现过数据一致性的问题。
目前,我和楼主的思想是一致的,把setcache,deletecache分别做成切面,切入到业务层中。
但是现在遇到一个问题,就是对于key值的选定,目前使用的是calss+parameters,但是这样造成的问题就是切面不够通用,每次在创建key的时候需要耦合到方法的参数中去,删除key的时候同样还需要对key的创建重新编写相关的逻辑。
不知道大家有兴趣讨论下memcached如何优雅的切入到代码中去么?
是有这样的问题。
whitesock很关注缓存的正确性,确实在金融行业这是必须的。
而互联网行业一般不在乎,在我的应用场景里,根本不需要关系一些细微的数据不一致,后面的缓存值直接覆盖前面的,没问题,就这么干。
但是缓存的更新楼主没考虑到,基本还不具备实用价值。
你提出的key删除就是个棘手的事。
用AOP拦截,key的生成就取决于参数,通用的方式就是楼主做的,对query方法的Method,Args做信息摘要得到一个key。
但我有时候还需要拦截另一个delete方法来删除缓存,显然这个delete方法生成的key和query肯定不一样。
@CacheInput
String queryTopicByName(String name);
@CacheDelete
void deleteTopicById(long id)
两个参数风马牛不相及,后面的delete会影响query的结果,但这个关系是极其隐晦的,通过方法参数无法识别。
此路不通!
稳妥的方案是由调用者来决定key怎么生成,如此则AOP的意义不大。
这就是hibernate之所有有用的关键:它掌握着数据的逻辑,所以能妥善处理缓存问题。