SpringBoot基于Spring Cache实现缓存(包括集成Redis和EhCache)
- 缓存的目的是:通过Cache来缓存不经常改变的数据以提高系统性能和增加系统吞吐量,避免直接访问数据库等低速的存储系统。
- Spring Cache对Cache进行抽象,提供了@Cacheable、@CachePut、@CacheEvict等注解。
- 可用于单体应用系统,也可集成Redis等缓存服务器用于大型系统或者分布式系统。
一、使用Spring自带的缓存管理器
- Spring自带的缓存类型为Simple,这个缓存与Spring Boot应用在同一个Java虚拟机内,适合单体应用系统。
- pom文件加入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId></dependency>
- application.properties中配置属性
spring.cache.type=Simple
- Simple:基于ConcurrentHashMap实现的缓存,只适合单体应用或者开发环境使用。
- None:禁止使用缓存。
- Redis:使用Redis缓存。
等…
- 然后在启动或者配置类上加入
@EnableCaching
注解来开启缓存注解。
//开启缓存注解@EnableCaching@SpringBootApplicationpublicclassApplication{publicstaticvoidmain(String[] args){
SpringApplication.run(Application.class, args);}}
- 示例一个Service实现类,并进行统一解释。
import com.example.demo.bean.User;import com.example.demo.service.UserService;import lombok.extern.slf4j.Slf4j;import org.springframework.cache.annotation.*;import org.springframework.stereotype.Service;import java.util.HashMap;import java.util.Map;@Service@Slf4j@CacheConfig(cacheNames={"user"})//统一指定value的值,这时可省略value,如果你在你的方法依旧写上了value,那么依然以方法的value值为准。publicclassUserServiceImplimplementsUserService{privatestaticfinal Map<Integer, User> users=newHashMap<>(3);static{
users.put(1,newUser(1,"bym"));
users.put(2,newUser(2,"wx"));
users.put(3,newUser(3,"sqp"));}@Override//@Cacheable(key = "targetClass + methodName +#p0")//@Cacheable(value="users", key="#p0")//@Cacheable(key = "#id")@Cacheablepublic UsergetUser(int id){
log.info("缓存中没有,从数据中取...");return users.get(id);}/**
* 在更新数据的同时,缓存也被更新
* @param user
* @return
*/@Override@CachePut(cacheNames="space", key="#user.id")public UserupdateUser(User user){
log.info("更新数据...");
users.put(user.getId(), user);return user;}//清除一条缓存,key为要清空的数据@CacheEvict(value="space", key="#id")publicvoiddelete(int id){
users.remove(id);}//方法调用后清空所有缓存@CacheEvict(value="space", allEntries=true)publicvoiddeleteAll(){
users.clear();}//方法调用前清空所有缓存@CacheEvict(value="space", beforeInvocation=true)publicvoiddeleteAllBefore(){
users.clear();}/**
* @Caching是 @Cacheable、@CachePut、@CacheEvict注解的组合
* 以下注解的含义:
* 1.当使用指定名字查询数据库后,数据保存到缓存
* 2.现在使用id、age就会直接查询缓存,而不是查询数据库
*/}
注意:这里的User实体类要实现Serializable序列化接口。
public class User implements Serializable
,否则会报异常。
@Cacheable
@Cacheable
作用在方法上,声明了方法的结果是可缓存的。
- 如果缓存存在,则目标方法不会被调用,直接取出缓存。可以为方法声明多个缓存,如果至少有一个缓存有缓存项,则其缓存项将被返回。
- 如果缓存不存在,则进入实际业务方法,将业务方法返回的结果缓存下来。
属性
打开此注解的源码,如下:
@AliasFor("cacheNames")
String[]value()default{};@AliasFor("value")
String[]cacheNames()default{};
Stringkey()default"";
StringkeyGenerator()default"";//key的生成器。key/keyGenerator二选一使用
StringcacheManager()default"";//指定缓存管理器
StringcacheResolver()default"";//或者指定获取解析器
Stringcondition()default"";//条件符合则缓存
Stringunless()default"";//条件符合则不缓存booleansync()defaultfalse;//是否使用异步模式
cacheNames
和value
是一个意思,都表示指定缓存的命名空间(或名称),注解中必须指定命名空间。(本文指定用cacheNames)
写法:
@Cacheable@Cacheable("user")@Cacheable({"user","info"})@Cacheable(cacheNames="user",key="xx")@Cacheable(cacheNames={"user","info"},key="xx")
key
指缓存的key。
1)可以为空,为空时,Spring使用KeyGenerator类来根据方法参数生成key。
KeyGenerator
KeyGenerator的规则:
1.如果方法只有一个参数,这个参数就是Key。
2.如果没有参数,则Key是SimpleKey.EMPTY。
3.如果有多个Key,则返回包含多个参数的SimpleKey。
Spring使用SimpleKeyGenerator来实现上述key的生成。
源码:
package org.springframework.cache.interceptor;import java.lang.reflect.Method;publicclassSimpleKeyGeneratorimplementsKeyGenerator{publicSimpleKeyGenerator(){}public Objectgenerate(Object target, Method method, Object... params){returngenerateKey(params);}publicstatic ObjectgenerateKey(Object... params){if(params.length==0){return SimpleKey.EMPTY;}else{if(params.length==1){
Object param= params[0];if(param!= null&&!param.getClass().isArray()){return param;}}returnnewSimpleKey(params);}}}
我们还可以实现自己的KeyGenerator方法:
@Cacheable(cacheNames="users",keyGenerator="myKg)public UsergetUser(int id){...}
myKg实现了KeyGenerator接口,然后从user中获取到id作为缓存的Key。
@Overridepublic Objectgenerate(Object target, Method method, Object... params){
User user=(User)params[0];return user.getId();}
通常情况下,直接使用SpEL表达式来指定Key比自定义KeyGenerator更简单。
2)可按照SpEL表达式编写。
SpEL
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root对象 | 当前被调用的方法名 | #root.methodname |
method | root对象 | 当前被调用的方法 | #root.method.name |
target | root对象 | 当前被调用的目标对象实例 | #root.target |
targetClass | root对象 | 当前被调用的目标对象的类 | #root.targetClass |
args | root对象 | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root对象 | 当前方法调用使用的缓存列表 | #root.caches[0].name |
Argument Name | 执行上下文 | 当前被调用的方法的参数,如findArtisan(Artisan artisan),可以通过#artsian.id获得参数 | #artsian.id |
result | 执行上下文 | 方法执行后的返回值(仅当方法执行后的判断有效,如 unless cacheEvict的beforeInvocation=false) | #result |
1.当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。 如
@Cacheable(key="targetClass + methodName +#p0")
2.使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。 如:
@Cacheable(value="users", key="#id")public Usermethod1(int id){...}@Cacheable(value="users", key="#p0")public Usermethod2(int id,String name){...}@Cacheable(value="users", key="#user.id")public Usermethod3(User user){...}
3.SpEL提供了多种运算符
类型 | 运算符 |
---|---|
关系 | <,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne |
算术 | +,- ,* ,/,%,^ |
逻辑 | &&, |
条件 | ?: (ternary),?: (elvis) |
正则表达式 | matches |
其他类型 | ?.,?[…],![…],$[…] |
@CachePut
@CachePut
作用在方法上,总是会执行方法体,使用方法体返回的结果更新缓存。属性和Cacheable一样。需要注意的是该注解的value 和 key 必须与要更新的缓存相同,也就是与@Cacheable 相同。
@CacheEvict
@CacheEvict
作用在方法上,能够根据一定的条件删除缓存项或清空缓存。
这里需要注意两个属性:
属性 | 解释 | 示例 |
---|---|---|
allEntries | 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 | @CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation | 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 | @CachEvict(value=”testcache”,beforeInvocation=true) |
注意:CacheEvict只具备删除缓存功能,不具备加载缓存功能,只有相应的@Cacheable方法被调用后,才会加载最新缓存项。
缓存系统中不经常改变的业务数据一旦发生改变时,通常会更新此业务相关的所有缓存 。
- 以菜单为例,如果添加菜单项目,应更新菜单相关的所有缓存。
//方法调用后清空所有缓存@CacheEvict(value={"menu","menuTree"}, allEntries=true)publicvoidaddMenu(Menu menu){
users.clear();}
在更新菜单后,菜单树和菜单缓存都需要清空以便下次获取的时候重新构造。
@Caching
@Caching
作用在方法上,可以混合以上三种注解,比如一个修改同时需要失效对应的用户缓存和用户扩展信息缓存。
@Caching(evict={@CacheEvict(value="user",key="#user.id"),@CacheEvict(value="userExt",key="#ext.id")})publicvoidupdateUser(User user,UserExt ext){...}
@CacheConfig
到目前为止,所有的Cache注解都需要提供Cache名称,如果每个Service方法上都包含一个Cache名称,可能写起来重复,注解@CacheConfig
作用于类上,可以为此类的方法的缓存注解提供默认值,包括缓存的默认名称和KeyGenerator。
用法:
@CacheConfig("users")
等。
这时可省略方法上的value,如果你在你的方法依旧写上了value,那么依然以方法的value值为准。
二、Spring Cache整合Redis
- 对于分布式应用,通常都会将缓存放在一台或者多台专门的缓存服务器上。使用Redis作为缓存是一种常用的选择。
- pom文件加入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- redis依赖commons-pool 这个依赖一定要添加 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency>
- application.properties中配置属性
spring.cache.type=Redis
spring.redis.host=192.168.2.185
spring.redis.port=6379
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
其注解使用方式与Simple方式一致。
- 通过上面两步的配置,SpringBoot的CacheManager就会使用RedisCache。
定制缓存:对于RedisCacheManager来说,还可以定制缓存项的存活时间,还有修改缓存序列化策略。
- 添加一个配置类:
@Configuration@AutoConfigureAfter(RedisAutoConfiguration.class)publicclassCacheConfigextendsCachingConfigurerSupport{//缓存管理器@Beanpublic RedisCacheManagerredisCacheManager(RedisConnectionFactory factory){
Jackson2JsonRedisSerializer serializer=newJackson2JsonRedisSerializer(Object.class);
ObjectMapper mapper=newObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);// 生成一个默认配置,通过config对象即可对缓存进行自定义配置
RedisCacheConfiguration config= RedisCacheConfiguration.defaultCacheConfig()//此配置可以使redis的值不为乱码。.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer));// 设置缓存的默认过期时间,也是使用Duration设置// config = config.entryTtl(Duration.ofMinutes(10))// .disableCachingNullValues(); // 不缓存空值// 设置一个初始化的缓存空间set集合
Set<String> cacheNames=newHashSet<>();
cacheNames.add("space");
cacheNames.add("user");// 对每个缓存空间应用不同的配置
Map<String, RedisCacheConfiguration> configMap=newHashMap<>();// 通过Duration可以自己实现以什么时间为单位
configMap.put("space", config.entryTtl(Duration.ofMinutes(1)));
configMap.put("user", config.entryTtl(Duration.ofSeconds(10)));return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(factory)).initialCacheNames(cacheNames)// 注意这两句的调用顺序,一定要先调用该方法设置初始化的缓存名,再初始化相关的配置.withInitialCacheConfigurations(configMap).cacheDefaults(config).build();}// 修改redis默认的缓存序列化策略(jdk序列化),// 这里我们注入了一个RedisTemplate 设置了里面的序列化,然后呢把他注入到redisCacheManger里就可以了。// @Bean// public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {// RedisTemplate<Object, Object> template = new RedisTemplate<>();// template.setConnectionFactory(connectionFactory);// //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)// Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);// ObjectMapper mapper = new ObjectMapper();// mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);// mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);// serializer.setObjectMapper(mapper);// template.setValueSerializer(serializer);// //使用StringRedisSerializer来序列化和反序列化redis的key值// template.setKeySerializer(new StringRedisSerializer());// template.afterPropertiesSet();// return template;// }}
- 配置上面一步前:
- 配置成功后没有乱码问题
二、Spring Cache整合EhCache
- EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认CacheProvider
- Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,Java EE和轻量级容器。它具有内存和磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序,一个gzip缓存servlet过滤器,支持REST和SOAP api等特点。
主要特性:
- 快速
- 简单
- 多种缓存策略
- 缓存数据有两级:内存和磁盘,因此无需担心容量问题
- 缓存数据会在虚拟机重启的过程中写入磁盘
- 可以通过RMI、可插入API等方式进行分布式缓存
- 具有缓存和缓存管理器的侦听接口
- 支持多缓存管理器实例,以及一个实例的多个缓存区域
- 提供Hibernate的缓存实现
ehcache 对分布式支持不够好,多个节点不能同步,通常和redis一块使用
和Redis比较:
-
ehcache直接在jvm虚拟机中缓存,速度快,效率高;但是缓存共享麻烦,集群分布式应用不方便。
-
redis是通过socket访问到缓存服务,效率比ecache低,比数据库要快很多,
处理集群和分布式缓存方便,有成熟的方案。如果是单个应用或者对缓存访问要求很高的应用,用ehcache。如果是大型系统,存在缓存共享、分布式部署、缓存内容很大的,建议用redis。
ehcache也有缓存共享方案,不过是通过RMI或者Jgroup多播方式进行广播缓存通知更新,缓存共享复杂,维护不方便;简单的共享可以,但是涉及到缓存恢复,大数据缓存,则不合适。
ehcache3.x与2.x的差距还是非常大的,主要区别在于3.x后使用了java的缓存规范JSR107!!!
EhCache3.x
- pom文件加入依赖
<!-- JSR107 API --><dependency><groupId>javax.cache</groupId><artifactId>cache-api</artifactId></dependency><dependency><groupId>org.ehcache</groupId><artifactId>ehcache</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId></dependency>
- yml配置
需要说明的是默认路径为config: classpath:/ehcache.xml
如果在这个目录下这个配置可以不用写,但ehcache.xml
必须有。
spring:cache:type: jcachejcache:config: classpath:/cache/ehcache.xml
- 配置文件
在resources的cache目录下新建ehcache.xml
<?xml version="1.0" encoding="UTF-8"?><configxmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'xmlns='http://www.ehcache.org/v3'xmlns:jsr107='http://www.ehcache.org/v3/jsr107'xsi:schemaLocation="
http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd
http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd"><cache-templatename="heap-cache"><resources><heapunit="entries">2000</heap><offheapunit