基本原理


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


扩展性


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


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


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


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


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


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


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的定义:


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 配置文件告诉它


<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>


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


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 都会失效,我们来演示一下。


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,即缺省情况下,都是在实际的方法执行完成后,才对缓存进行清空操作。期间如果执行方法出现异常,则会导致缓存清空不被执行。我们演示一下


// 清空 accountCache 缓存

 @CacheEvict(value="accountCache",allEntries=true)

 public void reload() { 

   throw new RuntimeException(); 

 }


我们的测试代码如下:


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 配置文件,设置一个找不到缓存就不做任何操作的标志位,如下


<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


<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