1.情景展示
从3.1开始,Spring引入了对Cache的支持。其使用方法和原理都类似于Spring对事务管理的支持。Spring Cache是作用在方法上的,其核心思想是这样的:
当我们在调用一个缓存方法时会把该方法参数和返回结果作为一个键值对存放在缓存中,等到下次利用同样的参数来调用该方法时将不再执行该方法,而是直接从缓存中获取结果进行返回。
spring中内部集成了缓存,我们可以拿来直接使用,如何实现对缓存的增、删、改、查操作?
2.@Cacheable、@CachePut、@CacheEvict
通过这三个注解,完全可以实现对缓存数据库的增删改查。
@Cacheable
使用范围:
@Cacheable可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。
特性:初次将数据存入缓存,以后从缓存读取,可以当作读取操作
对于使用@Cacheable标注的方法,Spring在每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回(不需要再次执行该方法);
否则才会执行并将返回结果存入指定的缓存中。
Spring在缓存方法的返回值时是以键值对进行缓存的;
值:就是方法的返回结果;
键:支持两种策略,默认策略和自定义策略。
自定义策略是指我们可以通过Spring的EL表达式来指定我们的key。这里的EL表达式可以使用方法参数及它们对应的属性。使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。
注意:当一个支持缓存的方法在对象内部被调用时是不会触发缓存功能的。
可用属性:
- value/cacheNames:两个等同的参数(cacheNames为Spring 4新增,作为value的别名),用于指定缓存存储的集合名。由于Spring 4中新增了@CacheConfig,因此在Spring 3中原本必须有的value属性,也成为非必需项了
- key:缓存对象存储在Map集合中的key值,非必需,缺省按照函数的所有参数组合作为key值,若自己配置需使用SpEL表达式,比如:@Cacheable(key = "#p0"):使用函数第一个参数作为缓存的key值,更多关于SpEL表达式的详细内容可参考官方文档
- condition:缓存对象的条件,非必需,也需使用SpEL表达式,只有满足表达式条件的内容才会被缓存,比如:@Cacheable(key = "#p0", condition = "#p0.length() < 3"),表示只有当第一个参数的长度小于3的时候才会被缓存。
- unless:另外一个缓存条件参数,非必需,需使用SpEL表达式。它不同于condition参数的地方在于它的判断时机,该条件是在函数被调用之后才做判断的,所以它可以通过对result进行判断。
- keyGenerator:用于指定key生成器,非必需。若需要指定一个自定义的key生成器,我们需要去实现org.springframework.cache.interceptor.KeyGenerator接口,并使用该参数来指定。需要注意的是:该参数与key是互斥的
- cacheManager:用于指定使用哪个缓存管理器,非必需。只有当有多个时才需要使用
- cacheResolver:用于指定使用那个缓存解析器,非必需。需通过org.springframework.cache.interceptor.CacheResolver接口来实现自己的缓存解析器,并用该参数指定。
@Cacheable("userCache") User selectUserById(final Integer id);
@Cacheable(value={"users1", "user2"}, key="caches[1].name") public User find(User user);
@Cacheable(value={"userCache"}, key="#user.id", condition="#user.id%2==0") User getSomeUsers(User user);
@CachePut
使用范围:@CachePut也可以标注在类上和方法上;
特性:一直往缓存中存,可以当作新增或者更新操作
@CachePut也可以声明一个方法支持缓存功能;
与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
可用属性:(同上)
@CachePut(value = "userCache") User updateUserById(final Integer id);
@CacheEvict
使用范围:(同上)
当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。
特性:一直从缓存中删除,可以当作删除操作
清除缓存
可用属性:(同上)
@CacheEvict还有下面两个参数:
- allEntries:非必需,默认为false。当为true时,清除缓存中的所有元素
- beforeInvocation:非必需,默认为false,会在调用方法之后移除数据。当为true时,会在调用方法之前移除数据。
@CacheEvict(cacheNames = {"userCache"}, allEntries = true) public Integer delete(Integer id);
@CacheEvict(cacheNames = {"userCache"}, beforeInvocation = true) public Integer delete(Integer id);
@CacheEvict(value = "userCache") User deleteUserById(final Integer id);
@Caching
使用范围:(同上)
特性:可同时指定多个Spring Cache相关的注解
可用属性:
拥有三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict。
@Caching( put = { @CachePut(cacheNames = "userCache", key = "#user.id"), @CachePut(cacheNames = "userCache", key = "#user.username"), @CachePut(cacheNames = "userCache", key = "#user.age") } }
自定义注解
前提:要想使用spring缓存注解,需要开启spring缓存,否则,一切都是枉然。
开启方法:@EnableCache
在springboot的项目启动类上加上此注解即可。
3.解决方案
上面简单的使用方式,这里不在赘述,下面我们来看高级一点的用法。
方式一:提供接口
接口:
/** * 单位信息缓存 * @description: * @author: Marydon * @date: 2020-12-12 9:42 * @version: 1.0 * @email: marydon20170307@163.com */ public interface IUnitInfoCache { UNITINFO insertOrUpdateCache(String unitKey,UNITINFO unitInfo); UNITINFO getCache(String unitKey); void deleteCache(String unitKey); }
实现类:
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.Component; /** * 单位信息表缓存实现类 * @description: * @author: Marydon * @date: 2020-12-12 10:05 * @version: 1.0 * @email: marydon20170307@163.com */ @Component @CacheConfig(cacheNames = {"unitInfo2"}) public class UnitInfoCacheImpl implements IUnitInfoCache{ /* * 缓存单位信息 * @description: 存入缓存 * @attention: 在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。 * 指定key后,就不再自动将所有入参当成key的组成部分啦 * @date: 2020年12月12日 0012 9:53 * @param: unitKey 作为缓存的主键 * @param: unitInfo 作为缓存的值 * @return: UNITINFO * 在这里添加返回值的目的不是为了调用返回值,因为我们把它作为入参传进来了,所以,看似是画蛇添足; * 实际上是因为@CachePut注解会将返回值作为value存入缓存中,所以返回值不能改成void。 */ @CachePut(key = "#unitKey", unless = "#result == null") @Override public UNITINFO insertOrUpdateCache(String unitKey, UNITINFO unitInfo) { return unitInfo; } /* * 获取单位信息 * @description: 从缓存中取 * @attention: 对于使用@Cacheable标注的方法,Spring在每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。 * @date: 2020年12月12日 0012 9:55 * @param: unitKey * @return: UNITINFO * 缓存中有就取,没有就执行方法并返回null。 */ @Cacheable(key = "#unitKey", unless = "#result == null") @Override public UNITINFO getCache(String unitKey) { // 缓存中没有对应的可以时,会进来 return null; } /* * 删除单位信息 * @description: 从缓存中删除 * @attention: * @date: 2020年12月12日 0012 9:56 * @param: unitKey * @return: void */ @CacheEvict(key = "#unitKey") @Override public void deleteCache(String unitKey) { // 不需要内部实现,@CacheEvict会自动将入参作为key进行移除 } }
方式二:提供父类
父类:
import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; /** * 缓存操作父类 * @description: 使用这种方式虽然可以实现多个子类共享方法, * 但是,这些子类需要共用一个cacheName,如果不想共用缓存名称,那就只能重写这些方法 * @author: Marydon * @date: 2020-12-12 11:04 * @version: 1.0 * @email: marydon20170307@163.com */ @CacheConfig(cacheNames = {"myCache"}) public abstract class CacheAbstractParent<T> { /* * 存入或更新指定缓存 * @description: * @attention: 在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。 * 指定key后,就不再自动将所有入参当成key的组成部分啦 * @date: 2020年12月12日 0012 9:53 * @param: key 作为缓存的主键 * @param: javaBean 作为缓存的值 * @return: java对象 * 在这里添加返回值的目的不是为了调用返回值,因为我们把它作为入参传进来了,所以,看似是画蛇添足; * 实际上是因为@CachePut注解会将返回值作为value存入缓存中,所以返回值不能改成void。 */ @CachePut(key = "#key", unless = "#result == null") public T insertOrUpdateCache(String key, T value) { return value; } /* * 获取单位信息 * @description: 从缓存中取 * @attention: 对于使用@Cacheable标注的方法,Spring在每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。 * @date: 2020年12月12日 0012 9:55 * @param: key * @return: java对象或null * 缓存中有就取,没有;就执行方法并返回null。 */ @Cacheable(key = "#key", unless = "#result == null") public T getCache(String key) { // 缓存中没有对应的可以时,会进来 return null; } /* * 删除单位信息 * @description: 从缓存中删除 * @attention: * @date: 2020年12月12日 0012 9:56 * @param: key * @return: 无返回值 */ @CacheEvict(key = "#key") public void deleteCache(String key) { // 不需要内部实现,@CacheEvict会自动将入参作为key进行移除 } }
子类:
import org.springframework.cache.annotation.CacheConfig; import org.springframework.stereotype.Component; /** * 单位信息表缓存 * @description: 在这里我们只需要定义好缓存的名称,以及要被缓存的对象就可以啦 * 缓存的操作:增、删、改、查,父类已经实现过了。 * 这样,我们就可以无限扩展N个缓存实体类 * @author: Marydon * @date: 2020-12-12 11:14 * @version: 1.0 * @email: marydon20170307@163.com */ @Component // 这里配置缓存名称无效 // @CacheConfig(cacheNames = {"unitCache"}) public class UnitInfoCache extends CacheAbstractParent<UNITINFO>{ // 继承了父类所有操作缓存的方法 }
4.调用及测试
在需要缓存的地方,注入缓存对象即可。比如:
调用
如果你开启了redis缓存,那我们可以看到具体的存储数据:
结果会形如这个样子。
如何才能从缓存中取出来呢?
第一步:在使用缓存的地方注入缓存管理对象
第二步:取值
一开始,我以为通过这种方式能取到值,事实证明我错了,无法直接通过key拿到value。
然后,按照redis的存储方式取值也拿不到
要想获得value,在调用get方法时,需要指定value的返回值类型,或者说用什么样的数据类型来接收返回值。
控制台输出结果:
这次,我们就可以拿到value啦。
5.缓存策略
如果缓存满了,从缓存中移除数据的策略,常见的有FIFO, LRU 、LFU。
- FIFO (First in First Out) 先进先出策略,即先放入缓存的数据先被移除
- LRU (Least Recently Used) 最久未使用策略, 即使用时间距离现在最久的那个数据被移除
- LFU (Least Frequently Used) 最少使用策略,即一定时间内使用次数(频率)最少的那个数据被移除
- TTL(Time To Live)存活期,即从缓存中创建时间点开始至到期的一个时间段(不管在这个时间段内有没被访问过都将过期)
- TTI (Time To Idle)空闲期,即一个数据多久没有被访问就从缓存中移除的时间。
6.缓存管理器
通过@EnableCaching注解自动化配置合适的缓存管理器(CacheManager),Spring Boot根据下面的顺序去侦测缓存提供者:
- Generic
- JCache (JSR-107)
- EhCache 2.x
- Hazelcast
- Infinispan
- Redis
- Guava
- Simple
可以通过配置属性spring.cache.type来强制指定,即:
spring.cache.type=Generic
2021-04-14
关于缓存条件参数的补充
unless = "#result == null and #result != ''",表示的含义是:当满足该条件时,不进行缓存(当方法返回值为空时,不将key添加到缓存当中);
condition = "#myKey != null and #myKey != ''",表示的含义是:当满足该条件时,才会进行缓存(当作为缓存主键的参数不为空时,才将该key-value存入缓存)