SpringBoot与缓存

一、SpringBoot与缓存

1、基础概念

缓存:缓存是指可以进行高速数据交换的存储器,它先于内存与CPU交换数据,因此速率很快。
缓存作用:缓存的工作原理是当CPU要读取一个数据时,首先从CPU缓存中查找,找到就立即读取并送给CPU处理;没有找到,就从速率相对较慢的内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。(例:在数据库中利用缓存机制可以大大降低数据库的负压能力,当我们查询相同的数据时不用每一次都在数据库中查询,这时我们就可以利用缓存机制将第一次在数据库中查询的内容保存在缓存中,这样不仅提高了效率而且减少了数据库的负担)

2、搭建环境

2.1、引入缓存的相关依赖
<!--缓存的依赖-->
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
 </dependency>
 <!--我们要连接数据库进行测试引入数据库驱动-->
<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
</dependency>
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
</dependency>
2.2、数据库配置文件
spring.datasource.url=jdbc:mysql://localhost:3306/spring_cache?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=1234567890
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.redis.host=xxx.xxx.xx.xxx
2.3、整合MyBatis操作数据库

[详情见博客SpringBoot与数据访问(MyBatis)]j(https://blog.csdn.net/qq_43775034/article/details/104151952)

2.4、编写实体类(bean)

Employee.java

package com.atorg.cache.bean;
import java.io.Serializable;
public class Employee implements Serializable {
    private Integer id;
    private String lastName;
    private String email;
    private Integer gender;    //性别 1男 0女
    private Integer dId;

    public Employee() {
        super();
    }

    public Employee(Integer id, String lastName, String email, Integer gender, Integer dId) {
        this.id = id;
        this.lastName = lastName;
        this.email = email;
        this.gender = gender;
        this.dId = dId;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Integer getGender() {
        return gender;
    }

    public void setGender(Integer gender) {
        this.gender = gender;
    }

    public Integer getdId() {
        return dId;
    }

    public void setdId(Integer dId) {
        this.dId = dId;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "id=" + id +
                ", lastName='" + lastName + '\'' +
                ", email='" + email + '\'' +
                ", gender=" + gender +
                ", dId=" + dId +
                '}';
    }
}

Department.java

package com.atorg.cache.bean;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.io.Serializable;
public class Department implements Serializable {
    private Integer id;
    private String departmentName;

    public Department() {
        super();
    }

    public Department(Integer id, String departmentName) {
        this.id = id;
        this.departmentName = departmentName;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getDepartmentName() {
        return departmentName;
    }

    public void setDepartmentName(String departmentName) {
        this.departmentName = departmentName;
    }

    @Override
    public String toString() {
        return "Department {id="+id+",departmentName="+departmentName+"}";
    }
}
2.5、编写mapper操作数据库

DepartmentMapper.java

package com.atorg.cache.mapper;
import com.atorg.cache.bean.Department;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface DepartmentMapper {
    @Select("SELECT * FROM department WHERE id=#{id}")
    Department getDeptById(Integer id);
}

EmployeeMapper.java

package com.atorg.cache.mapper;
import com.atorg.cache.bean.Employee;
import org.apache.ibatis.annotations.*;
@Mapper
public interface EmployeeMapper {
//查询数据
    @Select("SELECT * FROM employee WHERE id=#{id}")
    public Employee getEmpById(Integer id);
//更新数据
    @Update("UPDATE employee SET lastName=#{lastName},email=#{email},gender=#{gender},d_id=#{dId} WHERE id=#{id}")
    public void updateEmp(Employee employee);
//删除数据
    @Delete("DELETE FROM employee WHERE id=#{id}")
    public void deleteEmpById(Integer id);
//插入数据
    @Insert("INSERT INTO employee(lastName,email,gender,d_id) VALUES(#{lastName},#{email},#{gender},#{dId})")
    public void insertUser(Employee employee);
//按名查询
    @Select("SELECT * FROM employee WHERE lastName=#{lastName}")
    public Employee  getEmpByLastName(String lastName);
}

注意@Mapper扫描mapper接口 _(扫描操作数据库所在的接口)_,在每一个mapper接口写一个@Mapper注解很麻烦,所以我们在主配置类中使用 @MapperScan(“mapper所在的包名”) 将扫描所有操作数据库的接口在每一个接口上默认加上@Mapper注解

3、快速体验缓存

3.1、开启基于注解的缓存

我们要使用基于注解的缓存必须要在主配置类中使用 @EnableCaching注解

3.2、编写基于注解缓存的service类

EmployeeService.java

package com.atorg.cache.service;
import com.atorg.cache.bean.Employee;
import com.atorg.cache.mapper.EmployeeMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;

@Service
public class EmployeeService {

    @Autowired
    EmployeeMapper employeeMapper;

    public Employee getEmpById(Integer id){
        System.out.println("查询"+id+"号员工");
        Employee emp=employeeMapper.getEmpById(id);
        return emp;
    }

    @CachePut(value = "emp")
    public Employee updateEmp(Employee employee){
        System.out.println("updateEmp:"+employee);
        employeeMapper.updateEmp(employee);
        return employee ;
    }

    @CacheEvict(/*value = "emp"*//*,key = "#id",*/allEntries = true)
    public void deleteEmp(Integer id){
        System.out.println("deleteEmp"+id);

    }

    //@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);
    }
}
3.3、@Cacheable注解

Cacheable的几个属性详讲
1、 cacheNames/value
指定缓存的名字,将方法的返回结果放在那个缓存中,是数组的形式,可以指定多个缓存
2、 key(在缓存中是以键值对的形式存在)
缓存使用的key由于数据在缓存中是以键值对的形式存在,故key可以用它来指定,

1)、默认是使用方法参数的值如果方法参数是1则值为:方法的返回值
2)、编写SpEl #id;参数id的值 《=======》 #a0 #p0 #root.args[0]__

