Spring Cache原理与使用及ICOP平台中的缓存应用

来源:互联网 发布:php虚拟主机销售系统 编辑:程序博客网 时间:2024/06/16 09:58

一.Spring Cache概述

Spring3.1中提供了基于注解的缓存技术,他本身并不是缓存的具体实现,而是一个缓存的抽象。通过在既有程序的方法上添加spring cache的注解,可以达到缓存方法返回结果的效果。

 

二.Spring Cache的特点

1.通过使用少量注解,就可使既有代码支持缓存

2.支持开箱即用Out-of-the-box,即不用安装和部署第三方组件,就可以使用缓存

3.支持Spring Expression Language,能使用对象的任何属性和方法定义缓存的key和condition

4.支持AspectJ,并提供其实现任何方法的缓存支持

5.支持自定义key和缓存管理者,具有相当的灵活性和扩展性。

三.Spring Cache依赖的jar包


四.Spring Cache的使用方式

4.1.Spring 配置文件中配置支持基于注解的缓存

<?xmlversion="1.0"encoding="UTF-8"?>

<beansxmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:context="http://www.springframework.org/schema/context"xmlns:aop="http://www.springframework.org/schema/aop"

    xmlns:tx="http://www.springframework.org/schema/tx"xmlns:jpa="http://www.springframework.org/schema/data/jpa"

    xmlns:task="http://www.springframework.org/schema/task"

    xmlns:cache="http://www.springframework.org/schema/cache"

    xmlns:p="http://www.springframework.org/schema/p"

    xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans-4.0.xsd

        http://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context-4.0.xsd

        http://www.springframework.org/schema/txhttp://www.springframework.org/schema/tx/spring-tx-4.0.xsd

        http://www.springframework.org/schema/data/jpahttp://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd

        http://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop-4.0.xsd

        http://www.springframework.org/schema/task 

        http://www.springframework.org/schema/task/spring-task-3.0.xsd

       http://www.springframework.org/schema/cache

       http://www.springframework.org/schema/cache/spring-cache.xsd

        ">

    <description>Spring cache配置</description>

    <cache:annotation-driven/>

   <!-- generic cache manager -->

   <beanid="cacheManager"

     class="org.springframework.cache.support.SimpleCacheManager">

       <propertyname="caches">

         <set>

           <bean

             class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"

             p:name="default"/>        

           <bean

             class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"

             p:name="accountCache"/>

          </set>

       </property>

  </bean>  

  <beanid="accoutCacheService"class="com.cn.cache.service.AccoutCacheService"/>

</beans>

注意这个 spring配置文件有一个关键的支持缓存的配置项:<cache:annotation-driven/>这个配置项缺省使用了一个名字叫 cacheManager的缓存管理器,这个缓存管理器有一个 spring的缺省实现,即SimpleCacheManager它需要配置一个属性 caches,即此缓存管理器管理的缓存集合,除了缺省的名字叫 default 的缓存,我们还自定义了一个名字叫 accountCache的缓存,使用了缺省的内存存储方案 ConcurrentMapCacheFactoryBean,它是基于 java.util.concurrent.ConcurrentHashMap的一个内存缓存实现方案。

 

4.2. 使用注解实现缓存机制

4.2.1.@Cacheable-缓存支持,缓存存在则不在调用方法实现

在需要支持缓存的方法上增加一个注释@Cacheable(value=”缓存名称”),该缓存名称及其对应的缓存类在spring的配置文件中配置的缓存管理器中进行配置。这个注释的意思是,当调用这个方法的时候,会从一个指定名称的的缓存中查询,如果没有,则执行实际的方法(即查询数据库),并将执行的结果存入缓存中,否则返回缓存中的对象。这里的缓存中的value就是方法的返回值。


示例代码如下:

/**

 * 账户服务--支持缓存(应用Spring Cache

 * @authorshl

 *

 */

public classAccoutCacheService {

 

    @Cacheable(value="accountCache",condition="#username.length()<=4")//使用一个缓存名叫accountCache,只有username长度小于4才加入缓存

    public AccountgetAccountInfo(Stringusername) {

         System.out.println("real query account");

         return getFromDB(username);

     }

 

    @Cacheable(value="default")//使用一个缓存名叫accountCache

     public List getAccountList() {

         System.out.println("real query account");

         List list = new ArrayList();

         Account account1 = new Account("zyc");

         Account account2 = new Account("swm");

         list.add(account1);

         list.add(account2);

         returnlist;

     }

   

     private Account getFromDB(StringacctName) {

         System.out.println("real querying db..."+acctName);

         return new Account(acctName);

      }

}

