SpringBoot使用Ehcache进行缓存
Ehcache其实是一个缓存管理器,Ehcache支持本地缓存,Ehcache还支持分布式的缓存
在我们开发中经常碰到一个方法总是执行的很慢,但是这个方法对数据的实时准确度要求不是很高的时候,我们可以使用缓存技术来优化。
1. 导入jar包
<!-- Spring Boot 缓存支持启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Ehcache 坐标 -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId> <!--ehcache的版本 如果没有指定,springboot 会自动指定-->
</dependency>
2. 编写ehcache.xml 缓存配置文件(放在resources里面)
ehcache配置文件中元素说明
ehcach.xml配置文件主要参数的解释,其实文件里有详细的英文注释
//DiskStore 配置
cache 数据的存放目录(其实就是 <diskStorepath="Java.io.tmpdir"/>
path的取值),主要的值有
user.home - 用户主目录
user.dir - 用户当前的工作目录
* java.io.tmpdir - Default temp file path 默认的 temp 文件目录
// Ehcache 的三种清空策略
FIFO,first in first out,这个是大家最熟的,先进先出。
LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来
最少被使用的。如上面所讲,缓存的元素有一个 hit 属性,hit 值最小的将会被清出缓存。
LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量
满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时
间最远的元素将被清出缓存。
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
<!-- 下面这个会报错
<?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 = "Java.io.tmpdir"/>
<defaultCache
eternal="false" <--意味着该缓存会死亡-->
maxElementsInMemory="900"<--缓存的最大数目-->
overflowToDisk="false" <--内存不足时,是否启用磁盘缓存,如果为true则表示启动磁盘来存储,如果为false则表示不启动磁盘-->
diskPersistent="false"
timeToIdleSeconds="0" <--当缓存的内容闲置多少时间销毁-->
timeToLiveSeconds="60" <--当缓存存活多少时间销毁(单位是秒,如果我们想设置2分钟的缓存存活时间,那么这个值我们需要设置120)-->
memoryStoreEvictionPolicy="LRU" /> <--自动销毁策略-->
<!-- 这里的 users 缓存空间是为了下面的 demo 做准备 -->
<cache
name="data"
eternal="false"
maxElementsInMemory="200"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="60"
memoryStoreEvictionPolicy="LRU" />
</ehcache>
<!--<diskStore>==========当内存缓存中对象数量超过maxElementsInMemory时,将缓存对象写到磁盘缓存中(需对象实现序列化接口) -->
<!--<diskStore path="">==用来配置磁盘缓存使用的物理路径,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====缓存数据在失效前的允许闲置时间(单位:秒),仅当eternal=false时使用,默认值是0表示可闲置时间无穷大,此为可选属性 -->
<!--即访问这个cache中元素的最大间隔时间,若超过这个时间没有访问此Cache中的某个元素,那么此元素将被从Cache中清除 -->
<!--timeToLiveSeconds====缓存数据在失效前的允许存活时间(单位:秒),仅当eternal=false时使用,默认值是0表示可存活时间无穷大 -->
<!--即Cache中的某元素从创建到清楚的生存时间,也就是说从创建开始计时,当超过这个时间时,此元素将从Cache中清除 -->
<!--overflowToDisk=======内存不足时,是否启用磁盘缓存(即内存中对象数量达到maxElementsInMemory时,Ehcache会将对象写到磁盘中) -->
<!--会根据标签中path值查找对应的属性值,写入磁盘的文件会放在path文件夹下,文件的名称是cache的名称,后缀名是data -->
<!--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(Least Recently Used 最近最少使用)、LFU(Less Frequently Used最不常用的)、FIFO(first in first out先进先出) -->
3. 在springboot配置文件中引入ehcache.xml配置
# 在Springboot配置文件中把ehcache.xml配置进去;即在application.properties中加入以下配置代码
spring.cache.ehcache.config=ehcache.xml
# 在网上也看到要如下配置
spring.cache.type=ehcache
spring.cache.ehcache.config=classpath:ehcache.xml
# 如果springboot配置文件为yaml文件, 则如下配置:
#使用ehcache缓存配置
spring:
cache:
type: ehcache
ehcache:
config: classpath:ehcache.xml
4. 在启动类中开启缓存
在启动类前加上@EnableCaching注解;这样的话,启动类启动时会去启动缓存启动器。
@SpringBootApplication
@EnableCaching // 开启缓存 启动类启动时会去启动缓存启动器。
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
5. 使用Ehcache的注解进行缓存
注意:
缓存的数据要实现序列化接口Serializable,由于需要实体类支持缓存中的磁盘存储,所以需要实体类实现可序列化接口
缓存常用注解:
@CacheConfig
@Cacheable
@CachePut
@CacheEvict
1)@CacheConfig(cacheNames = {“lemonCache”})设置了ehcache的名称,这个名称就是ehcache.xml内的名称;(可以不指定,应为在yml 中已经制定了)
2)@Cacheable:应用到读取数据的方法上,即可缓存的方法,如查找方法:先从缓存中读取,如果没有再调 用方法获取数据,然后把数据添加到缓存中,适用于查找;
3)@CachePut:主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用。适用于更新和插入;
4)@CacheEvict:主要针对方法配置,能够根据一定的条件对缓存进行清空。适用于删除。
示例:
@Service
@Transactional //事务,表示该类下所有的都受事务控制
public class userServiceImpl implements userService {
@Autowired
private userMapper usermapper;
@Override
@Cacheable(value="users")
public user selectUserById(int id) {
user user=this.usermapper.selectUserById(id);
System.out.println("1111111111111111111111111");
return user;
}
}
说明: @Cacheable可以标记在一个方法上,也可以标记在一个类上,当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。对于一个支持缓存的方法,Spring会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。Spring在缓存方法的返回值时是以键值对进行缓存的,值就是方法的返回结果。
@Cacheable可以指定三个属性,value、key和condition。
value属性指定cache的名称(即选择ehcache.xml中哪种缓存方式存储)
key属性是用来指定Spring缓存方法的返回结果时对应的key的。该属性支持SpringEL表达式。当我们没有指定该属性时,Spring将使用默认策略生成key。我们也直接使用“#参数名”或者“#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;
}
最后,使用@CacheEvict清除缓存;
@CacheEvict(value="users",allEntries=true)
public void saveUsers(Users users) {
this.usersRepository.save(users);
}
说明:@CacheEvict是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。@CacheEvict可以指定的属性有value、key、condition、allEntries和beforeInvocation。
其中value、key和condition的语义与@Cacheable对应的属性类似;allEntries是boolean类型,表示是否需要清除缓存中的所有元素。默认为false,表示不需要。
当指定了allEntries为true时,Spring Cache将忽略指定的key。有的时候我们需要Cache一下清除所有的元素,这比一个一个清除元素更有效率。
package com.shyroke.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import com.shyroke.dao.UserMapper;
import com.shyroke.entity.UserBean;
import com.shyroke.service.CacheUserService;
@CacheConfig(cacheNames = "roncooCache")
@Service
public class CacheUserServiceImpl implements CacheUserService{
@Autowired
private UserMapper userMapper;
@Cacheable(key = "#p0")
@Override
public UserBean getUserById(int id) {
System.out.println("查询功能,缓存找不到,直接读库, id=" + id);
return userMapper.getOne(id);
}
@CachePut(key = "#p0")
@Override
public UserBean update(UserBean user) {
System.out.println("更新功能,更新缓存,直接写库, id=" + user);
return userMapper.save(user);
}
@CacheEvict(key = "#p0")
@Override
public String deleteById(int id) {
System.out.println("删除功能,删除缓存,直接写库, id=" + id);
userMapper.delete(id);
return "删除成功";
}
}
6. 将数据自定义进入缓存
public class EHCacheUtils {
/**
* 设置缓存对象
* @param cacheManager
* @param key
* @param object
*/
public static void setCache(CacheManager cacheManager,String key,Object object){
Cache cache = cacheManager.getCache("objectCache");
Element element = new Element(key,object);
cache.put(element);
}
/**
* 从缓存中取出对象
* @param cacheManager
* @param key
* @return
*/
public static Object getCache(CacheManager cacheManager,String key){
Object object = null;
Cache cache = cacheManager.getCache("objectCache");
if(cache.get(key)!=null && !cache.get(key).equals("")){
object = cache.get(key).getObjectValue();
}
return object;
}
}
存入缓存方法如下:
@Autowired
private CacheManager cacheManager;
//部分关键代码
EHCacheUtils.setCache(cacheManager,"op",searchOP);
取出缓存方法如下:
@Autowired
private CacheManager cacheManager;
Operator searchOP = (Operator) EHCacheUtils.getCache(cacheManager,"op");
问题:
net.sf.ehcache(Ehcache 2.x)和org.ehcache(Ehcache 3.x)共存时引起的BUG
问题再现:Maven中同时存在如下依赖
<!-- Ehcache 3.x -->
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.6.3</version>
</dependency>
<!-- Ehcache 2.x -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.6</version>
</dependency>
项目启动则会报
Caused by: net.sf.ehcache.CacheException: java.lang.annotation.IncompleteAnnotationException: org.terracotta.statistics.Statistic missing element type Caused by: java.lang.annotation.IncompleteAnnotationException: org.terracotta.statistics.Statistic missing element type
原因:net.sf.ehcache与org.ehcache存在jar包冲突 Ehcache官方issues:https://github.com/ehcache/ehcache3/issues/2354解决方法:去掉net.sf.ehcache或者org.ehcache其中一个
如果要使用Ehcache 3.x,则需要阻止Spring Boot将Hibernate配置为使用Ehcache 2.x进行缓存
对加入缓存方法的参数(并且这个参数要作为cache的key)传NULL值,出现的BUG
项目启动则会报
Null key returned for cache operation (maybe you are using named params on classes without debug info?) Builder[public java.lang.String com.gwm.service.impl.TestService.selectById(java.io.Serializable)] caches=[hour] | key='#id' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
调用方法:
@Cacheable(value = "hour", key = "#id")
public String selectById(Serializable id) {
Long timestamp = System.currentTimeMillis();
return timestamp.toString();
}
出错原因:
key中的id 没有传值,在调用方法时,不能传null值。
eg:
Serializable id = "aa";
String meg=testService.selectById(id); // 传上值!!!!!
在cache 中查询缓存数据时,key要和之前存进去的key的类型一致
demo:
@Service
public class EhcacheTest {
private static Integer count = 0;
@Autowired
UserRepository userRepository;
@Autowired
CacheManager cacheManager;
// key 为Long类型 值为1
@Cacheable(value = "users",key="#id")
public User findUserById(Long id) {
Optional<User> user = userRepository.findById(id);
System.out.println("加入缓存了 -- > " + count);
count++;
return user.get();
}
@Cacheable(value = "users",key="#name")
public User findUserByName(String name) {
User user = userRepository.findByUserName(name);
System.out.println("加入缓存了 -- > " + count);
count++;
return user;
}
@CacheEvict(value="users",allEntries=true)
public void clearCache() {
System.out.println("清空缓存");
}
public User getCache(String key) {
Cache cache = cacheManager.getCache(SystemConstant.CACHE_NAME);
List keys = cache.getKeys();
for (Object o : keys) {
System.out.println(o.toString());
}
Element element = cache.get(key);
User user = (User) element.getObjectValue();
return user;
}
// 查询的时候一定要这个方法来查 如果使用参数为String ("1") 就会报空指针错误 因为会查不到key为“1”的值
public User getCache2(Long key) {
Cache cache = cacheManager.getCache(SystemConstant.CACHE_NAME);
List keys = cache.getKeys();
for (Object o : keys) {
System.out.println(o.toString());
}
Element element = cache.get(key);
User user = (User) element.getObjectValue();
return user;
}
}
参考链接:
https://www.cnblogs.com/xzmiyx/p/9897623.html
https://www.cnblogs.com/xiaowangbangzhu/p/10558029.html
https://www.jianshu.com/p/05f3ede0b389
https://blog.csdn.net/qq_28143647/article/details/79789368
https://www.cnblogs.com/shyroke/p/8025604.html
https://blog.csdn.net/Lammonpeter/article/details/78602862
http://www.ehcache.org/apidocs/2.9/net/sf/ehcache/Ehcache.html#getKeys() ehcache API
How to Iterate on a cache entries 第二个链接也包含了spring下咋整的结论。
https://www.cnblogs.com/wchxj/p/8119528.html




