最新消息:欢迎各位技术大牛一起交流讨论,邮箱:gww0426@163.com

利用spring cache解耦业务中的缓存

Java技术 郭伟伟 3241浏览 0评论
虽然以前实现缓存的方式,是定义了缓存操作接口,可以灵活实现不同的缓存,可毕竟精力有限,要完成不同的缓存实现也是件麻烦的事。更要命的是,业务代码中有大量缓存操作的代码,耦合度太高,看着很不优雅。
所以呢,抽空了解了一下其它实现方案。这不,spring3.1开始,支持基于注解的缓存,算是目前我比较可以接受的一种方案吧。学完之后还是做一下笔记吧。

spring cache是一套基于注解实现的缓存技术,其本身是并不是具体实现,不过默认实现了ConcurrentMap和EHCache实现的缓存。当然也是支持其它缓存的。
spring cache有哪些特性:
1.通过少量的配置 annotation 注解即可使得既有代码支持缓存 (非常节省开发时间)
2.支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件即可使用缓存(位于spring-context包中,spring web项目都会引用这个包)
3.支持 Spring Express Language,能使用对象的任何属性或者方法来定义缓存的 key 和 condition (支持SpEL语法)
4.支持 AspectJ,并通过其实现任何方法的缓存支持 (默认基于AOP方案,采用AspectJ会更灵活,下文有介绍)
5.支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性(如果SpEL达不到你的预期,勇敢实现自己的KeyGenerator吧)
6.支持各种缓存实现,默认是基于ConcurrentMap实现的ConcurrentMapCache,同时支持ehcache实现。若要使用redis等缓存,引入redis的实现包即可。
有哪些遗憾呢?在我考虑的场景下,还有下面的一些遗憾:
1.不支持TTL,也就是不能设置expires time。这点挺遗憾的,spring-cache认为这是各个cache实现自己去完成的事情,有方案但是只能设置统一的过期时间,这明显不够灵活。比如用户的抽奖次数、有效期等业务,当天有效,或者3天、一周有效,我们倾向于设置缓存有效期解决这个问题,而spring-cache却无法完成。
2.无法根据查询结果中的内容生成缓存key,比如getUser(uid)方法,想通过查询出来的user.email生成缓存key就无法实现了。
3.调试起来麻烦

先贴一段以前的代码,看看以前是怎么做的:
 /**
     * 先取cache。如果没有,从DB取,再存cache
     */
    @Override
    public UserDetail getUserDetail(int userId) {
        Result<String> info = cacheManager.get(UCConstants.nameSpace,
                UCUtil.getKey(UCConstants.USER_DETAIL_UID_KEY, userId));
        if (StringUtils.isNotEmpty(info.getEntity())) {
            UserDetail userDetail = JSONUtils.fromJSON(info.getEntity(), UserDetail.class);
            if (null != userDetail) {
                return userDetail;
            }
        }
        UserDetail userDetail = userDetailDAO.getUserDetail(userId);
        if (userDetail != null) {
            if (logger.isDebugEnabled()) {
                logger.info("getUserDetail from db userDetail=" + userDetail.toString());
            }
            addToCache(userDetail);
            return userDetail;
        }
        return null;
    }
     private void addToCache(UserDetail userDetail) {
        cacheManager.put(UCConstants.nameSpace,
                UCUtil.getKey(UCConstants.USER_DETAIL_UID_KEY, userDetail.getId()),
                JSONUtils.toJSON(userDetail), UCConstants.USER_DETAIL_CACHE_TIME);
        //节约空间,存id吧。多查一次吧
        cacheManager.put(UCConstants.nameSpace,
                UCUtil.getKey(UCConstants.USER_DETAIL_NAME_KEY, userDetail.getUserNickName()),
                userDetail.getId() + "", UCConstants.USER_DETAIL_CACHE_TIME);
    }
那采用注解驱动的spring-cache之后代码怎么写的:
@Cacheable(key="#uid", value = "userCache")
     public UserDetail getUserDetail(int uid){
           return userDetailMapper.getUser(uid);
     }
当然,以前的代码写了两个缓存key,这点通过@Caching注解也是可以实现的,示例如下:
@Caching
     (evict={@CacheEvict(value="userCache",key="#user.uid"),
                @CacheEvict(value="userCache",key="#user.email")})

看了以上的代码,觉得用注解驱动的cache是不是很过瘾?代码简介,低耦合。简直是广大程序猿的福音。要像上面那样写代码,其实也挺容易的。下面就从头开始吧。