@Cacheable(value=”accountCache”),这个注释的意思是,当调用这个方法的时候,会从一个名叫 accountCache的缓存中查询,如果没有,则执行实际的方法(即查询数据库),并将执行的结果存入缓存中,否则返回缓存中的对象。这里的缓存中的 key就是参数 userNamevalue就是 Account对象。“accountCache”缓存是在 spring*.xml中定义的名称。

 

4.2.2 @CachePut-既要保证方法被调用,又希望结果被缓存

@CachePut 的作用主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用。

有些情况下我们希望方法一定会被调用,因为其除了返回一个结果,还做了其他事情,例如记录日志,调用接口等,这个时候,我们可以用 @CachePut注释,这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中。


示例代码:

    @CachePut(value="accountCache",condition="#username.length()>=4")//使用一个缓存名叫accountCache,只有username长度小于4才加入缓存

    public AccountgetCachePutAccountInfo(Stringusername) {

         System.out.println("real query account");

         return getFromDB(username);

    }

 

4.2.3 @CacheEvict-根据一定的条件对缓存进行清空

spring cache 清空缓存的方法很简单,就是通过 @CacheEvict注释来标记要清空缓存的方法,当这个方法被调用后,即会清空缓存。


示例代码:

    @CachePut(value="accountCache",condition="#username.length()>=4")//使用一个缓存名叫accountCache,只有username长度小于4才加入缓存

    public AccountgetCachePutAccountInfo(Stringusername) {

         System.out.println("real query account");

         return getFromDB(username);

    }

 

4.2.3 @CacheEvict-根据一定的条件对缓存进行清空

spring cache 清空缓存的方法很简单,就是通过 @CacheEvict注释来标记要清空缓存的方法,当这个方法被调用后,即会清空缓存。


示例代码:

   

    @CacheEvict(value="accountCache",allEntries=true)//重载时清空缓存,allEntries缺省为false。为true时表示清空所有缓存内容

    public void reload() {

 

    }

4.2.4 @Cache -组合多个Cache注解

@Caching定义如下:

public @interface Caching {

         Cacheable[]cacheable() default {}; //声明多个@Cacheable

         CachePut[]put() default {};        //声明多个@CachePut

         CacheEvict[]evict() default {};    //声明多个@CacheEvict

}

有时候我们可能组合多个Cache注解使用;比如用户新增成功后,我们要添加id-->userusername--->useremail--->user的缓存;此时就需要@Caching组合多个注解标签了。  

示例代码:

@Caching(

            put = {

                   @CachePut(value = "user", key = "#user.id"),

                   @CachePut(value = "user", key = "#user.username"),

                   @CachePut(value = "user", key = "#user.email")

            }

    )

    public User save(Useruser) {如用户新增成功后,添加id-->userusername--->useremail--->user到缓存; 

 

五.Spring Cache扩展

5.1 缓存管理器扩展

(1) 提供一个 CacheManager 接口的实现,这个接口告诉 spring有哪些 cache实例,spring会根据 cache的名字查找 cache的实例。

 

示例代码

 publicclass MyCacheManager extends AbstractCacheManager {

  private Collection<? extends MyCache> caches;

 

   /**

   *Specify the collection of Cache instances to use for this CacheManager.

   */

   publicvoid setCaches(Collection<? extends MyCache> caches) {

    this.caches = caches;

   }

 

  @Override

  protected Collection<? extends MyCache> loadCaches() {

    return this.caches;

   }

 

 }

(2) 在配置文件中配置自定义的缓存管理器

   <!-- generic cache manager -->

   <beanid="cacheManager"

     class="com.cnMyCacheManager">

       <propertyname="caches">

         <set>

           <bean

             class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"

             p:name="default"/>        

           <bean

             class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"

             p:name="accountCache"/>

          </set>

       </property>

   </bean>  

5.2 缓存接口扩展

1)实现 Cache接口,Cache接口负责实际的缓存逻辑,例如增加键值对、存储、查询和清空等。利用 Cache接口,我们可以对接任何第三方的缓存系统,例如 EHCacheOSCache,甚至一些内存数据库例如 memcache 或者 h2db等。

 

public class MyCache implements Cache {

   private String name;

   privateMap<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(Stringname) {

     this.name = name;

   }

 

   @Override

   public Object getNativeCache() {

     return store;

   }

 

