在日常工作中,我们时常会或多或少的使用的缓存。举个例子,比如在系统中我们时常会查询一个单位在的所有人员,如果每次我们都从数据库中查询,那么就会导致性能的浪费,这时候我们在类中声明一个静态Map,里面将单位id和人员列表对应起来,我们只在最初查询的时候从数据库中获取,而在之后只是查询Map中数据即可,这其实就是缓存的最简单实现。但是在真正的工作中,我们很少会用这种最简单的方式来做缓存处理,因为这种自身维护Map比较繁琐,而且受限于内存,不可能缓存太多数据。在SpringBoot中我们可以使用它为我们提供的缓存方式,很方便的就实现了数据缓存,在这篇博客中,我们就简单介绍一下EhCache与SpringBoot的结合。
SpringBoot缓存
在SpringBoot中,我们通过添加@EnableCaching注解,来开启SpringBoot的默认缓存,SpringBoot会自动化配置合适的缓存管理器(CacheManager)。SpringBoot侦测缓存提供者的顺序如下:
Generic
JCache (JSR-107)
EhCache 2.x
Hazelcast
Infinispan
Redis
Guava
Simple
EhCache简介
EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider。它具有许多的特性,主要特性:
1.快速
2. 简单
3. 多种缓存策略
4. 缓存数据有两级:内存和磁盘,因此无需担心容量问题
5. 缓存数据会在虚拟机重启的过程中写入磁盘
6. 可以通过RMI、可插入API等方式进行分布式缓存
7. 具有缓存和缓存管理器的侦听接口
8. 支持多缓存管理器实例,以及一个实例的多个缓存区域
SpringBoot与EhCache的结合
SpringBoot结合EhCache2 和 EhCache3配置有比较大的区别,下面首先对不同的配置分别说明。
SpringBoot和EhCache2的结合配置(不同于EhCache3部分)
pom.xml添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
添加EhCache.xml维护缓存配置
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd">
<!--timeToIdleSeconds 当缓存闲置n秒后销毁 -->
<!--timeToLiveSeconds 当缓存存活n秒后销毁 -->
<!-- 缓存配置
name:缓存名称。
maxElementsInMemory:缓存最大个数。
eternal:对象是否永久有效,一但设置了,timeout将不起作用。
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。 diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
maxElementsOnDisk:硬盘最大缓存个数。
diskPersistent:是否缓存虚拟机重启期数据 Whether the disk
store persists between restarts of the Virtual Machine. The default value
is false.
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是
LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除。 -->
<!-- 磁盘缓存位置 -->
<diskStore path="java.io.tmpdir" />
<!-- 默认缓存 -->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
<persistence strategy="localTempSwap" />
</defaultCache>
<!-- 测试 -->
<cache name="MyTest"
eternal="false"
timeToIdleSeconds="2400"
timeToLiveSeconds="2400"
maxEntriesLocalHeap="10000"
maxEntriesLocalDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
overflowToDisk="false"
memoryStoreEvictionPolicy="LRU">
</cache>
<!-- MM -->
<cache name="MM"
eternal="false"
timeToIdleSeconds="1200"
timeToLiveSeconds="1200"
maxEntriesLocalHeap="10000"
maxEntriesLocalDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
overflowToDisk="false"
memoryStoreEvictionPolicy="LRU">
</cache>
</ehcache>
application.properties设置
如果EhCache.xml没有放在默认的resources里面,我们还可以在applicaiton.properties中指定这xml所在的位置。并且我们可以在application.properties中强制设置使用EhCache2来做缓存,而不用依赖于上文所说的顺序。
spring.cache.ehcache.config=classpath:config/ehcache.xml
spring.cache.type=EHCACHE #指定ehcache作为缓存
SpringBoot和EhCache3的结合配置(不同于EhCache2部分)
pom.xml添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
由于EhCache3改为符合JCache (JSR-107)标准,因此需要额外导入包cache-api,并且注意SpringBoot中引用的EhCahe包的groupId也不一样。
添加EhCache.xml维护缓存配置
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
xmlns='http://www.ehcache.org/v3'
xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core.xsd">
<persistence directory="D://myCache" />
<cache alias="MM" uses-template="userCacheTemplate">
<key-type>org.springframework.cache.interceptor.SimpleKey</key-type>
<value-type>java.lang.String</value-type>
</cache>
<cache-template name="userCacheTemplate">
<key-type>java.lang.String</key-type>
<value-type>java.lang.String</value-type>
<expiry>
<tti>200</tti><!-- time to idle,条目在指定时间段内未被使用,则过期 -->
<!-- <ttl>20</ttl> time to live,指定条目的存活时间 -->
</expiry>
<resources>
<heap unit="entries">2000</heap>
<offheap unit="MB">500</offheap>
<disk persistent="true" unit="MB">1000</disk>
</resources>
</cache-template>
<!-- 缓存行情数据 -->
<cache alias="MyTest" uses-template="userCacheTemplate">
<key-type>java.lang.String</key-type>
<value-type>com.zhl.model.User</value-type>
</cache>
</config>
可以看出来,EhCache3和EhCache2的xml配置还是有较大不同的。
这里我们定义了一个cache-template 在设置其他cache的时候 使用uses-template 来设置使用的cache-template,这样就相当于这个cache的未单独设置的属性和cache-template一致,然后还可以覆盖属性。
expiry 设置缓存过期策略,如果是none表示 只有显示调用删除缓存才会删除,而tti 是指定缓存如果在指定时间(单位s)未被使用则删除,而ttl表示缓存存活多久之后会被删除(缓存过期策略只能设置一个,否则会在启动时候报错,提示expiry不能有子元素)。
<resources>
<heap unit="entries">2000</heap>
<offheap unit="MB">500</offheap>
<disk persistent="true" unit="MB">1000</disk>
</resources>
这部分表示缓存最多可以使用堆空间保存2000条目缓存,offheap 堆外空间 保存500MB 使用磁盘空间 最多1000M。
application.properties设置
spring.cache.type=jcache
spring.cache.jcache.config=classpath:config/EhCache3.xml
可以看到这里cache.type设置的是jcache 因为EhCache3符合JCache (JSR-107)标准 所以使用jcache的Mananger。
SpringBoot与EhCache结合示例(EhCache2与EhCache3相同之处)
开启缓存
通过在Application添加@EnableCaching或者添加一个新的类,添加注解@Configuration和@EnableCaching即可。
下面部分,就是我们简单结合的代码示例。
建立CacheDaoImpl模拟Dao层
如下,我们建立了一个CacheDaoImpl类模拟数据的读取,更新,插入与删除。
@Repository
@CacheConfig(cacheNames="MyTest")
public class CacheDaoImpl {
Map<String,User> map = new HashMap<String, User>();
@Cacheable(key="#user.getId()")
public User save(User user) {
System.out.println("save()被执行了....");
map.put(user.getId(), user);
return user;
}
@CachePut(key="#user.getId()")
public User update(User user) {
System.out.println("update()执行了=============");
map.put(user.getId(), user);
return user;
}
@CacheEvict(key="#user.getId()")
public boolean delete(User user) {
System.out.println("delete()执行了=============");
map.remove(user.getId());
return true;
}
@CacheEvict(allEntries=true,value= {"MyTest",”MM”})
public void deleteAll() {
map.clear();
}
@Cacheable(value="MM")
public String selectFromOtherCache() {
System.out.println("其他缓存");
return "来自缓存";
}
@Cacheable(key="#userId")
public User select(String userId) {
System.out.println("select()执行了=============");
User user = map.get(userId);
if(user!=null)
return user;
else
return new User("","","");
}
}
首先,我们在这个类添加了@CacheConfig(cacheNames="MyTest"),表示在这个类中的缓存方法,我们默认操作的缓存对象是MyTest(也就是操作缓存的方法注解中没有value属性的)。我们可以看到我们在方法中使用的注解有@Cacheable,@CachePut,@CacheEvict。三者的不同之处,我们会在后面提到。
建立Controller使用CacheDaoImpl
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private CacheDaoImpl cacheDao;
@RequestMapping("/selectUser")
public String selectUser(@PathParam("id") String id) {
User user = cacheDao.select(id);
return user.toString();
}
@RequestMapping("/saveUser")
public String saveUser(@PathParam("id")String id,@PathParam("username") String username,@PathParam("password") String password) {
User user = new User(id,username,password);
cacheDao.save(user);
return user.toString();
}
@RequestMapping("/updateUser")
public String updateUser(@PathParam("id") String id,@PathParam("username") String username,@PathParam("password") String password){
User user = cacheDao.select(id);
user.setId(id);
user.setUsername(username);
user.setPassword(password);
cacheDao.update(user);
return user.toString();
}
@RequestMapping("/deleteUser")
public String updateUser(@PathParam("id") String id){
User user = cacheDao.select(id);
cacheDao.delete(user);
return "<a href='/user/selectUser?id="+id+"'>查找人员</a>";
}
@RequestMapping("/deleteAll")
public String deleteAll() {
cacheDao.deleteAll();
return "清除所有缓存";
}
@RequestMapping("/selectFromOtherCache")
public String selectFromOtherCache() {
return cacheDao.selectFromOtherCache();
}
}
测试分析
首先我们先在浏览器使用
http://localhost:8081/user/saveUser?id=123&username=test&password=test
可以看到后台会打印
save()被执行了....
多次调用saveUser,后台不再打印save()被执行了....
说明我们这里用@Cachable修饰的方法,会判断缓存是否存在,如果存在则不再执行方法而是走缓存数据,如果不存在那么就执行完方法,将方法返回值缓存下来。
然后打开第二个浏览器调用
http://localhost:8081/user/selectUser?id=123
前台的数据显示
[User]:(id)123,(username)test,(password)test
并且查看后台,未出现调用select的打印,说明是取得的缓存。
然后浏览器调用
http://localhost:8081/user/updateUser?id=123&username=test123&password=test
将原来用户名由test改为test123
查看后台打印了
update()执行了=============
刷新之前的selectUser那个浏览器,发现前台数据发生改变
[User]:(id)123,(username)test123,(password)test
说明缓存已经被更新
重复调用updateUser 发现每次后台都会打印
update()执行了=============
说明我们用@CachePut修饰的方法每次都会执行,但是执行完会用返回值更新/添加到缓存。
我们再调用
http://localhost:8081/user/deleteUser?id=123&username=test123&password=test
后台打印
delete()执行了=============
再次重复执行saveUser,发现后台会打印
save()被执行了....
说明我们用@CacheEvict注解修饰的方法会清空指定key的缓存。
@Cacheable,@CachePut,@CacheEvict进一步说明
@Cacheable
能够根据方法的请求参数对其结果进行缓存(会先判断缓存是否存在,如果存在则取缓存,如果不存在则执行方法,将返回结果缓存下来。)感觉适用于save 和 select 相关的操作。
@Cacheable 主要的参数
value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个(因为我们这里在类上加了注解CacheConfig(cacheNames="MyTest"),所以这里不用添加也可以,若未指定value那么,保存的缓存名称就是MyTest)例如:value=”MyTest”。
key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合如@Cacheable(key="#user.getId()")。如key="#p0" 表示取第一个参数,而key="#p0.getId()"也是可以的。
condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 例如:@Cacheable(value=”#user.getId()”,condition=”#user.getId().length()>2”)
@CachePut
能够根据方法的请求参数对其结果进行缓存,不管是否有无缓存都会执行方法,并且用返回结果添加/更新缓存。感觉适用于 update的操作。参数跟@Cacheable类似。
@CacheEvict
@CacheEvict是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。
能够根据方法的请求参数删除指定的缓存,感觉适用于delete的操作。
@CacheEvict 主要的参数
value key condition 跟@Cacheable一致。还有两个额外参数:
allEntries 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存。例如:@CachEvict(value= {"MyTest",”MM”,allEntries=true),这个就会删除MyTest和MM 对应的所有缓存。如果没有allEntries 设置了value和key那么只删除指定缓存名称下和删除的这个key 一致的缓存数据。
beforeInvocation 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存。
总结分析
Spring使用缓存,实际就是通过代理,在Controller调用(其中有方法被缓存注解修饰的)注入类的方法时,执行方法的时候 首先去查看这个方法是否启用了缓存,然后根据不同的注解进行不同的操作,如果没启用缓存,那么直接执行方法。