强大的Spring缓存技术(下)

来源:互联网 发布:佳能1880清零软件 编辑:程序博客网 时间:2024/06/05 06:25

基本原理

一句话介绍就是spring AOP的动态代理技术。 如果读者对Spring AOP不熟悉的话,可以去看看官方文档

扩展性

直到现在,我们已经学会了如何使用开箱即用的 spring cache,这基本能够满足一般应用对缓存的需求。

但现实总是很复杂,当你的用户量上去或者性能跟不上,总需要进行扩展,这个时候你或许对其提供的内存缓存不满意了,因为其不支持高可用性,也不具备持久化数据能力,这个时候,你就需要自定义你的缓存方案了。

还好,spring 也想到了这一点。我们先不考虑如何持久化缓存,毕竟这种第三方的实现方案很多。

我们要考虑的是,怎么利用 spring 提供的扩展点实现我们自己的缓存,且在不改原来已有代码的情况下进行扩展。

首先,我们需要提供一个 CacheManager 接口的实现,这个接口告诉 spring 有哪些 cache 实例,spring 会根据 cache 的名字查找 cache 的实例。另外还需要自己实现 Cache 接口,Cache 接口负责实际的缓存逻辑,例如增加键值对、存储、查询和清空等。

利用 Cache 接口,我们可以对接任何第三方的缓存系统,例如 EHCache、OSCache,甚至一些内存数据库例如 memcache 或者Redis 等。下面我举一个简单的例子说明如何做。