   @Override

   public ValueWrapperget(Object key) {

     ValueWrapper result =null;

     Account thevalue =store.get(key);

     return result;

   }

 

   @Override

   public void put(Object key,Object value) {

     Account thevalue =(Account)value;

     store.put((String)key,thevalue);

   }

 

   @Override

   public void evict(Objectkey) {

   }

 

   @Override

   public void clear() {

   }

 }

(2) 在配置文件中配置自定义的缓存

   <!-- generic cache manager -->

  <bean id="cacheManager"class="cacheOfAnno.MyCacheManager">

    <property name="caches">

      <set>

        <bean

          class="cacheOfAnno.MyCache"

          p:name="accountCache" />

      </set>

    </property>

  </bean>

六.自定义缓存注解

比如之前的那个@Caching组合,会让方法上的注解显得整个代码比较乱,此时可以使用自定义注解把这些注解组合到一个注解中,如: 

@Caching(

       put = {

                @CachePut(value ="user", key = "#user.id"),

                @CachePut(value ="user", key = "#user.username"),

                @CachePut(value ="user", key = "#user.email")

       }

)

@Target({ElementType.METHOD,ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

@Inherited

public @interface UserSaveCache {

}

这样我们在方法上使用如下代码即可,整个代码显得比较干净。 

@UserSaveCache

public User save(User user)

 

七.键生成策略

 键的生成策略有两种,一种是默认策略,一种是自定义策略。

7.1 默认策略

 <cache:annotation-driven key-generator="key生成策略"/>

key-generator 默认使用org.springframework.cache.interceptor.SimpleKeyGenerator ,即默认的key生成策略。

       默认的key生成策略是通过KeyGenerator生成的,其默认策略如下:

n  如果方法没有参数,则使用0作为key。

n  如果只有一个参数的话则使用该参数作为key。

n  如果参数多余一个的话则使用所有参数的hashCode作为key。

 

7.2 自定义策略

7.2.1 通过Spring的EL表达式来指定我们的key

 自定义策略是指我们可以通过Spring的EL表达式来指定我们的key。这里的EL表达式可以使用方法参数及它们对应的属性。使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。下面是几个使用参数作为key的示例。

   @Cacheable(value="users", key="#id")

   public User find(Integer id) {

      returnnull;

   }

 

   @Cacheable(value="users", key="#p0")

   public User find(Integer id) {

      returnnull;

   }

 

   @Cacheable(value="users", key="#user.id")

   public User find(User user) {

      returnnull;

   }

 

   @Cacheable(value="users", key="#p0.id")

   public User find(User user) {

      returnnull;

   }

 

       除了上述使用方法参数作为key之外,Spring还为我们提供了一个root对象可以用来生成key。通过该root对象我们可以获取到以下信息。

属性名称

描述

示例

methodName

当前方法名

#root.methodName

method

当前方法

#root.method.name

target

当前被调用的对象

#root.target

targetClass

当前被调用的对象的class

#root.targetClass

args

当前方法参数组成的数组

#root.args[0]

caches

当前被调用的方法使用的Cache

#root.caches[0].name

当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。如:

   @Cacheable(value={"users""xxx"}, key="caches[1].name")

   public User find(User user) {

      returnnull;

   }

7.2.2 自定义Key-generator

如果我们需要指定自己的默认策略的话,那么我们可以实现自己的KeyGenerator,然后指定我们的Spring Cache使用的KeyGenerator为我们自己定义的KeyGenerator。

       使用基于注解的配置时是通过cache:annotation-driven指定的.

   <cache:annotation-driven key-generator="userKeyGenerator"/>

  

   <bean id="userKeyGenerator" class="com.xxx.cache.UserKeyGenerator"/>

 

       而使用基于XML配置时是通过cache:advice来指定的。

   <cache:advice id="cacheAdvice" cache-manager="cacheManager" key-generator="userKeyGenerator">

   </cache:advice>

 

       需要注意的是此时我们所有的Cache使用的Key的默认生成策略都是同一个KeyGenerator。

八.ICOP中的缓存应用

8.1 ICOP缓存全局类图


8.2 缓存配置类-CacheConfig

提供spring配置类,注入缓存用到的配置信息和bean。

 

8.2.1  配置文件注入CacheConfig信息

(1) application.xml注入 <beanclass="com.yyjz.icop.cache.config.CacheConfig"/>

(2) application.properties配置redis信息

#redis

redis.session.url=direct://123.56.19.206:19379?poolSize=50&poolName=mypool

redis.url=direct://123.56.19.206:19379?poolSize=20&poolName=x

sessionMutex=false

redis.hostName=123.56.19.206

redis.host=123.56.19.206

redis.port=19379

#redis.url=direct://localhost:19379?poolSize=20&poolName=x

#redis.host=localhost

#redis.port=19379

redis.pass=

redis.maxIdle=10

redis.maxWait=10000

 

8.2.2 配置redis基本配置信息

   @Value("${redis.pool.maxTotal:10}")

   privateintmaxTotal;//最大连接数

   @Value("${redis.pool.maxIdle:10}")

   privateintmaxIdle;//最大空闲连接数

   @Value("${redis.pool.minIdle:0}")

   privateintminIdle;//最小空闲连接数

   @Value("${redis.pool.maxWaitMillis:-1}")

   privatelongmaxWaitMillis;//最大建立连接等待时间

   @Value("${redis.pool.testOnBorrow:false}")

   privatebooleantestOnBorrow;//获取连接时检查有效性

   @Value("${redis.pool.testWhileIdle:false}")

   privatebooleantestWhileIdle;//空闲时检查有效性

 

   @Value("${redis.hostName:127.0.0.1}")

   privateStringhostName;// 主机名

   @Value("${redis.port:6379}")

   privateintport;//监听端口

   @Value("${redis.password:}")

   privateStringpassword;// 密码

   @Value("${redis.timeout:2000}")

   privateinttimeout;//客户端连接时的超时时间(单位为秒)

 

   @Value("${redis.cache.defaultExpiration:3600}")

   privatelongdefaultExpiration;//缓存时间,单位为秒(默认为0,表示永不过期)

 

   @Value("${guava.cache.expireAfterWrite:1h}")

    private String expireAfterWrite; // 缓存时间(m:分钟;h:小时;d:天)

 

8.2.3 注入RedisCacheManager

 

   /**

     * 注入redis cache manager

     *

     * @return

     */

   @Bean

   @Primary

   publicRedisCacheManager redisCacheManager() {

        RedisCacheManager cacheManager =newRedisCacheManager(redisTemplate());

        cacheManager.setDefaultExpiration(defaultExpiration);

        returncacheManager;

}

 

8.2.4 注入GuavaCacheManager

  /**

     * 注入guava cache manager

     *

     * @return

     */

   @Bean

   publicGuavaCacheManager guavaCacheManager() {

        GuavaCacheManager cacheManager =new GuavaCacheManager();

        StringBuilder spec = new StringBuilder();

        spec.append("expireAfterWrite=" +expireAfterWrite);

        spec.append(",");

        if (spec.length() > 0) {

            spec = spec.deleteCharAt(spec.length()- 1);

        }

        cacheManager.setCacheSpecification(spec.toString());

        returncacheManager;

}

 

8.2.5 缓存对象的获取

(1) 从ApplicationContext获取

            ApplicationContextctx= WebApplicationContextUtils.getWebApplicationContext(context);

        redisTemplate = ctx.getBean("redisTemplate",RedisTemplate.class);

(2)自动装配

@Resource(“redisTemplate”)

publicRedisTemplate redisTemplate;

 

8.3 高效批量读取/写入-Pipeline

8.3.1 Redis的通讯方式

Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。

这意味着通常情况下一个请求会遵循以下步骤:

·        客户端向服务端发送一个查询请求,并监听Socket返回,通常是以阻塞模式,等待服务端响应。

·        服务端处理命令,并将结果返回给客户端。

因此,例如下面是4个命令序列执行情况:

·        Client: INCR X

·        Server: 1

·        Client: INCR X

·        Server: 2

·        Client: INCR X

·        Server: 3

·        Client: INCR X

·        Server: 4

客户端和服务器通过网络进行连接。这个连接可以很快(loopback接口)或很慢(建立了一个多次跳转的网络连接)。无论网络延如何延时,数据包总是能从客户端到达服务器,并从服务器返回数据回复客户端。

这个时间被称之为 RTT (Round Trip Time -往返时间). 当客户端需要在一个批处理中执行多次请求时很容易看到这是如何影响性能的(例如添加许多元素到同一个list,或者用很多Keys填充数据库)。例如,如果RTT时间是250毫秒(在一个很慢的连接下),即使服务器每秒能处理100k的请求数,我们每秒最多也只能处理4个请求。

如果采用loopback接口,RTT就短得多(比如我的主机ping127.0.0.1只需要44毫秒),但它任然是一笔很多的开销在一次批量写入操作中。

幸运的是有一种方法可以改善这种情况。

8.3.2 Redis 管道(Pipelining)

一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应。这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。

这就是管道(pipelining),是一种几十年来广泛使用的技术。例如许多POP3协议已经实现支持这个功能,大大加快了从服务器下载新邮件的过程。

Redis很早就支持管道(pipelining)技术,因此无论你运行的是什么版本,你都可以使用管道(pipelining)操作Redis

8.3.3 ICOP的Pipeline实现

ICOP提供了JedisCacheTool工具类,该类提供了redis的pipeline操作



JedisCacheTool源码如下:

package com.yyjz.icop.cache.strategy;

 

importorg.springframework.beans.factory.annotation.Autowired;

importorg.springframework.data.redis.serializer.JdkSerializationRedisSerializer;

importorg.springframework.stereotype.Component;

import redis.clients.jedis.Jedis;

import redis.clients.jedis.JedisPool;

import redis.clients.jedis.Pipeline;

import redis.clients.jedis.Response;

 

import java.util.ArrayList;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

 

/**

 * 采用Pipeline管道模式提高批量插入效率

 *

 *@author hupeng 2016年10月11日

 *

 */

@Component

public class JedisCacheTool {

         @Autowired

         privateJedisPool jedisPool;

         privatestatic JdkSerializationRedisSerializer redisSerializer = newJdkSerializationRedisSerializer();

 

         /**

          * 批量插入

          *

          * @param keys

          * @param values

          */

         publicvoid putBatch(List<CacheObject> cacheObjects) {

                   Jedisjedis = jedisPool.getResource();

                   Pipelinepipeline = jedis.pipelined();

                   for(CacheObject cacheObject : cacheObjects) {

                            pipeline.set(redisSerializer.serialize(cacheObject.getKey()),

                                               redisSerializer.serialize(cacheObject.getValue()));

                   }

                   pipeline.sync();

                   jedis.close();

         }

 

         /**

          * 批量获取

          *

          * @param keys

          * @return

          */

         publicMap<Object, Object> getBatch(List<Object> keys) {

                   Map<Object,Object> map = new HashMap<Object, Object>();

                   Jedisjedis = jedisPool.getResource();

                   Pipelinepipeline = jedis.pipelined();

                   List<Response<byte[]>>results = new ArrayList<>();

                   for(Object key : keys) {

                            Response<byte[]>result = pipeline.get(redisSerializer.serialize(key));

                            results.add(result);

                   }

                   pipeline.sync();

                   for(int i = 0; i < results.size(); i++) {

                            Response<byte[]>result = results.get(i);

                            Objectobj = redisSerializer.deserialize(result.get());

                            if(obj != null)

                                     map.put(keys.get(i),obj);

                   }

                   jedis.close();

                   returnmap;

         }

 

         /**

          * 批量删除

          *

          * @param keys

          * @return

          */

         publicvoid deleteBatch(List<Object> keys) {

                   Jedisjedis = jedisPool.getResource();

                   Pipelinepipeline = jedis.pipelined();

                   pipeline.del(redisSerializer.serialize(keys));

                   pipeline.sync();

                   jedis.close();

         }

 

         /**

          * 单个获取

          *

          * @param key

          * @return

          */

         publicObject get(Object key) {

                   Jedisjedis = jedisPool.getResource();

                   Pipelinepipeline = jedis.pipelined();

                   Response<byte[]>result = pipeline.get(redisSerializer.serialize(key));

                   pipeline.sync();

                   Objectobj = redisSerializer.deserialize(result.get());

                   jedis.close();

                   returnobj;

         }

 

         /**

          * 单个插入

          *

          * @param keys

          * @param values

          */

         publicvoid put(Object key, Object value) {

                   Jedisjedis = jedisPool.getResource();

                   Pipelinepipeline = jedis.pipelined();

                   pipeline.set(redisSerializer.serialize(key),redisSerializer.serialize(value));

                   pipeline.sync();

                   jedis.close();

         }

 

         /**

          * 单个获取

          *

          * @param key

          * @return

          */

         publicvoid delete(Object key) {

                   Jedisjedis = jedisPool.getResource();

                   Pipelinepipeline = jedis.pipelined();

                   pipeline.del(redisSerializer.serialize(key));

                   pipeline.sync();

                   jedis.close();

         }

}



0 0
原创粉丝点击