缓存
一、JSR107简介
1、5个核心接口
- CachingProvider:定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运che行期间访问多个CachingProvider;
- CacheManager:定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有;
- Cache:一个类似Map的数据结构并临时存储以key为索引的值。一个Cache仅被一个CacheManager所拥有;
- Entry:存储在Cache中key-value对;
- Expire:每一个存储在Cache中的条目有一个定义的有效期;
2、JSR107架构图
3、JSR107引入jar包
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
</dependency>
二、Spring缓存抽象
- org.springframework.cacheCache:为缓存组件提供规范定义,包含缓存的各种集合;提供各种XXXCache的实现;
- 每次调用需要缓存功能的方法时,Spring会检查指定参数的指定目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回用户。下次调用直接从缓存中获取。
- 使用Spring缓存抽象需要关注两点:
- 确定方法需要被缓存以及他们的缓存策略;
- 从缓存中读取之前缓存存储的数据;
- 几个重要概念和注解
说明 | |
---|---|
Cache | 缓存接口、定义缓存操作。如:RedisCache |
CacheManager | 缓存管理器、管理各种缓存组件 |
@Cacheable | 主要针对方法配置,能根据方法的请求参数对其结果进行缓存 |
@CacheEvict | 清空缓存 |
@EnableCaching | 开启基于注解的缓存 |
keyGenerator | 缓存数据时key生成策略 |
serialize | 缓存数据时value序列化策略 |
三、代码演示
1、环境搭建
- 导入数据库文件,创建出department和employee表
- 创建Javabean封装数据
- 整合Mybatis操作数据库
- 配置数据源信息
- 使用注解版Mybatis:
@MapperScan指定需要扫描的mapper接口所在的包
搭建基本环境详见第六章:SpringBoot Data整合Mybatis部分
2、快速体验缓存
-
步骤:
- 开启基于注解的缓存:@EnableCaching
@MapperScan(basePackages = "cn.hymll.cache.springboot.mapper") @SpringBootApplication @EnableCaching public class Springboot01CacheApplication { public static void main(String[] args) { SpringApplication.run(Springboot01CacheApplication.class, args); } }
- 标注缓存注解即可
@Cacheable(cacheNames = "emp") public Employee getEmp(Integer id){ System.out.println("查询 " + id + "号员工"); return employeeMapper.getEmpById(id); }
- 开启基于注解的缓存:@EnableCaching
-
@Cacheable注解:
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Cacheable { @AliasFor("cacheNames") String[] value() default {}; @AliasFor("value") String[] cacheNames() default {}; String key() default ""; String keyGenerator() default ""; String cacheManager() default ""; String cacheResolver() default ""; String condition() default ""; String unless() default ""; boolean sync() default false; }
- 将方法的运行结果进行缓存;以后再要相同的数据,直接从缓存获取,不要调用方法
- CacheManager管理多个Cache组的,对缓存的真正CRUD操作在Cache组件中,每一个缓存组有自己唯一的名字
- 几个属性:
- cacheNames/value:指定缓存组件的名字;将方法的返回值放到那个缓存中,是数组的形式,可以指定多个缓存;
- key:缓存数据使用的key可以用它指定。默认使用方法参数的值。1-方法的返回值;
编写SpEL: #id;参数id的值 <==> #a0 #p0 #root.args[0] - keyGenerator:key生成器;可以指定key生成器的组件id;key/keyGenerator:二选一使用;
Key示例:key = "#root.methodName + '[' + #id + ']'"
keyGenerator:keyGenerator = "myKeyGenerator"
@Configuration public class MyCacheConfig { @Bean("myKeyGenerator") public KeyGenerator keyGenerator(){ return (target, method, params) -> method.getName() + "[" + Arrays.asList(params).toString() + "]"; } }
- cacheManager:指定缓存管理器,或者cacheResolver指定获取解析器
- condition:指定符合条件的情况下才缓存;例如:
condition = "#id > 1":第一个参数值大于1才缓存
- unless:否定缓存;当unless指定条件为true,方法的返回值就不会缓存,可以获取到结果进行判断,如:
unless = "#a0 ==2":第一个参数值是2.就不缓存
- sync:是否使用异步模式,在异步模式下,unless不支持
-
spEL表达式
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root object | 当前被调用的方法名 | #root.methodName |
method | root object | 当前被调用的方法 | #root.method.name |
target | root object | 当前被调用的目标对象 | #root.target |
targetClass | root object | 当前被调用的目标对象类 | #root.targetClass |
args | root object | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root object | 当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”,“cache2”})),则有两个cache | #root.caches[0].name |
argument name | evaluation context | 方法参数的名字,可以直接 #参数名,也可以使用#p0或#a0 的形式,0代表参数的索引; | #ban、#a0、#p0 |
result | evaluation context | 方法执行后的返回值(仅当方法执行之后的判断有效,如 ‘unless’,'cache put’的表达式 ’cache evict‘的表达式beforeInvocation=false) | #result |
-
@CachePut注解:即调用方法,又更新缓存数据;同步更新缓存
一般用于修改了数据库的某个数据,同时更新缓存;-
运行时机:
- 先调用目标方法;
- 将目标方法的结果缓存起来;
-
测试步骤:
- 查询1号员工;查到的结果放到缓存中;
key:1 value:张三
- 以后查询还是之前的结果
- 更新1号员工:
lastName:zhangsan;gender:0
将方法的返回值也放进缓存了;
key:传入的employee对象 value:返回的employee对象 - 查询1号员工?
应该是更新后的员工:
key = “#employee.id”:使用传入参数的员工id
key = “#result.id”:使用返回后的id
@Cacheable的key是不能用result
为什么是没更新前的数据?【1号员工没有在缓存中更新】
- 查询1号员工;查到的结果放到缓存中;
-
-
@CacheEvict注解: 缓存清除
- key:指定要清除的key
- allEntries:清除当前缓存所有的数据
- beforeInvocation:缓存的清除是否在方法之前执行
默认代表是在方法执行之后执行;如果出现异常缓存就不会清除
beforeInvocation = true:代表清除缓存操作是在方法之前,无论方法是否出现异常,缓存都清除
-
@Caching:定义复杂缓存规则
@Caching( cacheable = { @Cacheable(value = "emp", key = "#lastName") }, put = { @CachePut(value = "emp", key = "#result.id"), @CachePut(value = "emp", key = "#result.email") } ) public Employee getEmpByLastName(String lastName){ return employeeMapper.getEmpByLastName(lastName); }
-
@CacheConfig:抽取缓存的公共配置
@CacheConfig(cacheNames = "emp") @Service public class EmployeeService {}
3、原理:
- 自动配置类:CacheAutoConfiguration
- 缓存的配置类:
- 默认配置生效SimpleCacheConfiguration
- 给容器中注册了一个cacheManager:ConcurrentMapCacheManager
- 可以获取和创建ConcurrentMapCache类型的缓存组件,他的作用将数据保存在ConcurrentMap中
4、运行流程:@Cacheable
- 方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定名字获取;(CacheManager先获取相应的缓存),第一次获取缓存如果没有Cache组件则自动创建
- 去Cache中查找缓存的内容,使用一个key,默认就是方法参数;key是按照某种策略生成的;默认是使用keyGenerator生成的,默认使用SimplekeyGenerator生成key
- 生成key策略:
- 如果没有参数:key=new SimpleKey();
- 如果有一个参数:key=参数的值
- 如果有多个参数:key=new SimpleKey(params)
- 生成key策略:
- 没有查到缓存就调用目标方法;
- 将目标方法返回结果放进缓存
@Cacheable 标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存。如果没有就运行方法并将结果放入缓存;以后在来调用就可以直接使用缓存中数据;
5、核心:
- 使用cacheManager【ConcurrentMapCacheManager】按照名字获取Cache【ConcurrentMapCache】组件
- key是使用keyGenerator生成的,默认使用SimplekeyGenerator
四、整合redis作为缓存
默认使用的是ConcurrentMapCacheManager=ConcurrentMapCache:将数据保存在ConcurrentMap<Object,Object>中;开发中使用缓存中间件:redis、memcached、和chcache等;
1、redis安装
- redis的docker安装
#拉取redis的docker官方镜像
docker pull redis
#查看镜像ID
docker images
#启动一个redis容器
docker start -d -p 6379:6379 --name myredis imageId
注意:redis详细说明请参考相关的redis教程;
2、引入redis的starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3、SpringBoot配置redis
spring:
redis:
#配置redis的主机IP
host: 192.168.1.102
#redis如果有密码的话配置redis密码
#password: redis
#配置使用redis的哪一个数据库
database: 2
4、代码测试redis是否可以使用
- 配置自定义的RedisTemplate
@Bean
public RedisTemplate<Object, DmComponentIo> empRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, DmComponentIo> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setDefaultSerializer(new Jackson2JsonRedisSerializer<>(Employee.class));
return template;
}
- 测试代码
@SpringBootTest
class Springboot01CacheApplicationTests {
@Autowired
DmComponentIoMapper mapper;
@Autowired
StringRedisTemplate stringRedisTemplate;//操作k-v都是字符串
@Autowired
RedisTemplate<Object, Object> redisTemplate;//k-v都是对象的
@Autowired
RedisTemplate<Object, DmComponentIo> empRedisTemplate;//k-v都是对象的
/**
* Redis常见的五大数据类型
* String(字符串)、List(列表)、Set(集合)、Hash(散列)、ZSet(有序集合)
* stringRedisTemplate.opsForValue()[String(字符串)]
*/
@Test
public void TestRedis01(){
//给redis中保存了一个数据
//stringRedisTemplate.opsForValue().append("msg","hello");
final String msg = stringRedisTemplate.opsForValue().get("msg");
System.out.println("msg = " + msg);
}
/**
* 测试保存对象
*/
@Test
public void TestRedis02(){
final DmComponentIo dmComponentIo = mapper.getDmComponentIoById(15835L);
//默认如果保存对象,使用jdk序列化机制,序列化后的数据保存到redis中
//redisTemplate.opsForValue().set("emp-01",dmComponentIo);
//1.将数据以JSON的方式保存
//2、redisTemplate默认的序列化规则:改变默认的序列化规则
empRedisTemplate.opsForValue().set("emp-02",dmComponentIo);
}
}
5、测试缓存
原理:CacheManager===Cache 缓存组件老实际给缓存中存取数据
- 引入redis的starter,容器中保存的是RedisCacheManager
- RedisCacheManager帮我们创建RedisCache来作为缓存组件,RedisCache通过操作redis缓存数据
- 默认保存数据k-v都是Object;利用序列化保存,如何保存JSON
- 引入了redis的starter,CacheManager变为RedisCacheManager
- 默认创建的 RedisCacheManager 操作redis时使用的是 RedisTemplate<Object, Object>
- RedisTemplate<Object, Object>默认使用JDK序列化机制;使用其他序列化方式可以使用自定义的RedisTemplate;
- 自定义CacheManager:
缓存数据能存入redis
第二次从缓存中查询就不能反序列化回来;
存的是dept的JSON数据;CacheManager默认使用RedisTemplate<Object,Employee>操作redis@Bean("employeeCacheManager") @Primary//将某个配置作为默认使用的 public RedisCacheManager employeeCacheManager(RedisConnectionFactory redisConnectionFactory){ RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(10)) //设置过期时间 .disableCachingNullValues() //禁止缓存null对象 //* .computePrefixWith(cacheName -> "yourAppName".concat(":").concat(cacheName).concat(":"))*//* //此处定义了cache key的前缀,避免公司不同项目之间的key名称冲突 .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) //定义了key和value的序列化协议,同时hash key和hash value也被定义 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<>(Employee.class))); return RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(cacheConfiguration) .build(); }
- 使用代码的方式操作缓存
public Employee getEmp(Integer id){
System.out.println("查询 " + id + "号员工");
final Employee empById = employeeMapper.getEmpById(id);
//通过注入employeeCacheManager来操作缓存
employeeCacheManager.getCache("emp");
return empById;
}
**注意:如果使用的自定义的RedisCacheManager & RedisTemplate,只会实例化特定的Bean,这时我们可以通过`cacheManager = "employeeCacheManager"`来单独设置CacheManager;**
1. 在Service类上设置,类中所有需要缓存的方法都起作用;
```java
@CacheConfig(cacheNames = "emp",cacheManager = "employeeCacheManager")
@Service
public class EmployeeService {}
```
2. 单独某个方法上设置,只对这个方法起作用
```java
@Cacheable(cacheNames = "emp",cacheManager = "employeeCacheManager")
public Employee getEmp(Integer id){}
```