SpringBoot08-缓存

JSR107缓存规范

  1. Java Caching定义了5个核心接口, 分别是CachingProvider, CacheManager, Cache, Entry, Expiry.
  2. CachingProvider定义了创建, 配置, 获取, 管理和控制多个CacheManager. 一个应用可以在运行期访问多个CachingProvider.
  3. CacheManager定义了创建, 配置, 获取, 管理和控制多个唯一命名的Cache, 这些Cache存在于CacheManager的上下文中. 一个CacheManager仅被一个CachingProvider所拥有.
  4. Cache是一个类似Map的数据结构并临时存储以Key为索引的值, 一个Cache仅被一个CacheManager所拥有.
  5. Entry是一个存储在Cache中的key-value对.
  6. Expiry每一个存储在Cache中的条目有一个定义的有效期. 一旦超过这个时间, 条目为过期的状态. 一旦过期, 条目将不可访问, 更新和删除. 缓存有效期可以通过ExpiryPolicy设置.
  7. 如果要使用JSR107, 需要导入如下依赖
            <dependency>
                <groupId>javax.cache</groupId>
                <artifactId>cache-api</artifactId>
                <version>1.1.1</version>
            </dependency>

Spring缓存抽象

  • 简介
  1. JSR107规范的技术复杂性较高, Spring3.1定义了Cache和CacheManager接口来统一不同的缓存技术.
    • 当然, 支持使用JSR107注解来简化我们的开发.
  2. Cache接口为缓存的组件规范定义, 包含缓存的各种操作集合.
    • Cache接口下Spring提供了各种xxxCache的实现, 如RedisCache.
  3. 每次调用需要缓存功能的方法时, Spring会检查检查指定参数的指定的目标方法是否已经被调用过. 如果有就直接从缓存中获取方法调用后的结果, 如果没有就调用方法并缓存结果后返回给用户. 下次调用直接从缓存中获取.
  4. 使用Spring缓存抽象时需要关注两点
    • 确定方法需要被缓存以及他们的缓存策略.
    • 从缓存中读取之前缓存存储的数据.
  • 几个重要概念
  1. Cache: 缓存接口, 定义缓存操作, 实现有RedisCache, EhCacheCache等.
  2. CacheManager: 缓存管理器, 管理各种Cache.
  3. @Cacheable: 主要针对方法配置, 能根据方法的请求参数对其结果进行缓存.
  4. @CacheEvict: 清空缓存, 如给删除方法上加该注解.
  5. @CachePut: 保证方法被调用, 又希望结果被缓存, 经常用于更新缓存.
    • 和@Cacheable的区别
      • @Cacheable是注解中有就不调用方法了.
      • 而@CachePut无论如何都会调用方法, 然后把新结果存入缓存.
  6. @EnableCaching: 开启基于注解的缓存.
  7. keyGenerator: 缓存数据时key生成策略.
  8. serialize: 缓存数据时value序列化策略.
  • 搭建缓存的基本环境
  1. 引入spring-boot-starter-cache模块
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-cache</artifactId>
            </dependency>
  2. 主启动类
    /**
     * 搭建基本环境
     * 1. 创建数据库spring_cache, 并创建department和employee表
     * 2. 创建javaBean封装数据
     * 3. 整合Mybatis操作数据库
     *      1. 配置数据源信息
     *      2. 使用注解版Mybatis
     *          1) @MapperScan指定需要扫描的mapper接口所在的包
     *          2) 对应的mapper接口要带@Mapper主机
     *
     * 快速体验缓存
     * 1. 开启基于注解的缓存.
     * 2. 标注缓存注解
     */
    
    @EnableCaching
    @MapperScan("top.binwenhome.cache.mapper")
    @SpringBootApplication
    public class SpringBootCache2Application {
    
        public static void main(String[] args) {
            SpringApplication.run(SpringBootCache2Application.class, args);
        }
    
    }
  3. 如EmployeeMapper
    @Mapper
    public interface EmployeeMapper {
    
        @Select("SELECT * FROM employee WHERE id = #{id}")
        public Employee getEmpById(Integer id);
    }
  4. 搭建service, controller层
    @Service
    public class EmployeeService {
    
        @Autowired
        private EmployeeMapper employeeMapper;
    
        public Employee getEmp(Integer id) {
            System.out.println("查询" + id + "号员工");
    
            Employee emp = employeeMapper.getEmpById(id);
    
            return emp;
        }
    }
    
    
    @RestController
    public class EmployeeController {
    
        @Autowired
        private EmployeeService employeeService;
    
        @GetMapping("/emp/{id}")
        public Employee getEmployee(@PathVariable("id")Integer id) {
            Employee emp = employeeService.getEmp(id);
            return emp;
        }
    }
  • 缓存中的SpEL用法
  • @Cacheable注解
  1. 将方法的结果进行缓存, 以后再要相同的数据, 就不用从数据库查了.
  2. CacheManager: 管理多个Cache组件, 对缓存的真正CRUD操作在Cache组件中, 每个缓存组件都有一个唯一的名字.
  3. 几个属性
    • cacheNames/value: 指定缓存的名字, 将方法的返回结果放在哪个缓存中, 是数组的方式.
    • key/keyGenerator
      • key: 缓存数据使用的key. 可以用它来指定. 默认是使用方法参数的值.
      • keyGenerator: key的生成器: 可以自己指定key的生成器的组件id.
      • 两者二选一.
    • condition: 指定符合条件的情况下, 才缓存.
    • cacheManager: 指定缓存管理器, 或cacheResolver指定缓存解析器.
    • unless: 否定缓存, 当unless指定的条件为true, 方法的返回值就不会被缓存. 可以获取到结果进行排查.
    • sync: 是否使用异步模式, 默认为false, 若使用, 则unless属性不支持.
  4. 举例(service方法中)
    • 使用key
      //condition = "#a0<2": 第一个参数的值<2时才进行缓存.
          //unless = "#a0==2" 如果第一个参数的值为2, 其结果不缓存
          @Cacheable(cacheNames = {"emp"}, key = "#root.methodName + '[' + #id + ']'",
                  condition = "#a0<2 and #root.methodName eq 'getEmp'", unless = "#a0==2")
          public Employee getEmp(Integer id) {
              System.out.println("查询" + id + "号员工");
              Employee emp = employeeMapper.getEmpById(id);
              return emp;
          }
    • 使用keyGenerator
          @Cacheable(cacheNames = {"emp"}, keyGenerator = "myKeyGenerator")
          public Employee getEmp(Integer id) {
              System.out.println("查询" + id + "号员工");
              Employee emp = employeeMapper.getEmpById(id);
              return emp;
          }
      • 需要自己手写KeyGenerator的实现加入容器中
        @Configuration
        public class MyCacheConfig {
        
            @Bean("myKeyGenerator")
            public KeyGenerator keyGenerator() {
                return new KeyGenerator() {
                    @Override
                    public Object generate(Object o, Method method, Object... objects) {
                        return method.getName() + "[" + Arrays.asList(objects).toString() + "]";
                    }
                };
            }
        }
  • @CachePut
        /**
         * @CachePut: 既调用方法, 又更新缓存数据
         * 1. 修改了数据库的某个数据, 同时更新缓存
         * 2. 运行时机
         *      1. 先调用目标方法
         *      2. 再把目标方法的结果缓存起来
         * 3.测试步骤
         *      1. 查询1号员工, 查到的结果会放到缓存中
         *          key: 1; value: 张三/1
         *      2. 以后查询还是之前的结果
         *      3. 更新1号员工 [lastName=zhangsan; gender=0]
         *          将方法的返回值也放进缓存了, 但它的key是传入的employee对象, 而值是返回的employee对象
         *      4. 查询1号员工
         *          是更新前的员工, 为什么是没更新前的? [1号员工没在缓存中更新]
         *      5. 所以应手动设置key
         *          key="#employee.id"; 或 key="#result.id"
         */
        @CachePut(cacheNames = {"emp"})
        public Employee updateEmp(Employee employee) {
            System.out.println("员工更新: " + employee);
            employeeMapper.updateEmp(employee);
            return employee;
        }
    
    只需要在@CachePut中添加属性key, 设置相应的值即可. (查询的key和更新的key必须一致)
  • @CacheEvict
        /**
         * @CacheEvict: 清除缓存
         *  1. 属性
         *      key: 指定要清楚的数据
         *      allEntries: 是否删除所有缓存. 默认为false. (只清空value/cacheNames中设置的缓存.)
         *      beforeInvocation: 缓存的清除是否在方法之前执行, 默认是false.
         *          举个例子: 如果当前方法出错, 若设false, 则不会清空缓存, 若true, 则会.
         */
        @CacheEvict(value = "emp", allEntries = true, beforeInvocation = true)
        public void deleteEmp(Integer id) {
            System.out.println("删除员工" + id);
            employeeMapper.deleteEmpById(id);
        }
  • @Caching&CacheConfig
    • @Caching
          /**
           * @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) {
              System.out.println("根据名字查询" + lastName);
              Employee emp = employeeMapper.getEmpByLastName(lastName);
              return emp;
          }
    • @CacheConfig
      /**
       * 在类上加@CachConfig, 指定cacheNames, 则该类所有的缓存名均为此值.
       */
      @CacheConfig(cacheNames = "emp")
      @Service
      public class EmployeeService {
        
          //...
      }

 整合Redis

  1.  默认给容器中注册的CacheManager是: ConcurrentMapCacheManager
    • 可以获取和创建ConcurrentMapCache类型的缓存组件, 其作用是将数据缓存在ConcurrentMap中.
  2. 而在开发中, 常用缓存中间件, 如Redis, EhCache等.
  • 整合Redis作为缓存
  1. 先安装Redis, 使用Docker方法.
    拉取Redis
    docker pull redis:latest

    运行Redis docker run
    -d -p 6379:6379 --name myredis docker.io/redis
  2. 引入Redis的starter
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
  3. 配置redis
    • application.properties中配置
      spring.redis.host=192.168.13.129
    • 测试字符串
          //操作k-v都是String的
          @Autowired
          private StringRedisTemplate stringRedisTemplate;
      
          /**
           * Redis五大数据类型
           *  String: stringRedisTemplate.opsForValue() 操作字符串
           *  List:  stringRedisTemplate.opsForList() 操作列表
           *  Set: stringRedisTemplate.opsForSet() 操作集合
           *  Hash: stringRedisTemplate.opsForHash() 操作哈希
           *  ZSet: stringRedisTemplate.opsForZSet() 操作有序集合
           *
           *  而redisTemplate只是泛型和stringRedisTemplate不同
           */
          @Test
          public void test01() {
      
              ValueOperations<String, String> stringOps = stringRedisTemplate.opsForValue();
              //给Redis中保存数据
              //stringOps.append("msg", "hello");
      
              //读取数据
              //String msg = stringOps.get("msg");
              //System.out.println(msg);
      
              //列表中添加数据
              ListOperations<String, String> listOps = stringRedisTemplate.opsForList();
              listOps.leftPush("mylist", "1");
              listOps.leftPush("mylist", "2");
          }
    • 测试对象
          //k-v都是Object的
          @Autowired
          private RedisTemplate redisTemplate;
      
          //测试保存对象
          @Test
          public void test02() {
              //这里Employee必须序列化
              //默认如果保存对象, 使用jdk序列化机制, 序列化后的数据保存到redis中
              Employee emp = employeeMapper.getEmpById(1);
              redisTemplate.opsForValue().set("emp-01", emp);
      }
      • 但此时保存到Redis中的是序列化数据, 很难看.
    • 将对象以json形式保存
      • 配置自定义序列化规则
        @Configuration
        public class MyRedisConfig {
        
            @Bean
            public RedisTemplate<Object, Employee> redisTemplate(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;
            }
        }
      • 使用
            @Autowired
            private RedisTemplate<Object, Employee> empTemplate;
        
            //测试保存对象
            @Test
            public void test02() {
                Employee emp = employeeMapper.getEmpById(1);
        
                //1.将数据以json的方式保存
                //redisTemplate有默认的序列化规则
                //改变默认的序列化规则
                empTemplate.opsForValue().set("emp-02", emp);
            }
  4. 测试缓存
    • 原理: CacheManager -> Cache缓存组件 -> 缓存组件给缓存中CRUD数据.
      • 引入redis的starter后, 容器中保存的是RedisCacheManager.
      • RedisCacheManager帮我们创建RedisCache来作为缓存组件. RedisCache通过Redis缓存数据.
      • 默认保存数据k-v, 都是Object, 利用序列化保存
    • 如何保存为json?
      • 默认创建的RedisCacheManager操作Redis时使用的RedisTemplate<Object, Object>
      • 而默认使用jdk序列化机制.
    • 自定义CacheManager
          @Bean
          public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
              //初始化一个RedisCacheWriter
              RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
              //设置CacheManager的值序列化方式为json序列化
              GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
              RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair.fromSerializer(jsonRedisSerializer);
              RedisCacheConfiguration redisCacheConfig = RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair);
      
              //设置默认超时过期时间30秒
              redisCacheConfig.entryTtl(Duration.ofSeconds(30));
      
              //初始化RedisCacheManager
              return new RedisCacheManager(redisCacheWriter, redisCacheConfig);
          }
  5. 之后, 上述测试缓存的例子如localhost:8080/emp/1 会将结果以json的形式保存在Redis中了.
原文地址:https://www.cnblogs.com/binwenhome/p/13126362.html