[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. import java.util.Collection;   
  2.  import org.springframework.cache.support.AbstractCacheManager;   
  3.  public class MyCacheManager extends AbstractCacheManager {   
  4.    private Collection<? extends MyCache> caches;   
  5.    /**  
  6.    * Specify the collection of Cache instances to use for this CacheManager.  
  7.    */  
  8.    public void setCaches(Collection<? extends MyCache> caches) {   
  9.      this.caches = caches;   
  10.    }   
  11.    @Override  
  12.    protected Collection<? extends MyCache> loadCaches() {   
  13.      return this.caches;   
  14.    }   
  15.  }  

上面的自定义的 CacheManager 实际继承了 spring 内置的 AbstractCacheManager,实际上仅仅管理 MyCache 类的实例。

下面是MyCache的定义:

[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. import java.util.HashMap;   
  2.  import java.util.Map;   
  3.  import org.springframework.cache.Cache;   
  4.  import org.springframework.cache.support.SimpleValueWrapper;   
  5.  public class MyCache implements Cache {   
  6.    private String name;   
  7.    private Map<String,Account> store = new HashMap<String,Account>();;   
  8.    public MyCache() {   
  9.    }   
  10.    public MyCache(String name) {   
  11.      this.name = name;   
  12.    }   
  13.    @Override  
  14.    public String getName() {   
  15.      return name;   
  16.    }   
  17.    public void setName(String name) {   
  18.      this.name = name;   
  19.    }   
  20.    @Override  
  21.    public Object getNativeCache() {   
  22.      return store;   
  23.    }   
  24.    @Override  
  25.    public ValueWrapper get(Object key) {   
  26.      ValueWrapper result = null;   
  27.      Account thevalue = store.get(key);   
  28.      if(thevalue!=null) {   
  29.        thevalue.setPassword("from mycache:"+name);   
  30.        result = new SimpleValueWrapper(thevalue);   
  31.      }   
  32.      return result;   
  33.    }   
  34.    @Override  
  35.    public void put(Object key, Object value) {   
  36.      Account thevalue = (Account)value;   
  37.      store.put((String)key, thevalue);   
  38.    }   
  39.    @Override  
  40.    public void evict(Object key) {   
  41.    }   
  42.    @Override  
  43.    public void clear() {   
  44.    }   
  45.  }  

上面的自定义缓存只实现了很简单的逻辑,但这是我们自己做的,也很令人激动是不是,主要看 get 和 put 方法,其中的 get 方法留了一个后门,即所有的从缓存查询返回的对象都将其 password 字段设置为一个特殊的值,这样我们等下就能演示“我们的缓存确实在起作用!”了。

这还不够,spring 还不知道我们写了这些东西,需要通过 spring*.xml 配置文件告诉它

[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. <cache:annotation-driven />   
  2. <bean id="cacheManager" class="com.rollenholt.spring.cache.MyCacheManager">  
  3.     <property name="caches">   
  4.       <set>   
  5.         <bean  
  6.           class="com.rollenholt.spring.cache.MyCache"  
  7.           p:name="accountCache" />   
  8.       </set>   
  9.     </property>   
  10.   </bean>  

接下来我们来编写测试代码:

[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. Account account = accountService.getAccountByName("someone");   
  2. logger.info("passwd={}", account.getPassword());   
  3. account = accountService.getAccountByName("someone");   
  4. logger.info("passwd={}", account.getPassword());  

上面的测试代码主要是先调用 getAccountByName 进行一次查询,这会调用数据库查询,然后缓存到 mycache 中,然后我打印密码,应该是空的;下面我再次查询 someone 的账号,这个时候会从 mycache 中返回缓存的实例,记得上面的后门么?我们修改了密码,所以这个时候打印的密码应该是一个特殊的值

注意和限制

基于 proxy 的 spring aop 带来的内部调用问题

上面介绍过 spring cache 的原理,即它是基于动态生成的 proxy 代理机制来对方法的调用进行切面,这里关键点是对象的引用问题.

如果对象的方法是内部调用(即 this 引用)而不是外部引用,则会导致 proxy 失效,那么我们的切面就失效,也就是说上面定义的各种注释包括 @Cacheable、@CachePut 和 @CacheEvict 都会失效,我们来演示一下。

[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. public Account getAccountByName2(String accountName) {   
  2.    return this.getAccountByName(accountName);   
  3.  }   
  4.  @Cacheable(value="accountCache")// 使用了一个缓存名叫 accountCache   
  5.  public Account getAccountByName(String accountName) {   
  6.    // 方法内部实现不考虑缓存逻辑,直接实现业务  
  7.    return getFromDB(accountName);   
  8.  }  

上面我们定义了一个新的方法 getAccountByName2,其自身调用了 getAccountByName 方法,这个时候,发生的是内部调用(this),所以没有走 proxy,导致 spring cache 失效

要避免这个问题,就是要避免对缓存方法的内部调用,或者避免使用基于 proxy 的 AOP 模式,可以使用基于 aspectJ 的 AOP 模式来解决这个问题。

@CacheEvict 的可靠性问题

我们看到,@CacheEvict 注释有一个属性 beforeInvocation,缺省为 false,即缺省情况下,都是在实际的方法执行完成后,才对缓存进行清空操作。期间如果执行方法出现异常,则会导致缓存清空不被执行。我们演示一下

[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. // 清空 accountCache 缓存  
  2.  @CacheEvict(value="accountCache",allEntries=true)  
  3.  public void reload() {   
  4.    throw new RuntimeException();   
  5.  }  

我们的测试代码如下:

[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. accountService.getAccountByName("someone");   
  2. accountService.getAccountByName("someone");   
  3. try {   
  4.   accountService.reload();   
  5. catch (Exception e) {   
  6.  //...  
  7. }   
  8. accountService.getAccountByName("someone");  

注意上面的代码,我们在 reload 的时候抛出了运行期异常,这会导致清空缓存失败。上面的测试代码先查询了两次,然后 reload,然后再查询一次,结果应该是只有第一次查询走了数据库,其他两次查询都从缓存,第三次也走缓存因为 reload 失败了。

那么我们如何避免这个问题呢?我们可以用 @CacheEvict 注释提供的 beforeInvocation 属性,将其设置为 true,这样,在方法执行前我们的缓存就被清空了。可以确保缓存被清空。

非 public 方法问题

和内部调用问题类似,非 public 方法如果想实现基于注释的缓存,必须采用基于 AspectJ 的 AOP 机制

Dummy CacheManager 的配置和作用

有的时候,我们在代码迁移、调试或者部署的时候,恰好没有 cache 容器,比如 memcache 还不具备条件,h2db 还没有装好等,如果这个时候你想调试代码,岂不是要疯掉?这里有一个办法,在不具备缓存条件的时候,在不改代码的情况下,禁用缓存。

方法就是修改 spring*.xml 配置文件,设置一个找不到缓存就不做任何操作的标志位,如下

[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. <cache:annotation-driven />   
  2. <bean id="simpleCacheManager" class="org.springframework.cache.support.SimpleCacheManager">   
  3.   <property name="caches">   
  4.     <set>   
  5.       <bean  
  6.         class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"  
  7.         p:name="default" />   
  8.     </set>   
  9.   </property>   
  10. </bean>   
  11. <bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">  
  12.   <property name="cacheManagers">   
  13.     <list>   
  14.       <ref bean="simpleCacheManager" />   
  15.     </list>   
  16.   </property>   
  17.   <property name="fallbackToNoOpCache" value="true" />   
  18. </bean>  

注意以前的 cacheManager 变为了 simpleCacheManager,且没有配置 accountCache 实例,后面的 cacheManager 的实例是一个 CompositeCacheManager,他利用了前面的 simpleCacheManager 进行查询,如果查询不到,则根据标志位 fallbackToNoOpCache 来判断是否不做任何缓存操作。

使用 guava cache

[java] view plain copy
print?在CODE上查看代码片派生到我的代码片
  1. <bean id="cacheManager" class="org.springframework.cache.guava.GuavaCacheManager">  
  2.     <property name="cacheSpecification" value="concurrencyLevel=4,expireAfterAccess=100s,expireAfterWrite=100s" />  
  3.     <property name="cacheNames">  
  4.         <list>  
  5.             <value>dictTableCache</value>  
  6.         </list>  
  7.     </property>  
  8. </bean>  

代码地址:

https://github.com/rollenholt/spring-cache-example
转载自:http://blog.csdn.net/a494303877/article/details/53780675


0 0
原创粉丝点击