springboot 缓存
springboot与缓存
JSR-107规范
为了统一缓存的开发规范、提高系统的扩展性和最小化开发成本等,J2EE 发布了 JSR-107 缓存规范。
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 设置。
spring boot的缓存抽象
spring虽然遵循JSR-107规范,但是并没有完全保留它定义的所有接口,而是只保留了Cache和CacheManager。
Spring 从 3.1 开始定义了 org.springframework.cache.Cache和 org.springframework.cache.CacheManager接口来统一不同的缓存技术并支持使用 JCache(JSR-107)注解简化我们开发。
下面我们介绍一下springboot中缓存使用的一些类及注解
Cache | 缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等 |
---|---|
CacheManager | 缓存管理器,管理各种缓存(Cache)组件 |
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存(一般将其标注在新增方法上) |
@CacheEvict | 清空缓存(一般标注在删除方法上) |
@CachePut | 保证方法被调用,又希望结果被缓存。 (一般标注在更新操作的方法上) |
@EnableCaching | 开启基于注解的缓存(一般标注在配置类上) |
keyGenerator | 缓存key的生成策略(下面会详细描述) |
serialize | 缓存时的序列化策略 |
缓存使用案例与常用注解
- 使用idea spring初始化器创建项目,选中web/mybatis/mysql/cache
- 编写controller service
- 单元测试,我们在service的方法中标注@Cacheable注解,当每次调用方法的时候都会从缓存中获取
/**
* 将方法的运行结果进行缓存,再次运行该方法时从缓存中返回结果
* CacheManager 管理多个 Cache 组件,Cache 组件进行缓存的 CRUD,每一个缓存组件都有唯一一个名字。
* 属性:
* cacheNames/value:指定缓存组件的名字
* key:缓存数据键值对的 key,默认是方法的参数的值
* keyGenerator:key 的生成器,可以自己指定 key 的生成器的组件 id 与 key 二选一
* cacheManager:指定缓存管理器 cacheResolver:缓存解析器,二者二选一
* condition/unless(否定条件):符合指定条件的情况下才缓存
*
* @param id
* @return
*/
@Cacheable(cacheNames = {"emp"}, condition = "#id % 2 == 1")
public Employee getEmp(Integer id){
System.out.println("查询"+id+"号员工");
Employee emp = employeeMapper.getEmpById(id);
return emp;
}
缓存原理解析
根据上面的案例我们发现,首先我们标注了@cacheable注解,并且成功的将数据加入到了缓存,那么我们究竟是用的哪个缓存组件?redis?ehcache?
这里就要通过查看cacheAutoConfigure入手,主要看@Import注解,import注解的作用是导入,根据{}内的类,将类的返回值(也就是CacheType.values()
的类型的缓存自动配置类)加入到容器中,然后让这些自动配置类自动根据condition来判断自己的组件是否生效;
这个注解使用了一个ImportSelector接口的静态内部类,并调用了这个类中的方法,这个方法的作用是将所有在CacheType.values()
的数组中匹配的类型的自动配置类加入容器,然后spring boot会根据这些配置类上的condition条件注解来判断哪个缓存生效。
@Configuration
@ConditionalOnClass({CacheManager.class})
@ConditionalOnBean({CacheAspectSupport.class})
@ConditionalOnMissingBean(
value = {CacheManager.class},
name = {"cacheResolver"}
)
@EnableConfigurationProperties({CacheProperties.class})
@AutoConfigureAfter({CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class, HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class})
@Import({CacheAutoConfiguration.CacheConfigurationImportSelector.class})
public class CacheAutoConfiguration {
public CacheAutoConfiguration() {
}
....
static class CacheConfigurationImportSelector implements ImportSelector {
CacheConfigurationImportSelector() {
}
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
CacheType[] types = CacheType.values();
String[] imports = new String[types.length];
for(int i = 0; i < types.length; ++i) {
imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
}
//这就是加入到容器中的所有缓存配置类的类名
return imports;
}
}
ok,我们接着往下看,当我们知道了现在已经给容器中加入了十一个spring boot默认支持的缓存组件,那么到底是哪个组件生效了呢?有两种方法:
- 通过分析类头,也就是标注在配置类上面的conditional注解来分析
- 通过application配置文件debug=true让程序启动时打印自动配置报告 ,在这个报告中就可以看到有哪些组件生效了
类头我们就 不在细说,这里通过打印自动配置报告发现,当我们没有加入任何缓存配置的时候,spring boot默认使用的就是SimpleCacheConfiguration,那么这个类干了什么呢?
@Configuration
@ConditionalOnMissingBean({CacheManager.class})
@Conditional({CacheCondition.class})
class SimpleCacheConfiguration {
private final CacheProperties cacheProperties;
private final CacheManagerCustomizers customizerInvoker;
SimpleCacheConfiguration(CacheProperties cacheProperties, CacheManagerCustomizers customizerInvoker) {
this.cacheProperties = cacheProperties;
this.customizerInvoker = customizerInvoker;
}
@Bean
public ConcurrentMapCacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
cacheManager.setCacheNames(cacheNames);
}
return (ConcurrentMapCacheManager)this.customizerInvoker.customize(cacheManager);
}
}
通过上面的代码我们可以发现,这个配置类给容器中增加了一个ConcurrentMapCacheManager这个类,通过查看代码我们发现这个类时一个Cache Manager的实现类,那么一定实现了接口中定义的getCache方法,这个方法就是根据缓存的名字获取缓存对象。我们再来看一下这个getCache方法的实现
public interface CacheManager {
@Nullable
Cache getCache(String var1);
Collection<String> getCacheNames();
}
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap(16);
@Nullable
public Cache getCache(String name) {
//从cacheMap中根据名字获取缓存,如果不存在就去创建一个,这个cacheMap就是一个map,
Cache cache = (Cache)this.cacheMap.get(name);
if (cache == null && this.dynamic) {
synchronized(this.cacheMap) {
cache = (Cache)this.cacheMap.get(name);
if (cache == null) {
cache = this.createConcurrentMapCache(name);
this.cacheMap.put(name, cache);
}
}
}
return cache;
}
//创建了一个ConcurrentMapCache
protected Cache createConcurrentMapCache(String name) {
SerializationDelegate actualSerialization = this.isStoreByValue() ? this.serialization : null;
return new ConcurrentMapCache(name, new ConcurrentHashMap(256), this.isAllowNullValues(), actualSerialization);
}
通过上面的代码我们发现,这个ConcurrentMapCacheManager的缓存管理器的作用就是可以获取或者创建一个ConcurrentMapCache缓存对象,进入这个对象内后我们发现这个类中定义了一些缓存的增删改方法,例如put/lookup/clear/evict等等,我们进入lookup方法中看
@Nullable
protected Object lookup(Object key) {
return this.store.get(key);
}
可以发现,就是根据key从store中拿数据,这个store是什么?就是一个CurrentMap;
总结:
这里只是例举了spring boot默认生效的缓存,也就是Cache 接口的默认实现ConcurrentMapCache和CacheManager接口的默认实现ConcurrentMapCacheManager,那么其他缓存组件也是一样的,一定会有例如RedisConfiguration,RedisCacheManager,RedisCache等等
spring boot集成redis
当我们没有指定其他缓存配置时,springboot默认使用currentMapCache,但是在开发中一般使用的其他的缓存中间件
整合步骤
- 引入依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
- 配置redis,在application.yml配置文件中加入配置。当我们引入 了上面的依赖后,RedisAutoConfig自动配置类就生效了,那么这个RedisAutoConfig做了什么呢?接着往下看
spring:
redis:
host: 127.0.0.1
port: 6379
# password: 123456
jedis:
pool:
max-active: 8
max-wait: -1
max-idle: 500
min-idle: 0
lettuce:
shutdown-timeout: 0
可配置的项在RedisProperties类中
@ConfigurationProperties(
prefix = "spring.redis"
)
public class RedisProperties {
private int database = 0;
private String url;
private String host = "localhost";
private String password;
private int port = 6379;
private boolean ssl;
private Duration timeout;
private RedisProperties.Sentinel sentinel;
private RedisProperties.Cluster cluster;
private final RedisProperties.Jedis jedis = new RedisProperties.Jedis();
private final RedisProperties.Lettuce lettuce = new RedisProperties.Lettuce();
- 配置类中除了添加链接工厂之类的方法,主要的还有两个,就是给容器中添加了两个类,分别是RedisTemplate和StringRedisTemplate,这两个类就是用来操作redis缓存的类,类似以前的jdbcTemplate,这两个类的区别就是,一个是操作字符串,一个是操作对象。如下:
@Configuration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
@Cacheable注解使用的是RedisTemplate,那么分析一下RedisTemplate
序列化配置
下面这个序列化配置并不是完美的,具体情况还要再研究。
@Configuration
@EnableCaching
public class MyRedisConfig {
@Bean
public KeyGenerator keyGenerator(){
return new KeyGenerator(){
/**
* 自定义keyGenerator
new一个keyGenerator,这是一个接口,我们实现他的抽象发方法,这个方法的参数分别是目标对象,目标方法,和参数列表
当我们使用@bean注解将该对象加入容器中后,在使用的时候,bean的默认id就是类名首字母小写
*/
@Override
public Object generate(Object o, Method method, Object... objects) {
return method.getName()+"["+ Arrays.asList(objects).toString() +"]";;
}
};
}
/**
* @bean name属性
* 如果未指定,则bean的名称是带注解方法的名称。
* 如果指定了,方法的名称就会忽略,如果没有其他属性声明的话,bean的名称和别名可能通过value属性配置
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
//使用json的方式序列化所有对象
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
//创建string的序列化器
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//设置所有的key都使用string类型的序列化器
template.setKeySerializer(stringRedisSerializer);
//设置所有的hash类型的key也是用string类型的序列化器
template.setHashKeySerializer(stringRedisSerializer);
//设置hash类型的value使用json序列化器
template.setHashValueSerializer(jackson2JsonRedisSerializer);
//设置所有的value都使用json序列化器
template.setValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
//给redisCacheManager设置序列化方式,这种方式
//当有多个缓存管理器的时候,需要加上@Primary注解来指定一个默认的缓存管理器
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer(Object.class)))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
; //使用 Jackson2JsonRedisSerialize
RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.transactionAware()
.build();
return redisCacheManager;
}
}
五大数据类型操作测试
缓存注解测试
-
环境
- spring boot
- mybatis
- redis
-
controller
@RestController
public class EmpController {
@Autowired
private EmpService empService;
@GetMapping("/hello")
public String hello(){
return "hello";
}
@GetMapping("/emp/{id}")
public Employee getEmp(@PathVariable("id") Integer id){
System.out.println("----- emp get by id-----");
Employee emp = empService.getEmpById(id);
return emp;
}
@PostMapping("/emp")
public Map<String,Object> save(Employee employee){
System.out.println(employee);
empService.saveEmp(employee);
return new HashMap<>();
}
//redis缓存注解使用测试
@GetMapping("/hash")
public String hash(){
empService.hash();
return "缓存保存has成功";
}
//加入缓存集合
@GetMapping("/selectEmpList")
public String selectEmpList(){
empService.selectEmpList();
return "缓存保存List成功";
}
//更新缓存集合
@GetMapping("/updateEmpList")
public String updateEmpList(@RequestParam("lastName") String lastName,@RequestParam("id") Integer id){
empService.updateEmpList(id,lastName);
return "缓存更新List成功";
}
//删除缓存集合
@GetMapping("/removeEmpList")
public String removeEmpList(){
empService.removeEmpList();
return "缓存删除成功成功";
}
}
- service
@Service
public class EmpServiceImpl extends ServiceImpl<BaseMapper<Employee>,Employee> implements EmpService{
private static final Logger logger = LoggerFactory.getLogger(EmpServiceImpl.class);
@Autowired
EmpMapper empMapper;
@Autowired
private RedisTemplate redisTemplate;
/**
* 1、首先要在springbootapplication类上redis配置类上添加@enableCache注解,表示启用缓存
* @Cacheable注解属性
* 1. cacheName或value 表示指定缓存的名字
* 2. key 默认是参数值,指定可以使用spel表达式
* - 如果我们想使用方法名+[参数值] 这种方式:key=“#root.methodName+'['+#id+']'”
* - 需要注意的是,不能使用#result.id 这种方式来指定key,原因是因为cacheable和cacheput的调用时机不同,cacheable要先根据key检查缓存,所以在这个时候无法获取到result,而cacheput是先执行方法,然后将方法的返回值放进缓存,因此可以使用result获取
* 3. keyGenerator 上面的自动配置类中有自定义的生成器
* - keyGenrator = “bean 的id”
* key和keyGenerator 二选一使用
* 4. cacheManager 指定缓存管理器,或者cacheResolver 指定获取解析器
* 5. condition 指定符合体哦阿健的情况下才能缓存
* 例如:condition=‘#id>0’ 表示当id大于0的时候才缓存
* condition=“#a0 > 1 ” 表示第一个参数大于1才缓存
* 多个条件(使用spel表达式):condition="#a0 > 1 and #root.methodName eq 'aaa'"
* 6.unless 除非的意思,例如condition=“#id>0 ” unless='id==10' 表示当id大于0时缓存,除非id==10,如果等于10则不缓存
* 7. sync:是否异步,使用异步则unless不生效
*/
@Cacheable(value = "emp",key = "#id")
public Employee getEmpById(Integer id){
Employee emp = empMapper.getEmp(id);
System.out.println(emp);
return emp;
}
@Override
public void saveEmp(Employee employee) {
empMapper.insert(employee);
}
/**
* 加入缓存
* @return
*/
@Cacheable(cacheNames = "empList", key = "11")
@Override
public List<Employee> selectEmpList() {
logger.info("使用@cacheable 将list加入redis缓存");
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("gender",0);
return super.list(queryWrapper);
}
/**
* 更新缓存
* @CachePut() 和 @Cacheable() 注解的方法返回值要一致
* 作用:即调用方法又更新缓存,先调用目标方法,然后将目标方法的结果缓存起来
* 要指定更新的缓存名字和key,如果没有,则新建
*/
@CachePut(cacheNames = "empList" , key = "11")
@Override
public List<Employee> updateEmpList(Integer id ,String lastName ){
Employee empById = getEmpById(id);
empById.setLastName(lastName);
updateById(empById);
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("gender",0);
return super.list(queryWrapper);
}
/**
* 缓存清除
* 不写key的情况下也是默认使用参数值
* allEntries:是否删除empList缓存中的所有数据,默认false
* beforeInvocation:缓存的清除是否在方法之前执行,默认false 在方法之后执行。之前与之后有什么区别?如果是在之前,则不论方法是否有异常,都先清除。如果在方法之后,则抛出异常后就无法删除缓存了
*/
@Override
@CacheEvict(cacheNames = "empList",key = "11")
public void removeEmpList(){
}
@Override
public void hash() {
logger.info("使用redisTemplate 操作hash类型缓存");
HashOperations hashOperations = redisTemplate.opsForHash();
Map<String,Object> map = new HashMap<>();
map.put("hash1",getEmpById(1));
map.put("hash2",getEmpById(2));
hashOperations.putAll("redis_hash",map);
}
}
@Cacheing和@CacheConfig注解
是一个组合注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
测试
@service
@CacheConfig(cacheNames = "emp")//作用就是抽取公共的缓存属性
public class service{
//定义复杂缓存规则
@Caching(
cacheable = {
@Cacheable(cacheNames = "empList")
},
put = {
//数组形式,可以指定多个规则,例如下面,就是不仅根据id缓存数据,还要根据email缓存数据
//由于cacheable注解,因此在缓存中有key=lastName的数据,但是如果我们一直用lastName查询,则每次都会查询数据库
//这是因为这个方法上还有cachePut注解,这个注解会保证每次方法都调用
@CachePut(cacheNames = "empList",key = "#result.id"),
@CachePut(cacheNames = "empList",key = "#result.email")
},
evict = {
@CacheEvict()
}
)
@Override
public Employee getEmpByLastName(String lastName){
return empMapper.getEmpByLastName(lastName);
}
}