3、keyGenerator:
key的生成器,可以自己指定key的生成器的组件id keyGenerator/key:二选一使用
4、==cacheManager==
指定缓存管理器或者指定缓存解析器cacheResolver
cacheManager/cacheResolver二选一使用
5、==condition==
指定符合条件的情况下才缓存 例:condition = “#id>1”当id>1时进行缓存
6、==unless==
否定缓存;当unless指定的条件为true,方法的返回值就不会被缓存,可以获取到结果进行判断
例:unless = “#result==null” 如果结果为null时不缓存

例:unless = “#a0==2”如果第一个参数的值是2,结果就不缓存
7、sync
是否使用异步模式
Cacheable运行流程
1、方法运行之前先去查询Cache组件,按照cacheNames指定的名字存取(cacheManager先获取相应的缓存),第一次获取缓存如果没有Cache组件会自动创建。
2、去Cache中查找缓存的内容,使用一个key,key默认就是方法的参数 key是按照某种策略生成的;是使用keyGenerator生成的,默认使用simpleKeyGenerator生成key simpleKeyGenerator生成的key的默认策略;
如果没有参数;key=new SimpleKey();
如果有一个参数;key=参数的值;
如果有多个参数;key=new SimpleKey(params)
3、没有查到缓存就调用目标方法
4、将目标方法返回的结果放入缓存中
5、自定义KeyGenerator

package com.atorg.cache.config;

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")  //id为myKeyGenerator
    public KeyGenerator keyGenerator(){
        return new KeyGenerator() {
            @Override
            public Object generate(Object o, Method method, Object... objects) {
                return method.getName()+"["+ Arrays.asList(objects)+"]";
            }
        };
    }
}

使用自定义的KeyGenerator只需在缓存注解中声明自定义KeyGenerator的id

@Cacheable(cacheNames = {"emp"},keyGenerator = "myKeyGenerator")

核心
@Cacheable方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值key去查询缓存,如果没有就去运行这哥方法并将结果放入缓存;以后再来调用就可以直接使用缓存中的数据

3.4、@CachePut注解

