首页 专题 文章 代码 归档
SpringBoot开启缓存
2020.04.08 12:55 2020.04.09 10:23

1. 缓存

一个程序的瓶颈通常都在数据库,很多场景需要获取相同的数据。比如网站页面数据等,需要一次次的请求数据库,导致大部分时间都浪费在数据库查询和方法调用上,这时就可以利用到缓存来缓解这个问题。

2. 步骤

2.1. 加入依赖

加入缓存组件的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

2.2. Enble

在启动类上加入@EnableCaching注解;

然后在需要缓存的方法上加上@Cacheable注解即可!

@Cacheable(key = "#p0.id", cacheNames = {"hello"})
public String hello(Hello hello) {
    System.out.println("从HelloService获取数据了");
    return hello.getName();
}

2.3. 验证

说一下我们的验证思路:

1、简单定义了一个bean,也即实体类:

@Data
public class Hello {
    private Long id;
    private String name;
}

2、定义了一个service类:

@Service
public class HelloService {

    @Cacheable(key = "#p0.id", cacheNames = {"hello"})
    public String hello(Hello hello) {
        System.out.println("从HelloService获取数据了");
        return hello.getName();
    }
}

在Service类中的hello方法上添加了注解,并且在方法内打印一句话;

之后我们在控制层一直调用此方法,如果此hello方法中只打印了一次HelloService,那么说明缓存有效!

"#p0"表示取第一个参数,如果参数为对象,则可以通过#p0.id获取对象的id

除了@Cacheable还有:

@CacheEvict表示删除该缓存数据

@CachePut表示修改该缓存数据

3、控制层代码:

@RestController
public class HelloController {

    @Autowired
    private HelloService helloService;

    @RequestMapping("/hello")
    public String hello() {
        Hello hello = new Hello();
        hello.setId(1L);
        hello.setName("hello方法!");
        String hello1 = helloService.hello(hello);

        return hello1;
    }
}

事实证明有效!

3. 实现原理

将方法的运行结果进行缓存;以后再要相同的数据,直接从缓存中获取,不用调用方法,CacheManager管理多个Cache组件的,对缓存的真正CRUD操作在Cache组件中,每一个缓存组件有自己唯一一个名字。


@Cacheable(key = "#p0.id", cacheNames = {"hello"})中的参数是什么意思?

先说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;
  • cacheNames:用于指定缓存组件的名称,必须指定;
  • key:缓存都是键值对形式的,所以缓存一个值需要一个键;与keyGenerator只能使用一个;
  • keyGenerator:配置键的生成策略,与key只能存在一个;
  • cacheManager:缓存组件
  • condition:定义缓存条件(满足条件的才缓存)
  • unless:否定缓存;当unless指定的条件为true,方法的返回值就不会被缓存;可以获取到结果进行判断
  • sync:是否同步;

缓存流程:

1、方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定的名字获取,(CacheManager先获取相应的缓存),第一次获取缓存如果没有Cache组件会自动创建; 2、去Cache中查找缓存的内容:

  • 指定了key,那么以key去寻找;
  • 未指定key,且未指定keyGenerator,那么久以默认的KeyGenerator去生成key(使用实现类SimpleKeyGenerator
  • 指定了keyGenerator,那么久按照keyGenerator中的规则;

3、没有查到缓存就调用目标方法,将目标方法返回的结果,放进缓存中,@Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存,如果没有就运行方法并将结果放入缓存,以后再来调用就可以直接使用缓存中的数据。

4. 使用Redis

Springboot默认使用的是ConcurrentMapCache缓存组件,实际中,我们肯定不能使用默认的这种;

使用redis等中间件效率更高!

4.1. 添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lettuce pool 缓存连接池 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

4.2. 配置

spring:
  redis:
    host: 127.0.0.1
    password:
    port: 6379
    lettuce:
      pool:
        max-active: 8 #最大连接数
        max-wait: -1 # 表示未限制
        max-idle: 8 # 接池中的最大空闲连接
        min-idle: 0 # 最小空闲链接
    timeout: 5000 # 超时时间
  cache:
    redis:
      time-to-live: 600m

注意:time-to-live支持单位,如上面的600m中的m指:分钟,也即过期时间为600分钟;600s即600s

4.3. 新建配置类

package com.misiai.embed.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    @Value("${spring.cache.redis.time-to-live}")
    private Duration timeToLive = Duration.ZERO;

    /**
     * 配置Jackson2JsonRedisSerializer序列化策略
     */
    private Jackson2JsonRedisSerializer<Object> serializer() {
        // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();

        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        // 

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        return jackson2JsonRedisSerializer;
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
        redisTemplate.setValueSerializer(serializer());

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        redisTemplate.setKeySerializer(stringRedisSerializer);

        // hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // hash的value序列化方式采用jackson
        redisTemplate.setHashValueSerializer(serializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        // 配置序列化(解决乱码的问题)
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                // 缓存有效期
                .entryTtl(timeToLive)
                // 使用StringRedisSerializer来序列化和反序列化redis的key值
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer()))
                // 禁用空值
                .disableCachingNullValues();

        return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
    }
}

如果我们有配置类了,那么可以把@EnableCaching加到该类上,不必每个注解都加到启动类上;

注意:

网上的老教程一般使用objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);来序列号数据,但是这个方法被报重大安全隐患,已被启用;

需要使用objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);来代替!

本节阅读完毕! (分享
二维码图片 扫描关注我们哟