Springboot缓存实例

Spring框架支持透明地向应用程序添加缓存对缓存进行管理,其管理缓存的核心是将缓存应用于操作数据的方法,从而减少操作数据的执行次数,同时不会对程序本身造成任何干扰。
Spring Boot继承了Spring框架的缓存管理功能,通过使用 @EnableCaching 注解开启基于注解的缓存支持,Spring Boot就可以启动缓存管理的自动化配置。

基础环境搭建:

Spring boot版本:2.7.6

持久层:Spring Data Jpa

数据库:mysql 5.7

redis:latest

maven:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
​
    <dependency>
       <groupId>com.mysql</groupId>
       <artifactId>mysql-connector-j</artifactId>
       <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

application.properties

# MySQL数据库连接配置
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=root
#显示使用JPA进行数据库查询的SQL语句
spring.jpa.show-sql=true
#开启驼峰命名匹配映射
mybatis.configuration.map-underscore-to-camel-case=true
#解决乱码
server.servlet.encoding.force-response=true

pojo

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "tb_resume")
public class Resume {
​
    /**
     * GenerationType.SEQUENCE:依靠序列来产⽣主键 Oracle
     * GenerationType.IDENTITY:依赖数据库中主键⾃增功能 Mysql
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;
​
    @Column(name = "address")
    private String address;
​
    @Column(name = "name")
    private String name;
​
    @Column(name = "phone")
    private String phone;
​
}

repository

public interface ResumeRepository extends JpaRepository<Resume,Long> {
​
    //根据id修改人物
    @Query(value = "update t_resume t set t.name = ?1 where t.id = ?2",nativeQuery = true)
    int updateResume(String name,Long id);
}

service

@Service
public class ResumeService {
​
    @Autowired
    private ResumeRepository repository;
​
    public Resume getResumeById(Long id){
        Resume resume = repository.findById(id).get();
        System.out.println(resume);
​
        return resume;
    }
}

controller

@RestController
public class ResumeController {
​
    @Autowired
    private ResumeService service;
​
    @RequestMapping("/getResumeById")
    public Resume getResumeById(Long id){
        Resume resume = service.getResumeById(id);
        return resume;
    }
}

测试结果

Hibernate: select resume0_.id as id1_0_0_, resume0_.address as address2_0_0_, resume0_.name as name3_0_0_, resume0_.phone as phone4_0_0_ from tb_resume resume0_ where resume0_.id=?
Resume(id=1, address=北京, name=刘二, phone=010100000)

改造Spring boot默认缓存

在前面搭建的Web应用基础上,开启Spring Boot默认支持的缓存,体验Spring Boot默认缓存的使用效果

使用 @EnableCaching 注解开启基于注解的缓存支持

@EnableCaching //开启spring基于注解缓存管理支持
@SpringBootApplication
public class SpringbootCacheApplication {
​
    public static void main(String[] args) {
        SpringApplication.run(SpringbootCacheApplication.class, args);
    }
​
}

使用 @Cacheable 注解对数据操作方法进行缓存管理。将 @Cacheable 注解标注在Service类的查 询方法上,对查询结果进行缓存

@Service
public class ResumeService {
​
    @Autowired
    private ResumeRepository repository;
​
    //@Cacheable 该注解作用就是将查询结果存放到springboot默认缓存中
    //cacheNames 起一个缓存命名空间  对应缓存唯一标识
    @Cacheable(cacheNames = "resume")
    public Resume getResumeById(Long id){
        Resume resume = repository.findById(id).get();
        System.out.println(resume);
​
        return resume;
    }
}

测试结果,除了第一次点击后台进行数据库访问外,后面的每一次调用都是直接从Springboot缓存中获取数据。

简单分析一下原理:

  • 底层结构:在诸多的缓存自动配置类中, SpringBoot默认装配的是 SimpleCacheConfiguration 他使用的 CacheManagerConcurrentMapCacheManager ,使用 ConcurrentMap 当底层的数据 结构,按照Cache的名字查询出Cache, 每一个Cache中存在多个k-v键值对,缓存值


@EnableCaching注解解析

@EnableCaching 是由spring框架提供的,Springboot 框架对该注解进行了继承,该注解需要配置在类上(通常配置在项目启动类上),用于开启基于注解的缓存支持

@Cacheable注解解析

@Cacheable 注解也是由spring框架提供的,可以作用于类或方法(通常用在数据查询方法上),用于 对方法结果进行缓存存储。注解的执行顺序是,先进行缓存查询,如果为空则进行方法查询,并将结果 进行缓存;如果缓存中有数据,不进行方法查询,而是直接使用缓存数据

@Cacheable 注解提供了多个属性,用于对缓存存储进行相关配置

属性名 说明
value/cacheNames 指定缓存空间的名称,必配属性。这两个属性二选一使用
key 指定缓存数据的key,默认使用方法参数值,可以使用SpEL表达式
keyGenerator 指定缓存数据的key的生成器,与key属性二选一使用
cacheManager 指定缓存管理器
cacheResolver 指定缓存解析器,与cacheManager属性二选一使用
condition 指定在符合某条件下,进行数据缓存
unless 指定在符合某条件下,不进行数据缓存
sync 指定是否使用异步缓存。默认false

执行流程&时机

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

去Cache中查找缓存的内容,使用一个key,默认就是方法的参数,如果多个参数或者没有参数,是按 照某种策略生成的,默认是使用 KeyGenerator 生成的,使用 SimpleKeyGenerator 生成 keySimpleKeyGenerator 生成 key 的默认策略:

参数个数 key
没有参数 new SimpleKey()
有一个参数 参数值
多个参数 new SimpleKey(params) (params:所有参数集合)

常用的SPEL表达式

描述 示例
当前被调用的方法名 #root.mathodName
当前被调用的方法 #root.mathod
当前被调用的目标对象 #root.target
当前被调用的目标对象类 #root.targetClass
当前被调用的方法的参数列表 #root.args[0] 第一个参数, #root.args[1] 第二个参数...
根据参数名字取出值 #参数名, 也可以使用 #p0 #a0 0是参数的下标索引
当前方法的返回值 #result

@CachePut注解

目标方法执行完之后生效, @CachePut 被使用于修改操作比较多,哪怕缓存中已经存在目标值了,但是这个 注解保证这个方法依然会执行,执行之后的结果被保存在缓存中。

@CachePut注解也提供了多个属性,这些属性与@Cacheable注解的属性完全相同。

更新操作,前端会把 id+ 实体传递到后端使用,我们就直接指定方法的返回值从新存进缓存时的 key="#id" , 如果前端只是给了实体,我们就使用 key="#实体.id" 获取 key. 同时,他的执行时机是目标 方法结束后执行, 所以也可以使用 key="#result.id" , 拿出返回值的id;

@CacheEvict注解

@CacheEvict 注解是由Spring框架提供的,可以作用于类或方法(通常用在数据删除方法上),该注解 的作用是删除缓存数据。@CacheEvict 注解的默认执行顺序是,先进行方法调用,然后将缓存进行清 除。


Spring Boot整合Redis缓存实现

在Spring Boot默认缓存管理的基础上引入Redis 缓存组件,使用基于注解的方式整合Redis缓存的具体实现;

pom.xml

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

当我们添加进 redis 相关的启动器之后, SpringBoot 会使用 RedisCacheConfigratioin 当做生效的自动配置类进行缓存相关的自动装配,容器中使用的缓存管理器是 RedisCacheManager , 这个缓存管理器创建的 Cache 为 RedisCache , 进而操控 redis 进行数据的缓存

Redis服务连接配置

# Redis服务地址
spring.redis.host=192.168.192.200
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=wsy@123

Service进行改造,使用@Cacheable、@CachePut、@CacheEvict三个注 解定制缓存管理,分别进行缓存存储、缓存更新和缓存删除

@Service
public class ResumeService {
​
    @Autowired
    private ResumeRepository repository;
​
    /**
     * select
     * @param id
     * @return
     */
    //@Cacheable 该注解作用就是将查询结果存放到springboot默认缓存中
    //cacheNames 起一个缓存命名空间  对应缓存唯一标识
    //value 缓存结果,key:默认在只有一个参数的情况下,key值默认就是方法参数值,如果没有参数或者多个参数的情况下,由SimpleKeyGenerator生成key
    //unless 指定在符合某条件下,不进行数据缓存
    @Cacheable(cacheNames = "resume",unless = "#result==null")
    public Resume getResumeById(Long id){
        Resume resume = repository.findById(id).get();
        System.out.println(resume);
​
        return resume;
    }
​
    /**
     * update
     * @param resume
     * @return
     */
    @CachePut(cacheNames = "resume",key = "#result.id")
    public Resume updataResume(Resume resume){
        repository.updateResume(resume.getName(), resume.getId());
        return resume;
    }
​
    /**
     * delete
     * @param id
     */
    @CacheEvict(cacheNames = "resume")
    public void deleteResume(Long id){
        repository.deleteById(id);
    }
}