即调用方法又同步更新了缓存数据【即修改数据库的数据时同时同步更新了缓存】(注意取缓存与放缓存的key必须是同一个值
运行时机:
1、先调用目标方法
2、将目标方法的结果缓存起来

3.4.1、测试步骤

1、查询1号员工(第一次查询查询数据库并将数据放入数据库中)
在浏览器中输入请求地址:http://localhost:8080/emp/1
第一次查询查询数据库并将数据存入缓存
2、查询1号员工(第二次不通过数据库而是通过缓存)
在浏览器中输入请求地址:http://localhost:8080/emp/1
第二次直接通过缓存得到数据
3、更新1号员工【lastName=’zhangsan’, gender=0】
在浏览器中输入请求地址:http://localhost:8080/emp?lastName=zhangsan&gender=0
更新后的控制台打印
4、查询1号员工此时查询到的数据还是汉字张三而我们重新编写的为英文zhangsan
查询数据还是张三
然而数据库信息已经更新为什么我们得到的还是原来的值???
数据库信息已经更新
控制台打印更新数据
经过分析我们得到结果:我们在存入缓存时的 _key_默认为方法的参数值在调用查询方法时查询员工时key的值为id(在此例中为Employee的id的值即为 key=#employee.id而我们在更新缓存时默认的key为方法的返回值即为:key=employee为Employee的对象 存与更新缓存的key值不一样造成了最终取出的结果不一样,而此时的缓存也已更新,当我们再次查询缓存时调用查询方法使用的key的值为id因此查询出的结果时key的值按照id存入的缓存,故我们看到查到原来的数据
缓存更新
解决方法:
我们将查询与更新数据的缓存的key值设置为相同的,这样保证了我们再存缓存与取缓存时的key值相同,更新以后查处的数据也是更新以后的

    @Cacheable(cacheNames = {"emp"})//默认key的值为方法的参数id
    public Employee getEmpById(Integer id){
        System.out.println("查询"+id+"号员工");
        Employee emp=employeeMapper.getEmpById(id);
        return emp;
    }
    @CachePut(value = "emp",key = "#result.id")//指定方法的参数是返回值的id
    public Employee updateEmp(Employee employee){
        System.out.println("updateEmp:"+employee);
        employeeMapper.updateEmp(employee);
        return employee ;
    }

注意:@Cacheable的key是不能用#result的,因为Cacheable是先查缓存如果缓存没有再去执行方法,然而一开始的缓存肯定是空的,因为没有调用方法所以没有返回值此时返回值为null因此#result为null会报错
此时向浏览器发送:http://localhost:8080/emp?id=1&lastName=zhangsan&gender=0
更新数据查询1号员工:http://localhost:8080/emp/1
查询1号员工
控制打印语句也没有数据库查询语句说明去查缓存了
控制台打印
缓存更新
缓存更新

3.5、@CacheEvict注解

几个重要的属性
1、key:指定要清除的数据
2、 allEntries = true:指定清除这个缓存中所有的数据
3、beforeInvocation=false:缓存的清除是否在方法之前执行 ==默认代表缓存清除操作是在方法之后;如果出现异常缓存就不会被清除==
4、beforeInvocation=true代表缓存清除操作在方法执行之前,无论方法是否出现异常,缓存都清除。
此时缓存中有数据
缓存中有数据
向浏览器发送:http://localhost:8080/delemp?id=1删除1号员工
删除1号员工成功
此时的缓存中的数据被清空了
缓存的数据被清空

4、整合Redis作为缓存(序列化)

1)、引入redis的starter,容器中保存的是 RedisCacheManager
2)、RedisCacheManager帮我们创建 redisCache作为缓存组件,RedisCache通过操作redis缓存数据
3)、默认保存数据k-v都是object;利用序列化保存,如何保存为json

  • 1|引入了redis的starter,CacheManager变为RedisCacheManager

    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  • 2、默认创建的RedisCacheManager操作Redis的时候使用的是RedisTemplate<Object,Object>

  • 3.RedisTemplate<Object,Object>是默认使用jdk的序列化机制
    默认使用jdk的序列化机制

4)、自定义CacheManager(==SpringBoot2.2.x以上版本==)

package com.atorg.cache.config;
import com.atorg.cache.bean.Department;
import com.atorg.cache.bean.Employee;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
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.*;

import java.net.UnknownHostException;

@Configuration
public class MyRedisConfig {
    //CacheManagerCustomizers可以来定制缓存的一些规则
    @Bean
    public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //定义value序列化方式
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setKeySerializer(new StringRedisSerializer());//定义key序列化方式
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return  redisTemplate;
    }

    @Primary//将某个缓存管理器设置为默认
    @Bean
    public RedisCacheManager empCacheManager(RedisConnectionFactory redisConnectionFactory){
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Employee.class);
        RedisCacheConfiguration config =  RedisCacheConfiguration.defaultCacheConfig();
        RedisCacheConfiguration redisCacheConfiguration1 = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
        RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration1)
                .build();
        return  redisCacheManager;
    }

    @Bean
    public RedisCacheManager deptCacheManager(RedisConnectionFactory redisConnectionFactory){
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Department.class);
        RedisCacheConfiguration config =  RedisCacheConfiguration.defaultCacheConfig();
        RedisCacheConfiguration redisCacheConfiguration1 = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
        RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration1)
                .build();
        return  redisCacheManager;
    }
}

有问题希望大家能多多斧正


Author: Lelege
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source Lelege !
评论
 Previous
SpringBoot与消息 SpringBoot与消息
一、概述1、在大多数应用中,我们系统之间需要进行异步通信,即异步消息2、异步消息中两个重要概念: 消息代理(message broker)和目的地(destination)消息代理: 是一种在数据源与目的地之间移动数据使信息处
Next 
SpringBoot与数据访问(MyBatis) SpringBoot与数据访问(MyBatis)
三、整合MyBatis1.引入mybatis-starter(mybatis-spring-boot-starter)依赖<dependency> <groupId>org.mybatis.spring.boot
  TOC