随着用户群体的扩展,系统所需要处理的数据请求将呈几何式增长,数据库很容易会因为无法处理庞大的请求而产生宕机现象,这对一个软件来说是十分可怕的,而缓存就是解决这一问题的一个方案。缓存的使用将大大提高数据库的承载能力,提高系统的承载力和安全性。
- JSR107
Java Caching定义了5个核心接口,分别是CachingProvider, CacheManager, Cache, Entry 和 Expiry。
CachingProvider定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。
CacheManager定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。
Cache是一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个CacheManager所拥有。
Entry是一个存储在Cache中的key-value对。
Expiry 每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。其中,CacheManager和Cache是最常用的。他们的关系类似于MySQL数据库中数据库和表的关系,和MySQl不同的是:Cache中存储的是key-value形式的Entry。
二、Spring缓存抽象
Spring从3.1开始定义了org.springframework.cache.Cache
和org.springframework.cache.CacheManager接口来统一不同的缓存技术;
并支持使用JCache(JSR-107)注解简化我们开发;
Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;
Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache , ConcurrentMapCache等;
每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
使用Spring缓存抽象时我们需要关注以下两点;
1、确定方法需要被缓存以及他们的缓存策略
2、从缓存中读取之前缓存存储的数据
三、几个重要概念&缓存注解
- 重要概念&缓存注解
Cache缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等CacheManager缓存管理器,管理各种缓存(Cache组件)@Cacheable主要针对方法配置,能够根据方法的请求参数对其结果进行缓存@CacheEvict清空缓存(和==@Cacheable配合使用才有意义)@CachePut保证方法被调用,又希望结果被缓存。(和@Cacheable==配合使用才有意义)@Caching组合注解,可同时使用上面三个注解。用于实现复杂的缓存策略@CacheConfig一般用在类上,抽取配置其他注解的共有属性,例如cacheNames@EnableCaching开启基于注解的缓存keyGenerator缓存数据时key生成策略serialize缓存数据时value序列化策略
四、@Cacheable用法
@Cacheable注解可以作用于方法上或类上,作用于方法上表示该方法的返回值会被缓存起来,该方法具有缓存功能,而作用于类上表示该类中的所有方法都具有缓存功能。
下面我们以作用于方法上来进行实例分析:
@Cacheable(cacheNames = {"emp","temp"} ,key="#id",condition = "#id>0",unless = "#result==null")public Employee findEmployeeById(Integer id){System.out.println("查询编号为 "+id+" 的员工!");return employeeMapper.findEmployeeById(id);}
@Cacheable几个常用属性:
cacheNames/value
缓存的名字,我们将我们的缓存数据交给缓存管理器CacheManager管理,因此我们需要指定我们缓存的名字,也就是放在哪个缓存中,这样Springboot在存取缓存中的数据时候才知道要在哪个缓存中去找。
key
这是一个非常重要的属性,表示我们缓存数据的键,默认使用参数名作为缓存的键,值就是我们方法的返回结果。
当然,key的写法还支持SPEL表达式,这里的表达式可以使用方法参数及它们对应的属性。使用方法参数时我们可以直接使用“#参数名”、“#p参数index”、“#a参数index”。“参数index” 表示参数列表中第几个参数。下面是几个使用参数作为key的示例
//使用#参数名方式指定
@Cacheable(value="users", key="#id")
public User find(Integer id) {return null;}//使用#p参数index方式,0就表示参数列表中的第一次参数@Cacheable(value="users", key="#p0")public User find(Integer id) {return null;}//参数是对象的时候,我们可以使用#对象.属性方法指定@Cacheable(value="users", key="#user.id")public User find(User user) {return null;}
//即使参数是对象,我们也能用a下标或者p下标方式获取到对应参数中对应的对象@Cacheable(value="users", key="#a0.id")public User find(User user) {return null;}
当然,我们还可以通过编写配置类方式自定义配置key的生成策略。注意包不要导错哟。
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
import java.util.Arrays;
@Configuration
public class MyCacheConfig {@Bean("myKeyGenerator")public KeyGenerator keyGenerator(){return new KeyGenerator(){@Overridepublic Object generate(Object target, Method method, Object... params) {return method.getName()+"["+ Arrays.asList(params).toString()+"]";}};}
}
在@Cacheable中使用keyGenerator属性来进行使用我们的配置key生成策略。
@Cacheable(value="emp", keyGenerator = "myKeyGenerator")
key属性和keyGenerator属性我们只能二选一。
最后,我们的Spring还提供了一个root对象可以用来生成key。通过该root对象我们可以获取到以下信息。我们来看看文档中这部分内容:
属性名称 | 描述 | 示例 |
---|---|---|
methodName | 当前方法名 | #root.methodName |
method | 当前方法 | #root.method.name |
target | 当前被调用的对象 | #root.target |
targetClass | 当前被调用的对象的class | #root.targetClass |
args | 当前方法参数组成的数组 | #root.args[0] |
caches | 当前被调用的方法使用的Cache | #root.caches[0].name |
@Cacheable(value="emp", key = "#root.args[0]")
public User getUserById(Integer id){
//根据id查询用户信息
User user=userMapper.getUserById(id);
return user;
}
注:当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。
condition
有些时候我们并不希望缓存一个方法所有的返回结果。我只需要满足条件的才进行缓存。通过condition属性可以实现这一功能。condition属性默认为空,表示将缓存所有的调用情形。其值是通过SpringEL表达式来指定的,当为true时表示进行缓存处理;当为false时表示不进行缓存处理,即每次调用该方法时该方法都会执行一次。
@Cacheable(cacheNames = {"emp","temp"} ,key="#id",condition = "#id>0")
public Employee findEmployeeById(Integer id){System.out.println("查询编号为 "+id+" 的员工!");return employeeMapper.findEmployeeById(id);
}
在上面代码中,我们使用condition属性指定id大于0的结果才进行缓存。
unless
否定缓存,当unless指定的条件为true,方法的返回值就不会被缓存;可以获取到结果进行判断。
sync
是否指定异步模式。
总结:@Cacheable标注的方法执行之前,先检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存,如果没有就运行方法并将结果放入缓存。
五、@CachePut用法
在支持Spring Cache的环境下,对于使用@Cacheable标注的方法,Spring在每次执行前都会检查Cache中是否存在相同key的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。@CachePut也可以声明一个方法支持缓存功能。与@Cacheable不同的是,使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
@CachePut(value="emp",key = "#result.id")
public Employee updateEmployee(Employee employee){System.out.println("updateEmp:"+employee);employeeMapper.updateEmployee(employee);return employee;
}
使用result表示方法的返回结果,这是为了同步查询功能。
也可以使用参数列表:key="#employee.id"进行同步更新缓存功能。
解释如下:
1、查询编号为1的员工信息,放入缓存emp中。
2、更新编号为1的员工信息,此时的key如果和查询时使用的key不一样,那么,我们更新该员工之后缓存的员工信息的key值和查询缓存中的key不一样,那么我们相当于是新添加了一组缓存数据。当再次执行查询方法时候将不能得到更新之后的员工信息。
强调:@Cacheable的key是不能够使用#result.id,其实是因为第一次方法没运行之前需要先去检查缓存得到这个key,而此时缓存中并没有这个key。
@Cacheput注解一般使用在保存,更新方法中。
四、@CacheEvict注解
@CacheEvict是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。@CacheEvict可以指定的属性有value、key、condition、allEntries和beforeInvocation。其中value、key和condition的语义与@Cacheable对应的属性类似。即value表示清除操作是发生在哪些Cache上的(对应Cache的名称);key表示需要清除的是哪个key,如未指定则会使用默认策略生成的key;condition表示清除操作发生的条件。
@CacheEvict的一些属性与@Cacheable和@CachePut是相同的,另外还有几个新的属性。与@Cacheable和@CachePut不同,@CacheEvict并没有提供unless属性。
下面看一下示例:
/*** 删除员工信息的时候同时删除相应的缓存* key指定要清除的数据,allEntries表示是否删除所有缓存数据* beforeInvocation=true:当我们指定该属性值为true时,会在调用该方法之前清除缓存中的指定元素。false表示在方法执行之后执行。默认为false* @param id*/
@CacheEvict(value = "emp",key = "#id"/*,allEntries = true,beforeInvocation = false*/)
public void deleteEmployee(Integer id){System.out.println("删除编号为"+id+"的员工");employeeMapper.deleteEmployee(id);
}
六、@Caching定义复杂注解
@Caching注解可以让我们在一个方法或者类上同时指定多个Spring Cache相关的注解。其拥有三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict。
示例:
@Caching(cacheable = {@Cacheable(value = "emp",key = "#lastName")},put = {@CachePut(value = "emp",key = "#result.id"),@CachePut(value = "emp",key = "#result.email")},evict = {@CacheEvict(cacheNames = "emp",allEntries = true)}
)
public Employee findByName(String lastName){return employeeMapper.findByLastName(lastName);
}
注:@Cacheput注解一定会执行方法,也就是说第一次按照lastName查询之后,虽然缓存中存有该数据,但是查询数据库的方法一定会执行。也就是说,当我们通过名字查询数据库的时候,即使缓存中有该数据,但还是会进行数据库的查询,只因有@Cacheput存在。
七、 @CacheConfig注解
@CacheConfig用在类上,将公共缓存内容写在该注解中,方法中这些属性即可不用再写了。
@CacheConfig(value="emp")
public class EmployeeService{@Cacheable(key="#user.id")public User find(User user) {return null;}@Cacheable(key = "#root.args[0]")public User getUserById(Integer id){//根据id查询用户信息User user=userMapper.getUserById(id);return user;}
}
Cache执行流程:
1、方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定名字获取;
(Cachemanager先获取相应的缓存),第一次获取缓存如果没有缓存组件Cache会自动创建。
2、去Cache中查找缓存的内容,使用一个key,默认就是方法的参数 key是按照某种策略生成的:默认是使用keyGenerator生成的,默认使用SimpleKeyGenerator生成key,如果没有参数:key=new SimpleKey(); 如果有一个参数:key=参数的值,如果有多个参数:key=new SimpleKey(params)。
3、没有查找到缓存就调用目标方法。
4、将目标方法返回的结果放进缓存中。
八 、@Cacheable注解不生效
在方法或者类上加了@Cacheable注解,但是数据并未被缓存到Redis。先检查配置问题:
配置文件中启用缓存
cache:redis:key-prefix: 'ems:'redis:host: localhost
启动类 或者RedisCacheAutoConfiguration是否加@EnableCaching注解
再考虑代码层的问题:
缓存的对象必须实现Serializable接口
@Data
public class WorkerDTO implements Serializable {@Schema(description = "司机工号")private String workNo;@Schema(description = "司机姓名")private String workName;}
@Cacheable注解的实现是AOP,AOP又得依赖代理对象,一个方法A调同一个类里的另一个有缓存注解的方法B,此时不走缓存 (内部调用不用代理对象 => 不用代理对象调用加了注解的方法 => AOP增强后的方法调用不到 =>那就一普通方法 =>注解自然不生效)。简单说就是注解的背后基本都是AOP一堆代码,而想AOP方法,得用代理对象去调用
@Cacheable(value = WORKER, unless = "#result == null ")
public List<WorkerDTO> getWorkers() {return getBaseMapper().getWorkers();
}@CachePut(value = WORKER, unless = "#result == null ")
public List<WorkerDTO> updateWorkers() {return getBaseMapper().getWorkers();
}
参考文件:https://blog.csdn.net/dl962454/article/details/106480438