controller

@RestController
public class ResumeController {
​
    @Autowired
    private ResumeService service;
​
    @RequestMapping("/getResumeById")
    public Resume getResumeById(Long id){
        Resume resume = service.getResumeById(id);
        return resume;
    }
​
    @RequestMapping("/updateResume")
    public Resume updateResume(Resume resume){
        Resume resume1 = service.getResumeById(resume.getId());
        resume1.setName(resume.getName());
        Resume resume2 = service.updataResume(resume1);
        return resume2;
    }
​
    @RequestMapping("/deleteResume")
    public void deleteResume(Long id){
        service.deleteResume(id);
    }
}

运行getResumeById 测试结果:

运动deleteResume 测试结果:


基于API的Redis缓存实现

ApiResumeService

@Service
public class ApiResumeService {
​
    @Autowired
    private ResumeRepository repository;
​
    @Autowired
    private RedisTemplate redisTemplate;
​
    /**
     * select
     * @param id
     * @return
     */
    public Resume getResumeById(Long id){
        Object o = redisTemplate.opsForValue().get("resume_" + id);
        if(o != null) {
            return (Resume) o;
        } else {
            Resume resume = repository.findById(id).get();
            //将查询结果存到缓存中,同时还可以设置有效期为1天
            redisTemplate.opsForValue().set("resume_" + id,resume,1, TimeUnit.DAYS);
            System.out.println(resume);
            return resume;
        }
    }
​
    /**
     * update
     * @param resume
     * @return
     */
    public Resume updataResume(Resume resume){
        repository.updateResume(resume.getName(), resume.getId());
        redisTemplate.opsForValue().set("resume_" + resume.getId(),resume);
        return resume;
    }
​
    /**
     * delete
     * @param id
     */
    public void deleteResume(Long id){
        repository.deleteById(id);
        redisTemplate.delete("resume_"+id);
    }
}

