强大的Spring缓存技术(下)

基本原理

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

扩展性

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

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

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

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

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

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

  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.  }  
import java.util.Collection; 
 import org.springframework.cache.support.AbstractCacheManager; 
 public class MyCacheManager extends AbstractCacheManager { 
   private Collection<? extends MyCache> caches; 
   /** 
   * Specify the collection of Cache instances to use for this CacheManager. 
   */
   public void setCaches(Collection<? extends MyCache> caches) { 
     this.caches = caches; 
   } 
   @Override
   protected Collection<? extends MyCache> loadCaches() { 
     return this.caches; 
   } 
 }

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

下面是MyCache的定义:

  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.  }  
import java.util.HashMap; 
 import java.util.Map; 
 import org.springframework.cache.Cache; 
 import org.springframework.cache.support.SimpleValueWrapper; 
 public class MyCache implements Cache { 
   private String name; 
   private Map<String,Account> store = new HashMap<String,Account>();; 
   public MyCache() { 
   } 
   public MyCache(String name) { 
     this.name = name; 
   } 
   @Override
   public String getName() { 
     return name; 
   } 
   public void setName(String name) { 
     this.name = name; 
   } 
   @Override
   public Object getNativeCache() { 
     return store; 
   } 
   @Override
   public ValueWrapper get(Object key) { 
     ValueWrapper result = null; 
     Account thevalue = store.get(key); 
     if(thevalue!=null) { 
       thevalue.setPassword("from mycache:"+name); 
       result = new SimpleValueWrapper(thevalue); 
     } 
     return result; 
   } 
   @Override
   public void put(Object key, Object value) { 
     Account thevalue = (Account)value; 
     store.put((String)key, thevalue); 
   } 
   @Override
   public void evict(Object key) { 
   } 
   @Override
   public void clear() { 
   } 
 }

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

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

  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>  
<cache:annotation-driven /> 
<bean id="cacheManager" class="com.rollenholt.spring.cache.MyCacheManager">
    <property name="caches"> 
      <set> 
        <bean
          class="com.rollenholt.spring.cache.MyCache"
          p:name="accountCache" /> 
      </set> 
    </property> 
  </bean>

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

  1. Account account = accountService.getAccountByName("someone");   
  2. logger.info("passwd={}", account.getPassword());   
  3. account = accountService.getAccountByName("someone");   
  4. logger.info("passwd={}", account.getPassword());  
Account account = accountService.getAccountByName("someone"); 
logger.info("passwd={}", account.getPassword()); 
account = accountService.getAccountByName("someone"); 
logger.info("passwd={}", account.getPassword());

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

注意和限制

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

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

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

  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.  }  
public Account getAccountByName2(String accountName) { 
   return this.getAccountByName(accountName); 
 } 
 @Cacheable(value="accountCache")// 使用了一个缓存名叫 accountCache 
 public Account getAccountByName(String accountName) { 
   // 方法内部实现不考虑缓存逻辑,直接实现业务
   return getFromDB(accountName); 
 }

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

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

@CacheEvict 的可靠性问题

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

  1. // 清空 accountCache 缓存  
  2.  @CacheEvict(value="accountCache",allEntries=true)  
  3.  public void reload() {   
  4.    throw new RuntimeException();   
  5.  }  
// 清空 accountCache 缓存
 @CacheEvict(value="accountCache",allEntries=true)
 public void reload() { 
   throw new RuntimeException(); 
 }

我们的测试代码如下:

  1. accountService.getAccountByName("someone");   
  2. accountService.getAccountByName("someone");   
  3. try {   
  4.   accountService.reload();   
  5. catch (Exception e) {   
  6.  //...  
  7. }   
  8. accountService.getAccountByName("someone");  
accountService.getAccountByName("someone"); 
accountService.getAccountByName("someone"); 
try { 
  accountService.reload(); 
} catch (Exception e) { 
 //...
} 
accountService.getAccountByName("someone");

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

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

非 public 方法问题

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

Dummy CacheManager 的配置和作用

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

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

  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>  
<cache:annotation-driven /> 
<bean id="simpleCacheManager" class="org.springframework.cache.support.SimpleCacheManager"> 
  <property name="caches"> 
    <set> 
      <bean
        class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
        p:name="default" /> 
    </set> 
  </property> 
</bean> 
<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
  <property name="cacheManagers"> 
    <list> 
      <ref bean="simpleCacheManager" /> 
    </list> 
  </property> 
  <property name="fallbackToNoOpCache" value="true" /> 
</bean>

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

使用 guava cache

  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>  
<bean id="cacheManager" class="org.springframework.cache.guava.GuavaCacheManager">
    <property name="cacheSpecification" value="concurrencyLevel=4,expireAfterAccess=100s,expireAfterWrite=100s" />
    <property name="cacheNames">
        <list>
            <value>dictTableCache</value>
        </list>
    </property>
</bean>

代码地址:

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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值