Spring缓存机制

Spring缓存

一 简介

Spring3.1引入了基于注释的缓存技术,它本质上并不是一个具体的缓存实现方案,而是对缓存使用的抽象。通过在既有代码中添加少量Spring定义的各种annotation,就能达到缓存方法的返回对象的效果。
Spring的缓存技术还具备相当的灵活性,不仅能够使用SpEL来定义缓存的key和各种condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如EHCache集成。

特点如下:

  • 通过少量的配置annotation注释即可使得既有代码支持缓存
  • 支持开箱即用,即不用安装和部署额外的第三方组件
  • 支持SpEL,能够使用对象的任何属性或方法来定义缓存的key和condition
  • 支持AspectJ,并通过其实现任何方法的缓存支持
  • 支持自定义key和自定义缓存管理者,具有相当的灵活性和扩展性

二 不用Spring的做法

这个例子是一个完全自定义的缓存实现,即不用任何第三方的组件来实现某种对象的内存缓存。
Account.java

package cacheOfAnno; 

public class Account { 
    private int id; 
    private String name; 

    public Account(String name) { 
        this.name = name; 
    } 
    public int getId() { 
        return id; 
    } 
    public void setId(int id) { 
        this.id = id; 
    } 
    public String getName() { 
        return name; 
    } 
    public void setName(String name) { 
        this.name = name; 
    } 
}

定义一个缓存管理器,用来实现缓存逻辑,支持对象的增加,修改和删除,支持值对象的泛型。如下:
MyCacheManager.java

package oldcache; 
 
import java.util.Map; 
import java.util.concurrent.ConcurrentHashMap; 
 
public class MyCacheManager<T> { 
  private Map<String,T> cache = 
      new ConcurrentHashMap<String,T>(); 
  
  public T getValue(Object key) { 
    return cache.get(key); 
  } 
  
  public void addOrUpdateCache(String key,T value) { 
    cache.put(key, value); 
  } 
  
  public void evictCache(String key) {
    // 根据 key 来删除缓存中的一条记录
    if(cache.containsKey(key)) { 
      cache.remove(key); 
    } 
  } 
  
  public void evictCache() {
    // 清空缓存中的所有记录
    cache.clear(); 
  } 
}

MyAccountService.java

package oldcache; 
 
import cacheOfAnno.Account; 
 
public class MyAccountService { 
  private MyCacheManager<Account> cacheManager; 
  
  public MyAccountService() { 
    // 构造一个缓存管理器
    cacheManager = new MyCacheManager<Account>();
  } 
  
  public Account getAccountByName(String acctName) { 
    Account result = cacheManager.getValue(acctName);// 首先查询缓存
    if(result!=null) { 
      System.out.println("get from cache..."+acctName); 
      return result;// 如果在缓存中,则直接返回缓存的结果
    } 
    result = getFromDB(acctName);// 否则到数据库中查询
    if(result!=null) {// 将数据库查询的结果更新到缓存中
      cacheManager.addOrUpdateCache(acctName, result); 
    } 
    return result; 
  } 
  
  public void reload() { 
    cacheManager.evictCache(); 
  } 
  
  private Account getFromDB(String acctName) { 
    System.out.println("real querying db..."+acctName); 
    return new Account(acctName); 
  } 
}

测试:

package oldcache; 
 
public class Main { 
 
  public static void main(String[] args) { 
    MyAccountService s = new MyAccountService(); 
    // 开始查询账号
    s.getAccountByName("somebody");// 第一次查询,应该是数据库查询
    s.getAccountByName("somebody");// 第二次查询,应该直接从缓存返回
    
    s.reload();// 重置缓存
    System.out.println("after reload..."); 
    
    s.getAccountByName("somebody");// 应该是数据库查询
    s.getAccountByName("somebody");// 第二次查询,应该直接从缓存返回
    
  } 
 
}

看一下运行结果:

real querying db...somebody// 第一次从数据库加载
get from cache...somebody// 第二次从缓存加载
after reload...// 清空缓存
real querying db...somebody// 又从数据库加载
get from cache...somebody// 从缓存加载

自定义的缓存方案有如下劣势:

  • 缓存代码和业务代码耦合度太高,如上面的AccountService中的getAccountByName()方法中有太多的缓存逻辑,不便于维护和变更
  • 不灵活,这种缓存方案不支持按照某种条件的缓存,比如只有某种类型的账号才需要缓存,这种需求会导致代码的变更
  • 缓存的存储这块写的比较死,不能灵活的切换为使用第三方的缓存模板

三 使用Spring缓存

Spring对缓存的支持有两种:

  • 注解驱动的缓存
  • XML声明的缓存

Spring缓存主要分为两种,一种是直接缓存在内存中,不算真正的缓存实现。其缓存管理器使用的是ConcurrentMapCacheManager,使用java.util.concurrent.concurrentHashMap作为其缓存存储。配置起来非常简单,但一般只推荐作为开发、测试或基础的应用使用。由于它的缓存存储是基于内存的,所以它的生命周期是与应用相关联的,对于生产级别的大型企业级应用程序而言,并不是理想的选择。

一般情况下我们都是使用基于注解驱动的缓存,先来看一下配置项

<cache:annotation-driven />


<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
    
    <property name="caches">
        <set>
            
            <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
                  p:name="sampleCache"/>
            <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
                  p:name="sampleCache2"/>
        set>
    property>
bean>

也可以使用java注解@EnableCaching

import java.util.Arrays;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableCaching
public class CachingConfig {

    @Bean
    public CacheManager cacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("sampleCache"),new ConcurrentMapCache("sampleCache2")));
        return cacheManager;
    }
}

以上这两个方法等价
注意:
1.Spring的XML配置文件需要启动注解驱动的缓存,在本质上,@EnableCaching和它的工作方式是相同的。它们都会创造一个切面(aspect)并触发Spring缓存注解的切点(pointcut),根据所使用的注解以及缓存的状态,这个切面会从缓存中取数据,将数据添加到缓存中或者从缓存中移除某个数据。
2.Spring还要声明一个名叫cacheManager的缓存管理器。缓存管理器是Spring缓存抽象的核心,它能够与多个流行的缓存实现进行集成。接下来讲几个常用的缓存管理器方案。

四 Spring的缓存管理器

Spring3.1内置了五个缓存管理器:

  • SimpleCacheManager
  • NoOpCacheManager
  • ConcurrentMapCacheManager
  • CompositeCacheManager
  • EhCacheCacheManager

Spring3.2引入了另一个缓存管理器,这个管理器可以用在基于JCache(JSR-107)的缓存供应商之中。
除了核心的Spring框架,Spring Data还提供了两个缓存管理器:

  • RedisCacheManager (来自于Spring Data Redis项目)
  • GemfireCacheManager (来自于Spring Data GemFire项目)

由此可见,在为Spring的缓存抽象选择缓存管理器时,我们有很多种选择方案。每一个方案都可以为应用提供不同风格的缓存,其中有一些会比其他的更加适用于生产环境。尽管所做出的选择会影响到数据如何缓存,但是Spring声明缓存的方式没什么差别。

4.1 Ehcache缓存

Ehcache是目前最为流行的缓存供应商之一,对应的缓存管理器就是EhCacheCacheManager。
先来看一下Spring的配置文件


<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:cache="http://www.springframework.org/schema/cache"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
    http://www.springframework.org/schema/cache
    http://www.springframework.org/schema/cache/spring-cache-4.0.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    
    <context:component-scan base-package="com.service"/>
    
        
    <cache:annotation-driven cache-manager="cacheManager" />
    
    
    <bean id="ehCacheManager"
        class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
        p:configLocation="classpath:ehcache.xml"
        p:shared="false" />
        
    
    <bean id="cacheManager"
        class="org.springframework.cache.ehcache.EhCacheCacheManager"
        p:cacheManager-ref="ehCacheManager" > 
    bean>