ApiResumeController

@RestController
@RequestMapping("/api")
public class ApiResumeController {
​
    @Autowired
    private ApiResumeService service;
​
    @RequestMapping("/getResumeById")
    public Resume getResumeById(Long id){
        Resume resume = service.getResumeById(id);
        return resume;
    }
​
    @RequestMapping("/updateResume")
    public Resume updateResume(Resume resume){
        Resume resume1 = service.getResumeById(resume.getId());
        resume1.setName(resume.getName());
        Resume resume2 = service.updataResume(resume1);
        return resume2;
    }
​
    @RequestMapping("/deleteResume")
    public void deleteResume(Long id){
        service.deleteResume(id);
    }
}

测试结果与上面无意;


自定义Redis缓存序列化机制

  1. Redis API默认序列化机制

    基于API的Redis缓存实现是使用RedisTemplate模板进行数据缓存操作的,这里打开 RedisTemplate类,查看该类的源码信息

    public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
        // 声明了key、value的各种序列化方式,初始值为空
        private boolean enableTransactionSupport = false;
        private boolean exposeConnection = false;
        private boolean initialized = false;
        private boolean enableDefaultSerializer = true;
        @Nullable
        private RedisSerializer<?> defaultSerializer;
        @Nullable
        private ClassLoader classLoader;
        @Nullable
        private RedisSerializer keySerializer = null;
        @Nullable
        private RedisSerializer valueSerializer = null;
        @Nullable
        private RedisSerializer hashKeySerializer = null;
        @Nullable
        private RedisSerializer hashValueSerializer = null;
        private RedisSerializer<String> stringSerializer = RedisSerializer.string();
        @Nullable
        private ScriptExecutor<K> scriptExecutor;
        private final ValueOperations<K, V> valueOps = new DefaultValueOperations(this);
        private final ListOperations<K, V> listOps = new DefaultListOperations(this);
        private final SetOperations<K, V> setOps = new DefaultSetOperations(this);
        private final StreamOperations<K, ?, ?> streamOps = new DefaultStreamOperations(this, ObjectHashMapper.getSharedInstance());
        private final ZSetOperations<K, V> zSetOps = new DefaultZSetOperations(this);
        private final GeoOperations<K, V> geoOps = new DefaultGeoOperations(this);
        private final HyperLogLogOperations<K, V> hllOps = new DefaultHyperLogLogOperations(this);
        private final ClusterOperations<K, V> clusterOps = new DefaultClusterOperations(this);
    ​
        public RedisTemplate() {
        }
        
        // 进行默认序列化方式设置,设置为JDK序列化方式
        public void afterPropertiesSet() {
            super.afterPropertiesSet();
            boolean defaultUsed = false;
            if (this.defaultSerializer == null) {
                this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader());
            }
        。。。。。。

    从上述 RedisTemplate 核心源码可以看出,在 RedisTemplate 内部声明了缓存数据key、value的各 种序列化方式,且初始值都为空;在afterPropertiesSet() 方法中,判断如果默认序列化参数 defaultSerializer 为空,将数据的默认序列化方式设置为 JdkSerializationRedisSerializer

    根据上述源码信息的分析,可以得到以下两个重要的结论:

    1, 使用RedisTemplate进行Redis数据缓存操作时,内部默认使用的是 JdkSerializationRedisSerializer序列化方式,所以进行数据缓存的实体类必须实现JDK自带的序列化接 口(例如Serializable);

    2, 使用RedisTemplate进行Redis数据缓存操作时,如果自定义了缓存序列化方式 defaultSerializer,那么将使用自定义的序列化方式。

  2. 自定义RedisTemplate序列化机制

    在项目中引入Redis依赖后,Spring Boot提供的RedisAutoConfiguration自动配置会生效。打开 RedisAutoConfiguration类,查看内部源码中关于RedisTemplate的定义方式

    @AutoConfiguration
    @ConditionalOnClass({RedisOperations.class})
    @EnableConfigurationProperties({RedisProperties.class})
    @Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
    public class RedisAutoConfiguration {
        public RedisAutoConfiguration() {
        }
    ​
        @Bean
        @ConditionalOnMissingBean(
            name = {"redisTemplate"}
        )
        @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
        public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<Object, Object> template = new RedisTemplate();
            template.setConnectionFactory(redisConnectionFactory);
            return template;
        }
        @Bean
        @ConditionalOnMissingBean
        @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
        public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
            return new StringRedisTemplate(redisConnectionFactory);
        }
        ......

    从上述RedisAutoConfiguration核心源码中可以看出,在Redis自动配置类中,通过Redis连接工厂 RedisConnectionFactory初始化了一个RedisTemplate;该类上方添加了 @ConditionalOnMissingBean注解(顾名思义,当某个Bean不存在时生效),用来表明如果开发者自 定义了一个名为redisTemplate的Bean,则该默认初始化的RedisTemplate不会生效。

    如果想要使用自定义序列化方式的RedisTemplate进行数据缓存操作,可以参考上述核心代码创建 一个名为redisTemplate的Bean组件,并在该组件中设置对应的序列化方式即可,如下:

    @Configuration
    public class RedisConfig {
    ​
        //自定义 RedisTemplate
        @Bean
        public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<Object, Object> template = new RedisTemplate();
            template.setConnectionFactory(redisConnectionFactory);
            //创建Json格式序列化对象,对缓存数据的key和value进行转换
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
            // 解决查询缓存转换异常的问题
            ObjectMapper om = new ObjectMapper();
            om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
            jackson2JsonRedisSerializer.setObjectMapper(om);
            //设置 RedisTemplate 模板Api的序列化方式为Json
            template.setDefaultSerializer(jackson2JsonRedisSerializer);
            return template;
        }
    }
  3. 测试结果: