2020:0705--15--SpringBoot与缓存

785 阅读20分钟

主要内容

1.  JSR-107缓存规范
2.  Spring缓存抽象
3.  整合Redis

1. 基本环境搭建

1.  创建项目

    1.  模块选择

2.  导入数据库文件
        SET FOREIGN_KEY_CHECKS=0;
        
        -- ----------------------------
        -- Table structure for department
        -- ----------------------------
        DROP TABLE IF EXISTS `department`;
        CREATE TABLE `department` (
          `id` int(11) NOT NULL AUTO_INCREMENT,
          `departmentName` varchar(255) DEFAULT NULL,
          PRIMARY KEY (`id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
        
        -- ----------------------------
        -- Table structure for employee
        -- ----------------------------
        DROP TABLE IF EXISTS `employee`;
        CREATE TABLE `employee` (
          `id` int(11) NOT NULL AUTO_INCREMENT,
          `lastName` varchar(255) DEFAULT NULL,
          `email` varchar(255) DEFAULT NULL,
          `gender` int(2) DEFAULT NULL,
          `d_id` int(11) DEFAULT NULL,
          PRIMARY KEY (`id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
3.  编写实体类

4.  整合durid连接池
    spring:
      #数据源基本配置
      datasource:
        username: root
        password: root
        url: jdbc:mysql://localhost:3306/spring_cache?serverTimezone=UTC
        driver-class-name: com.mysql.cj.jdbc.Driver
    
        # 开启后就能执行sql语句文件
        initialization-mode: always
        # 自定义加载的schema.sql语句文件的位置和名字
        schema:
          - classpath*:department.sql
    
        # 指明数据源类型
        type: com.alibaba.druid.pool.DruidDataSource
    
        #   数据源其他配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
    
        #   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
        filters: stat,wall,slf4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
        
        # 开启驼峰命名法
        mybatis:
          configuration:
            map-underscore-to-camel-case: true
5.  整合mybati操作数据库 --- 使用注解版的MyBatis

    1.  @MapperScan指定需要扫描的mapper接口所在的包
        @MapperScan("com.atguigu.springboot.cache.bean.mapper")
        @SpringBootApplication
        public class SpringbootAdvanced01ChcheApplication {
        
            public static void main(String[] args) {
                SpringApplication.run(SpringbootAdvanced01ChcheApplication.class, args);
            }
        
        }
    2.  两个mapper接口
@Mapper //声明这是mybatis的一个mapper类
public interface DepartmentMapper {
}

@Mapper //声明这是mybatis的一个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=#{email}, #{gender}, #{dId})")
    public void insertEmployee(Employee employee);
}
    3.  EmployeeService
        @Service
        public class EmployeeService {
        
        
            @Autowired
            EmployeeMapper employeeMapper;
        
            public Employee getEmp(Integer id){
        
                System.out.println("查询"+id+"号员工");
                Employee emp = employeeMapper.getEmpById(id);
                return emp;
            }
        }

    4.  EmployeeController
        @RestController
        public class EmployeeController {
        
            @Autowired
            EmployeeService employeeService;
        
            @GetMapping("/emp/{id}")
            public Employee getEmployee(@PathVariable("id") Integer id){
                Employee emp = employeeService.getEmp(id);
        
                return emp;
            }
        }
    5.  测试一下

2. 缓存使用

    步骤:
        1.  开启基于注解的缓存
                
            @EnableCaching :标注在主配置类上
        
        2.  标注缓存注解即可
        
            @Cacheable:主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
            
            @CacheEvict:清空缓存
            
            @CachePut: 保证方法被调用,又希望结果被缓存。

2.1 没有开启缓存--测试

    1.  EmployeeController
        @RestController
        public class EmployeeController {
        
            @Autowired
            EmployeeService employeeService;
        
            @GetMapping("/emp/{id}")
            public Employee getEmployee(@PathVariable("id") Integer id){
                Employee emp = employeeService.getEmp(id);
        
                return emp;
            }
        }
    2.  开启mapper包的日志
    
        将相关日志打印出来:包括sql语句
    
        我写在了application.properties
        # 开启mapper的日志
        logging.level.com.atguigu.springboot.cache.mapper=debug
    3.  开启缓存之前,测试:发送若干次同一个请求

        结果:
        
            每次发送同一个请求,都会执行一次相同的方法,得到相同的结果。
            没有对其结果进行缓存。

2.2 开启缓存@Cacheable

    将方法的运行结果进行缓存,以后再要相同的数据,直接从缓存中获取,不用调用方法。
    
    不用在调用数据库,进行sql查询了。
    
    1.  在EmployeeService的方法中标注:
@Service
public class EmployeeService {


    @Autowired
    EmployeeMapper employeeMapper;

    /**
     * 讲方法的运行结果进行缓存,以后再要相同的数据,直接从缓存中获取,不用调用方法。
     *
     * CacheManager管理多个Cache组件的,对缓存的真正CRUD操作在Cache组件中,每一个缓存组件有自己唯一的一个名字
     * Cache中数据以key-value对的数据存储。
     * 几个属性:
     *      cacheNames/value:指定缓存组件的名字
     *      key:缓存数据时,数据的key。不指定时默认是使用方法参数的值
     *          比如参数传个1,结果返回user。
     *          那么存到cache中的就是:1-user。
     *
     *          自己指定:编写SpEl表达式
     *          key="#id":取出参数id的值,把这个值作为key
     *
     * @param id
     * @return
     */
    @Cacheable(cacheNames = "emp", key="#id")
    public Employee getEmp(Integer id){

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

    2.   @Cacheable(cacheNames = {"emp", "temp"}, key="#id", condition = "#a0>0", unless = "#result == null")
    
        分析一下缓存注解
        
        1.  作用
        
            讲方法的运行结果进行缓存,以后再要相同的数据,直接从缓存中获取,不用调用方法。
            
        2.  cacheNames/value属性
        
            CacheManager管理多个Cache组件的,对缓存的真正CRUD操作在Cache组件中,
            每一个缓存组件有自己唯一的一个名字。
            
            cacheNames/value:指定缓存组件的名字
            
        3.  key属性 key="#id"/key="#root.aggs[0]"
            
            Cache中数据以key-value对的数据存储。
            
            指定缓存数据时,数据的key,不指定时默认是使用方法参数的值。
            比如参数传个1,结果返回user。那么存到cache中的就是:1-user。
            
            通过编写SpEl表达式,来指定key。
            
            key="#id":取出参数id的值,把这个值作为key
            
            等价于:key="#root.aggs[0]", #a0, #p0
            
            相关的SpEl写法

        4.  cacheManager:指定相关的缓存管理器

        5.  cacheResolver: 缓存解析器。类似缓存管理器,配置时两者二选一。

        6.  condition:判断条件,指定符合条件的情况下才缓存。
        
             condition = "#a0>0", 
        
        7.  keyGenerator:key的生成器;
        
            指定了key,就不要指定keyGenerator了。二选一
            
        8.  unless:否定缓存,当unless指定的条件为true,方法的返回值就不会缓存。
            可以对获取到的结果进行判断。
            
            unless = "#result == null"
            
        9.  sync:是否使用异步模式
        
        
            
    3.  测试一下

3. 原理

    自动配置类 : CacheAutoConfiguration

3.1 第一次请求

    1.  点进去

    2.  导入一个CacheConfigurationImportSelector
    
        @Import({ CacheConfigurationImportSelector.class,
    
    3.  点过去,打上断点分析一下。

    4.  返回了十个缓存配置类

        org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration
        org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration
        org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration
        org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration
        org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration
        org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration
        org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration
        org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration
        org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration
        org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration
        
    5.  这么多配置类,哪个配置类能生效呢?
    
        # 打开自动配置报告
        debug: true
        
        在控制台搜索一下CacheConfiguration。

    6.  发现只有一个SimpleCacheConfiguration默认生效了
    
        SimpleCacheConfiguration matched
        
        CaffeineCacheConfiguration:Did not match:
        
        CouchbaseCacheConfiguration:Did not match:
        
        EhCacheCacheConfiguration: Did not match:
        
        GenericCacheConfiguration:Did not match:
        
        HazelcastCacheConfiguration:Did not match:
        
        InfinispanCacheConfiguration:Did not match:
        
        JCacheCacheConfiguration:Did not match:
        
        NoOpCacheConfiguration:Did not match:
        
        RedisCacheConfiguration:Did not match:
        
    7.  进入SimpleCacheConfiguration分析一下
    
        给容器中注册了一个:ConcurrentMapCacheManager cacheManager
        @Configuration(proxyBeanMethods = false)
        @ConditionalOnMissingBean(CacheManager.class)
        @Conditional(CacheCondition.class)
        class SimpleCacheConfiguration {
        
        	@Bean
        	ConcurrentMapCacheManager cacheManager(CacheProperties cacheProperties,
        			CacheManagerCustomizers cacheManagerCustomizers) {
        		ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
        		List<String> cacheNames = cacheProperties.getCacheNames();
        		if (!cacheNames.isEmpty()) {
        			cacheManager.setCacheNames(cacheNames);
        		}
        		return cacheManagerCustomizers.customize(cacheManager);
        	}
        
        }
    7.1 进入ConcurrentMapCacheManager看一下
    
        实现了CacheManager

    7.1.1   CacheManager中一个方法
    
            Cache getCache(String var1);
            
            按照一个名字得到一个Cache缓存组件

    7.2 ConcurrentMapCacheManager重写了getCache()方法
    
        按照一个名字得到一个Cache缓存组件
        @Nullable
        public Cache getCache(String name) {
            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;
        }
    7.3 分析一下该方法
    
        大概分析一下:按照一个名字得到一个Cache缓存组件
        @Nullable
        public Cache getCache(String name) {
            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;
        }
    7.3.1    
        Cache cache = (Cache)this.cacheMap.get(name);
        
        cacheMap是ConcurrentMap<String, Cache>类型的Map
        
        所以就是从cacheMap中根据cacheName获取ConcurrentMapCache类型的Cache
        
    7.3.2   
            
        if (cache == null && this.dynamic){
            //如果没有将创建一个ConcurrentMapCache类型的Cache
            cache = (Cache)this.cacheMap.get(name);
        }
  
    7.4 SimpleCacheConfiguration的作用
        
        向容器中注入ConcurrentMapCacheManager缓存管理器
        
        ConcurrentMapCacheManager缓存管理器可以获取和创建ConcurrentMapCache类型的Cache缓存组件。
        
    
    8.  看一下ConcurrentMapCache类型的缓存组件
    
        里面有一些操作缓存的方法
        
        比如:获取缓存中的数据和将数据存入缓存中
            private final ConcurrentMap<Object, Object> store;
            
            protected Object lookup(Object key) {
                return this.store.get(key);
            }
            
            public void put(Object key, @Nullable Object value) {
                this.store.put(key, this.toStoreValue(value));
            }
            
        store就是一个ConcurrentMap
        
    8.1 我们可以认为它将数据保存在ConcurrentMap,从这个Map中存取数据

4. 分析一下缓存的执行流程

    以@Cacheable为例    
        
    在Service方法里面打上断点。
    
    ConcurrentMapCache
    源码中的存取数据打上断点。

    ConcurrentMapCacheManager

    1.  debug模式启动,发送一个请求
    
        进入断点
        
    2.  发现发送请求后,没有进入service方法中,而是先来到缓存管理器的获取缓存的方法
    
        ConcurrentMapCacheManager
        (Cache)this.cacheMap.get(name);
        
        
        先去找name是emp的缓存

        emp就是我们在service方法中指定的cache的name。
        
        
    3.  由于是第一次发送请求所以还没有name是emp的缓存
    
        所以创建一个。
        
        把创建出来的缓存组件都放到cacheMap中。
        
        得到一个cache缓存组件。
        
    4.  放行来到了ConcurrentMapCache 中的 lookup(Object key)
    
        按照key去缓存中找值
            protected Object lookup(Object key) {
                return this.store.get(key);
            }
        按照key=1来获取cache中的数据。
        
        1是我们请求中传过来的参数。
    
    5.  key=1是怎么得到的呢
        
        keyGenerator.generate(this.target, this.metadata.method, this.args);
        
        通过key生成器,将对象,方法,参数都传过来生成的。

        所以默认是使用keyGenerator生成的key。
        
    
    6.  keyGenerator是一个接口,默认是SimplekeyGenerator实现

        SimplekeyGenerator生成key策略:
            如果没有参数:key=new SimpleKey()
            如果有一个参数:key=参数值
            如果有多个参数:key=new SimpleKey(params)
        
        
    7.  回到4步的lookup(Object key)
    
        查询,由于是第一次,缓存没数据。
        
        查到的value是个空值。
        
    8.  放行
        
        来到了目标方法。
        
        service中的方法。

    9.  放行
    
        将目标方法的返回值放进cache中。

        store是ConcurrentMap数据类型的。所以默认用的是ConcurrentMap来缓存数据。
        
    
    10. 所以@Cache标注的方法,执行之前先来检查缓存。
        
        默认按照参数的值作为key来查询缓存。
        
        如果没有就运行方法并将结果放入缓存。
        
    11. 存好之后,显示页面

3.2 第二次请求

    1.  现根据缓存name获取缓存

    2.  根据生成的key去缓存中取数据

    3.  取到数据,放行。直接页面显示
    
        没有调用service中的方法

3.3 小结

    1.  使用CacheManager 按照名字Cache的name属性得到Cache组件
        默认是ConcurrentMapCacheManager实现CacheManager 接口
        
        用ConcurrentHashMap容器来存取容器
        
    2.  key是使用keyGenerator生成的
    
    3.  先检查缓存,缓存中没有数据再去执行目标方法。

3.3. @Cacheable 的属性用法

    @Cacheable放在方法上

    1.  cacheName/value
    
        将数据放到名为XX的缓存cache中,可以同时指定多个cache名。
        
        @Cacheable(cacheNames = {"emp", "temp"})
    
   
   2.   key
        
        cache中的数据以key-value的形式存储。
        
        默认是方法的参数值,我们也可以通过SpEL表达式来手动指定

        例子:  key="#root.methodName+'['+#id+']'"
            
                方法名拼接参数,key=getEmp[id]
                
        注意:  @Cacheable中的key不能用@result.id这种形式指定。
                
                因为@Cacheable标注的方法在运行之前,就通过指定的key去缓存中查找。
                所以方法还没运行之前,还没有result,就要得到key。
                所以不能通过result得到key。

    3.  keyGenerator:key的生成器
    
        key就是由keyGenerator生成的,生成规则:
        
            如果没有参数:key=new SimpleKey()
            如果有一个参数:key=参数值
            如果有多个参数:key=new SimpleKey(params)
            
        key和keyGenerator只需指定其中一个即可。
        
        
        我们自定义一个keyGenerator,注意包不要导错。

            @Configuration
            public class MyCacheConfig {
            
                @Bean("myKeyGenerator") //加在容器中
                public KeyGenerator keyGenerator(){
                    return new KeyGenerator(){
            
                        @Override
                        public Object generate(Object o, Method method, Object... params) {
                            return method.getName()+"["+ Arrays.asList(params).toString()+"]";
                        }
                    };
                }
            }
       添加了keyGenerator,就不用再添加key属性了。
       @Cacheable(cacheNames = {"emp", "temp"}, keyGenerator = "myKeyGenerator")
       
       
       测试一下:这是我们自己指定的有参时的Key的生成策略。

    4.  CacheManager:后面整合多个缓存管理器时再说。
    
    
    5.  condition:符合条件时,才缓存方法返回的结果
    
        condition = "#a0>0"  
        方法传过来的参数中第一个参数值大于0时,才缓存返回结果。
        
        condition = "#a0>0 and #root.getMethodName() eq 'getEmp'"
        第一个参数要大于0,并且目标方法名为getEmp
        
    6.  unless:满足条件是不缓存
    
        unless = "#result == null"  结果为null时,不缓存

4. @CachePut:

    目标方法调用之后,再放到缓存中。
    可以通过指定和@Cacheable相同的key,实现即调用方法,又更新缓存数据。
    修改了数据库的某个数据,同时更新缓存。

        @Cacheable(cacheNames = {"emp", "temp"}, keyGenerator = "myKeyGenerator")
        public Employee getEmp(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;
        }
    1.  测试一下:
    
        测试步骤:
        1. 查询1号员工:查到的结果会放到缓存中
        2. 以后查询1号员工,还是原来的结果
        3. 更新一号员工,更新后的结果放到了缓存中
        4. 再来查询1号员工:竟然是没有更新前的数据。
        
        原因:  因为第一次查询1号员工将数据放到缓存中是以key-value的形式,我们指定了key:
                getEmp[[1]] --- 结果。
                更新1号员工后,将方法的返回值也放到缓存中,那么缓存中的数据的key,
                默认是参数:
                传入的employee --- 结果。
                 
                所以即使更新1号员工后在查询1号员工,得到的结果是更新前的数据。
                
    2.  想要实现查询1号员工的结果是更新后的数据。
        key="#result.id"  将更新后的结果存在cache中且key是employee.id。
        
        将查询方法和更新方法指定同样的返回值和同样的key。
            //查询
            @Cacheable(cacheNames = {"emp", "temp"})
            public Employee getEmp(Integer id){
        
                System.out.println("查询"+id+"号员工");
                Employee emp = employeeMapper.getEmpById(id);
                return emp;
            }
            
            //更新
            @CachePut(value = "emp", key="#result.id")
            public Employee updateEmp(Employee employee){
        
                System.out.println("updateEmp: " + employee);
        
                employeeMapper.updateEmp(employee);
                return employee;
            }
    3.  @CachePut通过指定和@Cacheable相同的key,达到了即调用方法,又更新缓存数据。
    
        
    4.  注意一个Bug.
    
        如果我发送的请求是:http://localhost:8080/emp?lastName=王五
        
        Controller: 
        接收到参数封装成一个employee
        Employee [id=null, lastName=王五, email=null, gender=null, dId=null]
        
        employee传给Service
        employeeMapper.updateEmp(employee)
        
        employee传给Mapper
        @Update("update employee set lastName=#{lastName}, email=#{email}, gender=#{gender}, d_id=#{dId} where id=#{id}")
        public void updateEmp(Employee employee);
        
        最后在Mapper的逻辑:
        找到id=#{id}的记录,将其更新,但是#{id}=null,所以显然会报错。
        
        即:请求后面跟参数时,必须带id

    5.  注意
    
        页面的返回值,是传入的参数封装成的Employee。不是更新后的Employee。
            @GetMapping("/emp")
            public Employee update(Employee employee){
                Employee emp = employeeService.updateEmp(employee);
                return emp;
            }
            
            @CachePut(value = "emp", key="#result.id")
            public Employee updateEmp(Employee employee){
        
                System.out.println("updateEmp: " + employee);
        
                employeeMapper.updateEmp(employee);
        
                return employee;
            }

5. @CacheEvict:缓存清除

    删除了一个数据后,将相应的缓存也删除掉。
    
    
    注意测试时发送的请求方式:
    1.  @GetMapping("/delete") --- http://localhost:8080/delete?id=1
    //发送的请求应该是:http://localhost:8080/delete?id=1
    
    @GetMapping("/delete")
    public String deleteEmp(Integer id){
        employeeService.deleteEmp(id);
        return "success";
    }
    2.  @GetMapping("/delete/{id}") --- http://localhost:8080/delete/1
        //发送的请求应该是:http://localhost:8080/delete/1
        @GetMapping("/delete/{id}")
        public String deleteEmp(@PathVariable("id") Integer id){
            employeeService.deleteEmp(id);
            return "success";
        }
    3.  测试结果:删除缓存之后,再次查询还是存缓存中拿到数据。
    
        删除缓存失败
    
    4.  原因
    
        查询的结果放到了cacheNames = {"emp", "temp"}两个缓存中。
        
        删除只删除了value = "emp"的缓存中的数据。
        @Cacheable(cacheNames = {"emp", "temp"})
        public Employee getEmp(Integer id){
    
            System.out.println("查询"+id+"号员工");
            Employee emp = employeeMapper.getEmpById(id);
            return emp;
        }
        
        @CacheEvict(value = "emp", key="#id")
        public void deleteEmp(Integer id){
            System.out.println("deleteEmp: "+id);
        }

    5.  重新测试一下
        @CacheEvict(value = {"emp", "temp"}, key="#id")
        public void deleteEmp(Integer id){
            System.out.println("deleteEmp: "+id);
        }
        测试成功,删除了value = {"emp", "temp"}的缓存中对应的key的数据。
        
    
    6.  属性
    
        1.  allEntries:是否删除指定缓存中的所有数据
        
            allEntries=true,此时就不用再指定Key了。
            
        2.  beforeInvocation : 缓存的清除是否在方法之前执行
        
            beforeInvocation=false  默认是false。
            
            如果将其设置为true,beforeInvocation=false
            
            可以理解为,无论方法执行如何,都必须先删除缓存。
            @CacheEvict(value = {"emp", "temp"}, key="#id", beforeInvocation = true)
            public void deleteEmp(Integer id){
                System.out.println("deleteEmp: "+id);
                int i =10/0;
            }
            先删除缓存,然后执行方法。方法有错,缓存仍然清除了。

6 @Caching

    是一个组合注解

bug注意

    注意:Controller中的两个方法
        @GetMapping("/emp/{id}")
        public Employee getEmployee(@PathVariable("id") Integer id){
            Employee emp = employeeService.getEmp(id);
    
            return emp;
        }
        
        @GetMapping("emp/{lastName}")
        public Employee getEmpByLastName(@PathVariable("lastName") String lastName){
            return employeeService.getEmpByLastNmae(lastName);
        }
    这样写是错误的。
    因为@GetMapping("/emp/{id}")和@GetMapping("emp/{lastName}"),它们要映射的请求是一样的,
    都会获取这个请求中的参数。
    
    比如:http://localhost:8080/emp/王五
    那么这俩个方法都会去匹配这个请求中的参数,所以会报一个:模糊含糊的错误

    修改:
    @GetMapping("/emp/{id}")
    @GetMapping("emp/lastName/{lastName}")

    
    测试:
        @GetMapping("emp/lastName/{lastName}")
        public Employee getEmpByLastName(@PathVariable("lastName") String lastName){
            return employeeService.getEmpByLastNmae(lastName);
        }
        
        @Caching(
            cacheable = {
                    @Cacheable(value = "emp", key = "#lastName")
            },
            put = {
                    @CachePut(value = "emp", key="result.id"),
                    @CachePut(value = "emp", key="#result.email")
            }
        )
        public Employee getEmpByLastNmae(String lastName){
               return employeeMapper.getEmpByLastName(lastName);
    
    分析一下 
        @Caching(
            cacheable = {
                    @Cacheable(value = "emp", key = "#lastName")
            },
            put = {
                    @CachePut(value = "emp", key="result.id"),
                    @CachePut(value = "emp", key="#result.email")
            }
        )
    其中的:@Cacheable(value = "emp", key = "#lastName")
            先查询缓存,没有的话再执行方法。将方法的返回值存到名为emp的chche中,且key=lastName
            
            @CachePut(value = "emp", key="result.id"),
            @CachePut(value = "emp", key="#result.email")
            先执行方法,再将结果存到emp缓存cache中,且key为result.id和key为result.email中。
            
        
            @Cacheable你先去缓存中找, @CachePut我直接执行方法。不影响。

7 @CacheConfig能为一个类配置一个公共的cache属性

    @CacheConfig(cacheNames="emp"):为整个类的所有方法都指定一个缓存cache:emp
    
    那么方法上的:value。cacheNames等属性就不用写了。
    @Cacheable(cacheNames = {"emp", "temp"})
    @CachePut(value = "emp", key="#result.id")

注意:这几个注解,一定要注意是先执行缓存,还是先执行目标方法。

8. 搭建Redis环境

    我们知道在没有配置时,缓存使用的是ConcurrentMapCache组件中的ConcurrentMap<Object, Object>
    来做缓存容器的,它是一个Map:ConcurrentMap<K, V> extends Map<K, V>
    
    实际在开发中,我们经常使用的是缓存中间件,比如redis,memcached...
    
    我们前面分析了SpringBoot能开启多种的缓存配置:
            org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration
            org.springframework.boot.autoconfigure.cache.JCacheCacheConfiguration
            org.springframework.boot.autoconfigure.cache.EhCacheCacheConfiguration
            org.springframework.boot.autoconfigure.cache.HazelcastCacheConfiguration
            org.springframework.boot.autoconfigure.cache.InfinispanCacheConfiguration
            org.springframework.boot.autoconfigure.cache.CouchbaseCacheConfiguration
            org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration
            org.springframework.boot.autoconfigure.cache.CaffeineCacheConfiguration
            org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration
            org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration
    默认开启的是SimpleCacheConfiguration这个缓存配置。
    
    其他的缓存配置在你导入相关的组件,才会生效相关的配置。

0.  整合Redis作为缓存。

1.  docker启动redis 

2.  连接redis

redis相关命令可以参考redis中国官网

redis中国

3.  引入redis的starter
        <!--引入redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    RedisAutoConfiguration的自动配置就起效了。
    
4.  在application.yml中配置redis
      # 配置redis的主机地址
      redis:
        host: 192.168.92.130
5.  RedisAutoConfiguration向容器中注入了两个组件

    1.  RedisTemplate<Object, Object>   : 操作所有的对象的
    2.  StringRedisTemplate : 专门简化操作reids中字符串的工具类
    
    这是SpringBoot为了简化Redis操作,为我们注入的两个组件。

    我们操作redis,就用这两个组件。
    

6.  测试一下

    1.  用StringRedisTemplate操作字符串
        (操作redis的5种数据类型,向其中添加/删除String类型的值)
    @Test
    public void test01(){
        //给redis中保存了一个String数据
        stringRedisTemplate.opsForValue().append("msg", "hello");

        
        String msg = stringRedisTemplate.opsForValue().get("msg");
        System.out.println(msg);
        
        //给redis保存了List数据
        stringRedisTemplate.opsForList().leftPush("mylist", "1");
        stringRedisTemplate.opsForList().leftPush("mylist", "2");
    }
    2.  用RedisTemplate<Object, Object>操作对象
        @Test
        public void test02(){
            Employee empById = employeeMapper.getEmpById(1);
            redisTemplate.opsForValue().set("emp01", empById);
        }

    注意,这里会报错:
        
    要求我们存入的对象是可以序列化的。

    2.1 将实体类实现Serializable接口

        如果保存对象,默认是使用JDK序列化机制,将序列化后的数据保存到redis中。

        结果

    2.2 将数据以JSON的方式保存
    
        1.  我们可以用市面上的一些JSON转换工具尝试一下
        
        2.  redisTemplate它有默认的序列化规则
            
            默认使用JDK的序列化规则

3.  我们想要以JSON的方式保存对象

    我们可以自己指定来添加一个RedisTemplate<Object, Object>,并且指定序列化规则。
    
    这是RedisAutoConfiguration源码中默认添加的RedisTemplate组件
    public class 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;
    	}
    }
3.1 我们自己写一个方法
    
    其中指明序列化采用JSON
    @Configuration
    public class MyRedisConfig {
    
        @Bean
        public RedisTemplate<Object, Employee> empRedisTemplate(RedisConnectionFactory redisConnectionFactory)
                throws UnknownHostException {
            RedisTemplate<Object, Employee> template = new RedisTemplate<>();
            template.setConnectionFactory(redisConnectionFactory);
    
            Jackson2JsonRedisSerializer<Employee> serializer = new
                Jackson2JsonRedisSerializer<Employee>(Employee.class);
                
            template.setDefaultSerializer(serializer);
            return template;
        }
    }
3.2 测试
    //注入我们自己的RedisTemplate
    @Autowired
    RedisTemplate<Object, Employee> employeeRedisTemplate;
    
    //测试保存redis对象
    @Test
    public void test02(){
        Employee empById = employeeMapper.getEmpById(1);

        employeeRedisTemplate.opsForValue().set("emp01", empById);
    }

9 缓存原理

    默认之前用的是CocurrentMap的缓存管理器帮我们创建cache组件,缓存组件来给缓存中进行数据操作。
    
    那么引入redis的启动器后,会发生什么样的效果呢?
    
    1.  打开自动配置的报告
# 打开自动配置报告
debug: true

    2.  启动主程序
    
        以前是默认开启SimpleCacheConfiguration这个配置类。
        
        在控制台发现:RedisCacheConfiguration开启了。
        RedisCacheConfiguration matched:
        
        其他的缓存配置类都没有匹配上。

9.1 分析

    1.  注入CacheManager
    
        谁向容器中放入了缓存管理器。

        @ConditionalOnMissingBean(CacheManager.class)
        如果没有CacheManager.class,RedisCacheConfiguration就会注入一个CacheManager.class
        
        
        我们也发现,SimpleCacheConfiguration也是当没有CacheManager.class时,
        也会注入一个CacheManager.class。

        但是虽然相同的规则,却又先后顺序。因为导入了Redis包,所以RedisCacheConfiguration会
        先进行判断,单后就直接注入一个CacheManager。之后SimpleCacheConfiguration和其他配置类
        在判断时,就不再满足这个条件了。
        
        
    2.  RedisCacheManager来为我们创建缓存cache

        有一个createRedisCache()创建缓存。
        
        所以RedisCacheManager帮我们创建RedisCache来作为缓存。
        
    3.  RedisCache通过操作reids来缓存数据的。

9.2 测试一下能不能缓存数据

    1.  发送多次http://localhost:8080/emp/1请求
        @Cacheable(cacheNames = {"emp", "temp"})
        public Employee getEmp(Integer id){
    
            System.out.println("查询"+id+"号员工");
            Employee emp = employeeMapper.getEmpById(id);
            return emp;
        }
        发现第二次请求就不在交互数据库。
        而且在redis中也发现了相关的缓存数据

        因为我们对Employee实现了序列化接口,所以在redis中也是以JDK序列化存储。
        
    
        引入redis的starter时,注入的cacheManager是RedisCacheManager
        RedisCacheManager操作redis的时候使用的是RedisTemplate<Object,Object>
        RedisTemplate<Object,Object>是默认使用JDK的序列化机制。
    
    2.  将其保存为JSON            
        
        引入redis的starter时,注入的cacheManager是RedisCacheManager
        RedisCacheManager操作redis的时候使用的是RedisTemplate<Object,Object>
        RedisTemplate<Object,Object>是默认使用JDK的序列化机制。
        
        因为这个原因,所以我们可以自定义一个CacheManager
        
    3.  自定义一个CacheManager

注意

        因为我的版本2.3.1和视频课1.5.1源码差别较大,无法用用视频中的方式重写cacheManager()方法。
        
        下面方法来自评论:具体实现原理不懂。
        效果:适用于所有的POJO,不是针对单独某个POJO。
        
        序列化:向Redis中存对象时要用到序列化
        反序列化:将Redis中的数据解析成对象时。
        
        反序列化会出错:
        视频课上面的实现是针对Emp这个POJO类的,也就是当我们进行查找时从缓存的取到的JSON数,要将其
        转成Emp POJO类。
        这个时候,当我们去查询Dept时,也会先从缓存中拿数据,然后再将其转成Dept类。但是此时的解析
        规则是针对Emp类的,那么显然会出错。
        比如Dept有2个属性,Emp有5个属性,那么就会报错。
        
        SpringBoot 2.x 版本的CacheManager配置
        如果写了多个CacheManager,要指定一个默认的CacheManager
        @Primary //标注其是默认使用的CacheManager
        //@Primary 标注其是默认使用的CacheManager,一般还是自动配置默认的
        @Bean
        public CacheManager cacheManager(RedisConnectionFactory factory){
            RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofDays(1)).
                    disableCachingNullValues().
                    serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                            new GenericJackson2JsonRedisSerializer()
                    ));
    
            return RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration).build();
        }

    4.  测试报错:500
    
        之前在Redis中的数据还没有删除,还是JDK序列化的形式。
        
        再次查询时,发现缓存中有key对应的数据,但是这解析时按照JSON解析的。所以会报错。
        
    5.  解决:删除掉redis的相关缓存。

        测试成功。

注意我们的key多了一个前缀,会将cache的name作为key的前缀。

    6.  在代码里操作Redis中的cache
        @Cacheable(cacheNames = "dept", key="#id")
        public Department getDeptById(Integer id){
    
            System.out.println("查询部门:"+id);
            Department department =departmentMapper.getDeptByid(id);
    
            //获取某个缓存
            Cache dept = cacheManager.getCache("dept");
            dept.put("dept::1", department);
    
            return department;
        }