Spring3.1引入了基于注释的缓存技术,它本质上并不是一个具体的缓存实现方案,而是对缓存使用的抽象。通过在既有代码中添加少量Spring定义的各种annotation,就能达到缓存方法的返回对象的效果。
Spring的缓存技术还具备相当的灵活性,不仅能够使用SpEL来定义缓存的key和各种condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如EHCache集成。
特点如下:
这个例子是一个完全自定义的缓存实现,即不用任何第三方的组件来实现某种对象的内存缓存。
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// 从缓存加载
自定义的缓存方案有如下劣势:
Spring对缓存的支持有两种:
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缓存抽象的核心,它能够与多个流行的缓存实现进行集成。接下来讲几个常用的缓存管理器方案。
Spring3.1内置了五个缓存管理器:
Spring3.2引入了另一个缓存管理器,这个管理器可以用在基于JCache(JSR-107)的缓存供应商之中。
除了核心的Spring框架,Spring Data还提供了两个缓存管理器:
由此可见,在为Spring的缓存抽象选择缓存管理器时,我们有很多种选择方案。每一个方案都可以为应用提供不同风格的缓存,其中有一些会比其他的更加适用于生产环境。尽管所做出的选择会影响到数据如何缓存,但是Spring声明缓存的方式没什么差别。
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.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提供了四个注解来声明缓存规则,这些注解都可以运用在方法或者类上。
注解 | 说明 |
---|---|
@Cacheable | Spring在调用该方法之前,首先要在缓存中查找方法的返回值。如果这个值能够被找到,就会返回缓存的值。否则就会调用该方法,并将返回值放到缓存之中 |
@CachePut | Spring应该将方法的返回值放到缓存中。在方法调用前不会检查缓存,方法始终会被调用 |
@CacheEvict | Spring应该在缓存中清除一个或多个条目 |
@Caching | 这是一个分组的注解,能够同时应用到其他多个缓存注解中 |
@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可以保证每次更新的数据都能写进缓存里,下次再获取相同的数据时,就能获取到更新后的数据。这里必须要保证:
@CacheEvict还有其他一些属性
参数 | 类型 | 说明 |
---|---|---|
allEntries | boolean | 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 |
beforeInvocation | boolean | 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 |
使用@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()