beans>

ehcache.xml


<ehcache updateCheck="false">
    
    <diskStore path="E:/cachetmpdir"/>

    
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="true"
            maxElementsOnDisk="10000000"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"/>

    
    <cache name="userCache"
           maxElementsInMemory="10000"
           maxElementsOnDisk="1000"
           eternal="false"
           diskPersistent="true"
           overflowToDisk="true"
           diskSpoolBufferSizeMB="20"
           timeToIdleSeconds="300"
           timeToLiveSeconds="600"
           memoryStoreEvictionPolicy="LRU"/>
ehcache>

上面这两个文件即为EhCache的基础配置。接下来对这两个配置文件做一个解释。

  • 我们需要使用EhCache的CacheManager来进行注入,就必须也要声明一个CacheManager bean。为了对其进行简化,Spring提供了EhCacheManagerFactoryBean来生成EhCache的CacheManager.
  • EhCache为XML定义了自己的配置模式,我们需要在一个XML文件中配置缓存,该文件需要符合EhCache所定义的模式。在创建EhCacheManagerFactoryBean的过程中,需要指定EhCache配置文件在什么地方,configLocation用来指定配置文件相对于根类路径(classpath)的位置。

ehcache.xml文件中的参数说明

参数 说明
当内存缓存中对象数量超过maxElementsInMemory时,将缓存对象写到磁盘缓存中(需对象实现序列化接口)。
用来配置磁盘缓存使用的物理路径,Ehcache磁盘缓存使用的文件后缀名是*.data和*.index。
name 缓存名称,cache的唯一标识(ehcache会把这个cache放到HashMap里)。
maxElementsOnDisk 磁盘缓存中最多可以存放的元素数量,0表示无穷大。
maxElementsInMemory 内存缓存中最多可以存放的元素数量,若放入Cache中的元素超过这个数值,则有以下两种情况:(1)若overflowToDisk=true,则会将Cache中多出的元素放入磁盘文件中。(2)若overflowToDisk=false,则根据memoryStoreEvictionPolicy策略替换Cache中原有的元素。
Eternal 缓存中对象是否永久有效,即是否永驻内存,true时将忽略timeToIdleSeconds和timeToLiveSeconds。
timeToIdleSeconds 缓存数据在失效前的允许闲置时间(单位:秒),即访问这个cache中元素的最大间隔时间,若超过这个时间没有访问此Cache中的某个元素,那么此元素将被从Cache中清除。默认值是0表示可闲置时间无穷大。仅当eternal=false时使用
timeToLiveSeconds 缓存数据在失效前的允许存活时间(单位:秒),即Cache中的某元素从创建到清除的生存时间,也就是说从创建开始计时,当超过这个时间时,此元素将从Cache中清除。默认值是0表示可闲置时间无穷大。仅当eternal=false时使用
overflowToDisk 内存不足时,是否启用磁盘缓存(即内存中对象数量达到maxElementsInMemory时,Ehcache会将对象写到磁盘中)
diskPersistent 是否持久化磁盘缓存。当这个属性的值为true时,系统在初始化时会在磁盘中查找文件名为cache名称,后缀名为index的文件,这个文件中存放了已经持久化在磁盘中的cache的index,找到后会把cache加载到内存,要想把cache真正持久化到磁盘,写程序时注意执行net.sf.ehcache.Cache.put(Element element)后要调用flush()方法。
diskExpiryThreadIntervalSeconds 磁盘缓存的清理线程运行间隔,默认是120秒。
diskSpoolBufferSizeMB 设置DiskStore(磁盘缓存)的缓存区大小,默认是30MB
memoryStoreEvictionPolicy 内存存储与释放策略,即达到maxElementsInMemory限制时,Ehcache会根据指定策略清理内存,共有三种策略,分别为LRU(最近最少使用)、LFU(最常用的)、FIFO(先进先出)。

