• 强大的Spring缓存技术(下)



    基本原理


    一句话介绍就是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





  • 相关阅读:
    为什么你需要升级 pip
    将py文件封装为exe
    Kafka原理分析之基础篇
    顶象验证码破解与研究
    优秀学员学习总结分享(三)
    优秀学员学习总结分享(二)
    ES6数组的扩展--Array.from()和Array.of()
    ES6核心特性
    优秀学员学习总结分享(一)
    JavaScript常见的六种继承方式
  • 原文地址:https://www.cnblogs.com/zhangboyu/p/7448911.html
Copyright © 2020-2023  润新知