来,先把配置弄起。spring这点就很不爽,什么都要弄个配置。不过也正是基于配置+注解的方式,使得我们脱离了不断去new对象的苦海。
我的Cache是不打算使用ConcurrentMapCache的,所以我就直接拿EHCache来做示例好了。在使用EHCache之前,我们需要引入ehcache的包,以及配置ehcache.xml文件。
当然,在线上生产环境中,还是不建议只用ehcache这种方式。可以采用ehcache+redis,或者redis的方案都可以。
Maven的pom.xml文件配置:
<dependency>
     <groupId>net.sf.ehcache</groupId>
     <artifactId>ehcache-core</artifactId>
     <version>2.6.9</version>
</dependency>
ehcache的配置,位于classpath下的ehcache.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false">
    <diskStore path="D:/cache" /> <!-- 缓存存放目录(此目录为放入系统默认缓存目录),也可以是”D:/cache“ java.io.tmpdir -->
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="true"
            maxElementsOnDisk="10000000"
            diskPersistent="true"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"
            />
    <cache name="userCache"
           maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="true"
            maxElementsOnDisk="10000000"
            diskPersistent="true"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"
            />
</ehcache>
这里说明一下,为什么两个cache呢,用一个不好吗?
我最早的时候是一个defaultCache,没有userCache。后来发现spring-cache配置的EhCacheCacheManager总是load失败,报错误:
loadCaches must not return an empty Collection
我很纳闷,这不是有一个defaultCache吗?于是我跟踪代码,在AbstractCacheManager源码的afterPropertiesSet方法中有如下代码:
public void afterPropertiesSet() {
           Collection<? extends Cache> caches = loadCaches();
           Assert.notEmpty(caches, "loadCaches must not return an empty Collection");
           this.cacheMap.clear();
           // preserve the initial order of the cache names
           for (Cache cache : caches) {
                this.cacheMap.put(cache.getName(), cache);
                this.cacheNames.add(cache.getName());
           }
     }
这里取到的caches居然为empty!确实我也不知道为什么会这样。我当时尝试增加了一个名为default的cache配置,结果ehcache又报错,提示已经有名为default的cache了。于是将name改为userCache,问题解决。
从ehcache的报错来看,ehcache应该配置了一个名为default的cache,但不知道为什么spring-cache认不到。知道的同学可以告诉下我。

配置好ehcache后,就该配置我们的spring-cache了,为了方便管理,我倾向于配置独立的spring-cache.xml文件放在spring的配置目录下。内容如下:
<beans xmlns="http://www.springframework.org/schema/beans"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cache="http://www.springframework.org/schema/cache"
     xmlns:p="http://www.springframework.org/schema/p"
     xsi:schemaLocation="http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans.xsd
     http://www.springframework.org/schema/cache
     http://www.springframework.org/schema/cache/spring-cache.xsd">
     <cache:annotation-driven cache-manager="ehCacheManager"/>
     <!--  缓存  属性-->
    <bean id="ehCacheManagerFactory" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
        <property name="configLocation"  value="classpath:ehcache.xml"/>
    </bean>
     <!-- generic cache manager -->
     <bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
         <property name="cacheManager"  ref="ehCacheManagerFactory"/>
     </bean>
</beans>
当然,如果你想在没有缓存的环境中不做任何代码上的修改(比如环境迁移、临时测试等),即可简单的切换,那也是OK的。
又或者,你的环境中既有ehcache,又有redis,还有ConcurrentMapCache,那也是可以的。
上面说到的亮点,你可以用CompositeCacheManager完成。
需要重新配置以下的cacheManager:
<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
<property name="cacheManagers">
        <list>
            <ref bean="ehCacheManager"/>
            <ref bean="otherCachaManager"/>
        </list>
</property>
    <property name="fallbackToNoOpCache" value="true"/>
</bean>
fallbackToNoOpCache参数决定了在没有Cachhe的情况下会出现什么现象。如果为true,则会直接忽略掉缓存,可能进入db查询;如果为false(默认为false),则在无缓存时会抛出异常:
Cannot find cache named [userCache] for CacheableOperation