编写一个测试类
将缓存注解加在某个类上:

@Service("userService")
@Cacheable(value="userCache")
public class UserServiceImpl implements UserService {
    @Override
    public User getUsersByNameAndAge(String name, int age) {
        System.out.println("正在执行getUsersByNameAndAge()..");
        return new User(name,age);
    }
}

将缓存注解加在某个方法上:

@Service("userCache")
public class UserServiceImpl implements UserService {
    @Override
    @Cacheable(value = "users", key = "#name", sync = true)
    public User getUsersByNameAndAge(String name, int age) {
        System.out.println("正在执行getUsersByNameAndAge()..");
        return new User(name,age);
    }
}

五 Spring的缓存注解

Spring提供了四个注解来声明缓存规则,这些注解都可以运用在方法或者类上。

注解 说明
@Cacheable Spring在调用该方法之前,首先要在缓存中查找方法的返回值。如果这个值能够被找到,就会返回缓存的值。否则就会调用该方法,并将返回值放到缓存之中
@CachePut Spring应该将方法的返回值放到缓存中。在方法调用前不会检查缓存,方法始终会被调用
@CacheEvict Spring应该在缓存中清除一个或多个条目
@Caching 这是一个分组的注解,能够同时应用到其他多个缓存注解中

5.1 @Cacheable和@CachePut

@Cacheable和@CachePut有一些属性是共有的。

属性 类型 说明 例子
value String[] 缓存的名称,在cache配置文件中定义,必须指定至少一个 @Cacheable(value=“users”)或@Cacheable(value={“cache1”,“cache2”}
key String 缓存的key,可以按SpEL表达式编写,也可以为空,即缺省按照方法的所有参数进行组合 @Cacheable(value=“users”,key="#userName")
condition String 缓存条件,SpEL表达式,返回true或false,只有返回true时才进行缓存 @Cacheable(value=“menuCache”,condition="#userName.length()>2")
unless String SpEL表达式,只有返回false时才进行缓存 @Cacheable(value=“menuCache”,unless="#userName.length()>2")

那么什么时候用@Cacheable,什么时候用@CachePut呢?
看一段代码:

@Cacheable(value="userCache",key="1000")  
public String getUserByName(String userName) {   
   System.out.println("两次调用第一次会执行,第二次不会执行!");  
   return getFromDB(userName);   
}   
@CachePut(value="userCache",key="1000")  
public String updateUserPut(String userName) {  
   return updateDB(userName);   
}   

在进行数据库相关的操作时,如果是更新数据,那么使用@CachePut可以保证每次更新的数据都能写进缓存里,下次再获取相同的数据时,就能获取到更新后的数据。这里必须要保证:

  • 必须是同一个缓存实例
  • 必须是相同的key

5.2 @CacheEvict

@CacheEvict还有其他一些属性

参数 类型 说明
allEntries boolean 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存
beforeInvocation boolean 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存

5.3 @Caching

使用@Caching自定义缓存注解,@Caching定义如下:

public @interface Caching {  
    Cacheable[] cacheable() default {}; //声明多个@Cacheable  
    CachePut[] put() default {};        //声明多个@CachePut  
    CacheEvict[] evict() default {};    //声明多个@CacheEvict  
}  

有时候我们可能组合多个Cache注解使用;比如用户新增成功后,我们要添加id–>user;username—>user;email—>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(User user) 

同理删除数据时从缓存中移除

@Caching(  
        evict = {  
                @CacheEvict(value = "user", key = "#user.id"),  
                @CacheEvict(value = "user", key = "#user.username"),  
                @CacheEvict(value = "user", key = "#user.email")  
        }  
)  
public User delete(User user)   

删除所有数据

@CacheEvict(value = "user", allEntries = true)  
public void deleteAll()  

你可能感兴趣的:(Spring)