配置好了,那么该干正事了。spring-cache的使用非常简单,只会用简单的几个注解即可。那么,有哪些注解呢?看看下表:
Spring Cache配置
JSR-107规范
描述
@Cacheable
@CacheResult
缓存方法返回的结果,有三个参数,分别是value(缓存名称)、key(缓存key)、condition(缓存条件)
@CachePut
@CachePut
缓存方法返回的结果,并且会在方法被调用的时候执行。参数同Cacheable
@CacheEvict
@CacheRemove
清除缓存。有五个参数,除过上面的3个外,还有2个:allEntries(是否清除所有缓存)、beforeInvocation(是否在方法调用前就清除,默认为false,因此当方法抛出异常则缓存不会被清掉)
@CacheEvict(allEntries=true)
@CacheRemoveAll
清除所有缓存
@CacheConfig
@CacheDefaults
在类级别上提供一些公共配置,比如value值,每个方法都一样,就只需要在class上配置一次就好了,这个属性很有用,可惜spring3.1不支持。
简单解释一下,上面的表示官方文档中的内容。左边是spring3.1关于Cache操作的注解;中间是JSR-107规范的注解,spring在4.1版本实现;右边是解释。我就懒得翻译了。
各个注解的作用与配置方法也有作者写的不错,我就直接拿来用了:

@Cacheable、@CachePut、@CacheEvict 注释介绍

通过上面的例子,我们可以看到 spring cache 主要使用两个注释标签,即 @Cacheable、@CachePut 和 @CacheEvict,我们总结一下其作用和配置方法。

表 1. @Cacheable 作用和配置方法
@Cacheable  的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
@Cacheable 主要的参数
value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:
@Cacheable(value=”mycache”) 或者
@Cacheable(value={”cache1”,”cache2”}
key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:
@Cacheable(value=”testcache”,key=”#userName”)
condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 例如:
@Cacheable(value=”testcache”,condition=”#userName.length()>2”)
表 2. @CachePut 作用和配置方法
@CachePut 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用
@CachePut 主要的参数
value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:
@Cacheable(value=”mycache”) 或者
@Cacheable(value={”cache1”,”cache2”}
key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:
@Cacheable(value=”testcache”,key=”#userName”)
condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 例如:
@Cacheable(value=”testcache”,condition=”#userName.length()>2”)
表 3. @CacheEvict 作用和配置方法
@CachEvict 的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空
@CacheEvict 主要的参数
value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:
@CachEvict(value=”mycache”) 或者
@CachEvict(value={”cache1”,”cache2”}
key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:
@CachEvict(value=”testcache”,key=”#userName”)
condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才清空缓存 例如:
@CachEvict(value=”testcache”,
condition=”#userName.length()>2”)
allEntries 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 例如:
@CachEvict(value=”testcache”,allEntries=true)
beforeInvocation 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 例如:

@CachEvict(value=”testcache”,beforeInvocation=true)

关于上面的注解作用与含义,没有多少再补充解释的了,原作者的文章介绍的也很详细。这里补充一下spring-cache的cache配置。cache元素配置除过cache-manager属性外,还有许多属性,简单罗列如下:
XML属性
注解属性
默认值
含义
cache-manager
cacheManager
默认的cacheManager名称。一个默认的CacheResolver在cacheManager的后台被初始化,要想更精细的管理缓存可以考虑设置cache-resolver属性
cache-resolver
SimpleCacheResolver
CacheResolver的bean名称,这个非必需属性,仅仅作为cache-manager属性的替代
key-generator
SimpleKeyGenerator
自定义的 key generator
error-handler
SimpleCacheErrorHandler
自定义的Cache Error handler,默认情况下,异常会直接抛出给客户端
mode
mode
proxy
spring cache默认使用了spring的AOP框架来通过proxy的方式处理注解,另一种可代替的方式就是aspectj。通过Spring AOP方式的cache注解,无法在内部调用的时候被proxy,因此也就在内部调用的时候缓存注解会失效,而aspectj的AOP则可以解决这个问题。
proxy-target-class
proxyTargetClass
false
仅适用于proxy模式,控制类上的注解@Cacheable或@CacheEvict采用哪种缓存代理。如果proxy-target-class属性设置为true,那么将创建基于类的代理。 如果proxy-target-class为false,那么将创建基于标准JDK接口的代理。具体可以参考AOP的proxy-target-class属性
order
order
Ordered.LOWEST_PRECEDENCE
确定bean注解中@Cacheable或@CacheEvict中cache advice的顺序,没有指定则意味着使用AOP决定的advice顺序。具体可以参考AOP的advice顺序规则。
参考资料:http://www.ibm.com/developerworks/cn/opensource/os-cn-spring-cache/
参考资料:http://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html

转载请注明:郭伟伟@互联网 » 利用spring cache解耦业务中的缓存


发表我的评论
取消评论

使用新浪微博登陆

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址