• Spring Boot 缓存的基本用法


    一、目的

    ​ 缓存是用于提升系统的性能,加速系统的访问,降低成本的一种技术。可以将一些高频、热点信息放入缓存中,避免直接从数据库中查询,如商品的页面信息这种经常被访问的数据。

    二、JSR-107 缓存规范

    为了统一缓存的开发规范、提高系统的扩展性和最小化开发成本等,J2EE 发布了 JSR-107 缓存规范。

    Java Caching 定义了 5 个核心接口,分别是CachingProvider, CacheManager, Cache, Entry
    Expiry

    • CachingProvider定义了创建、配置、获取、管理和控制多个 CacheManager。一个应用可
      以在运行期访问多个CachingProvider
    • CacheManager定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache
      存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。
    • Cache是一个类似 Map 的数据结构并临时存储以 Key 为索引的值。一个Cache仅被一个CacheManager所拥有。
    • Entry是一个存储在Cache中的 key-value 对。
    • Expiry 每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期
      的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过 ExpiryPolicy 设置。

    三、Spring 缓存抽象

    Spring 从 3.1 开始定义了 org.springframework.cache.Cache和 org.springframework.cache.CacheManager接口来统一不同的缓存技术并支持使用 JCache(JSR-107)注解简化我们开发。


    几个重要概念&注解:

    Cache 缓存接口,定义缓存操作。实现有:RedisCacheEhCacheCacheConcurrentMapCache
    CacheManager 缓存管理器,管理各种缓存(Cache)组件
    @Cacheable 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
    @CacheEvict 清空缓存
    @CachePut 保证方法被调用,又希望结果被缓存。
    @EnableCaching 开启基于注解的缓存
    keyGenerator 缓存数据时key生成策略
    serialize 缓存数据时value序列化策略

    四、Demo

    1、使用 IDEA 创建 Spring Boot 项目




    2、创建相应的数据表

    SQL 文件:

    
    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、创建 Java Bean 封装数据

    package com.yunche.bean;
    
    public class Department {
       
       private Integer id;
       private String departmentName;
       
       
       public Department() {
          super();
          // TODO Auto-generated constructor stub
       }
       public Department(Integer id, String departmentName) {
          super();
          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 + "]";
       }
    
    }
    
    package com.yunche.bean;
    
    public class Employee {
       
       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) {
          super();
          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 + "]";
       }
    }
    

    4、整合 MyBatis

    1.配置数据源信息

    application.properties:

    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/spring_cache?useSSL=false&serverTimezone=Asia/Shanghai
    spring.datasource.username=root
    spring.datasource.password=123456
    
    #开启驼峰转换规则
    mybatis.configuration.map-underscore-to-camel-case=true
    

    2.使用注解版 MyBatis

    1)、@MapperScan 指定需要扫描 Mapper 接口所在的包

    package com.yunche;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @MapperScan("com.yunche.mapper")
    @SpringBootApplication
    public class SpringbootDemoCacheApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SpringbootDemoCacheApplication.class, args);
        }
    
    }
    

    2)、定义 Mapper 接口中的方法

    package com.yunche.mapper;
    
    import com.yunche.bean.Employee;
    import org.apache.ibatis.annotations.*;
    
    /**
     * @ClassName: EmployeeMapper
     * @Description:
     * @author: yunche
     * @date: 2019/02/01
     */
    @Mapper
    public interface EmployeeMapper {
    
        @Select("SELECT * FROM employee WHERE id = #{id}")
        Employee getEmpById(Integer id);
    
        @Update("UPDATE employee set lastName=#{lastName},email=#{email},gender=#{gender},d_id=#{dId} WHERE id=#{id}")
        void updateEmp(Employee employee);
    
        @Delete("DELETE FROM employee WHERE id=#{id}")
        void deleteEmp(Integer id);
    
        @Insert("INSERT INTO employee(lastName,email,gender,d_id) VALUES(#{lastName},#{email},#{gender},#{d_id})")
        void insertEmp(Employee employee);
    }
    

    单元测试 Mapper:

    package com.yunche;
    
    import com.yunche.bean.Employee;
    import com.yunche.mapper.EmployeeMapper;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class SpringbootDemoCacheApplicationTests {
    
        @Autowired
        EmployeeMapper employeeMapper;
    
        @Test
        public void contextLoads() {
        }
    
        @Test
        public void testMapper() {
            Employee employee = employeeMapper.getEmpById(1);
            System.out.println(employee);
        }
    
    }
    

    5、实现 Web 访问

    1)、添加 service 包

    package com.yunche.service;
    
    import com.yunche.bean.Employee;
    import com.yunche.mapper.EmployeeMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    /**
     * @ClassName: EmployeeService
     * @Description:
     * @author: yunche
     * @date: 2019/02/01
     */
    @Service
    public class EmployeeService {
        @Autowired
        EmployeeMapper employeeMapper;
        
        public Employee getEmp(Integer id){
            System.out.println("查询"+id+"号员工");
            Employee emp = employeeMapper.getEmpById(id);
            return emp;
        }
    }
    

    2)、添加 controller 包

    package com.yunche.controller;
    
    import com.yunche.bean.Employee;
    import com.yunche.service.EmployeeService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @ClassName: EmployeeController
     * @Description:
     * @author: yunche
     * @date: 2019/02/01
     */
    @RestController
    public class EmployeeController {
        @Autowired
        EmployeeService employeeService;
    
        @GetMapping("/emp/{id}")
        public Employee getEmployee(@PathVariable("id") Integer id) {
            Employee employee = employeeService.getEmp(id);
            return employee;
        }
    }
    

    测试:


    6、缓存初体验

    EmployeeService:

    /**
     * 将方法的运行结果进行缓存,再次运行该方法时从缓存中返回结果
     * CacheManager 管理多个 Cache 组件,Cache 组件进行缓存的 CRUD,每一个缓存组件都有唯一一个名字。
     * 属性:
     *      cacheNames/value:指定缓存组件的名字
     *      key:缓存数据键值对的 key,默认是方法的参数的值
     *      keyGenerator:key 的生成器,可以自己指定 key 的生成器的组件 id 与 key 二选一
     *      cacheManager:指定缓存管理器 cacheResolver:缓存解析器,二者二选一
     *      condition/unless(否定条件):符合指定条件的情况下才缓存
     *
     * @param id
     * @return
     */
    @Cacheable(cacheNames = {"emp"}, condition = "#id % 2 == 1")
    public Employee getEmp(Integer id){
        System.out.println("查询"+id+"号员工");
        Employee emp = employeeMapper.getEmpById(id);
        return emp;
    }
    

    Cache SpEL available metadata

    名字 位置 描述 示例
    methodName root object 当前被调用的方法名 #root.methodName
    method root object 当前被调用的方法 #root.method.name
    target root object 当前被调用的目标对象 #root.target
    targetClass root object 当前被调用的目标对象类 #root.targetClass
    args root object 当前被调用的方法的参数列表 #root.args[0]
    caches root object 当前方法调用使用的缓存列表(如@Cacheable(value={"cache1", "cache2"})),则有两个 cache #root.caches[0].name
    argument name evaluation context 方法参数的名字. 可以直接 #参数名 ,也可以使用 #p0 或#a0 的形式,0 代表参数的索引; #iban 、 #a0 、 #p0
    result evaluation context 方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,’cache put’的表达式 ’cache evict’的表达式 beforeInvocation=false) #result

    EmployeeService:

    /**
     * 更新缓存,既调用方法 (更新数据库),又更新缓存
     * 测试步骤:1、查询 1 号员工,将其纳入缓存
     *          2、修改 1 号员工
     *          3、再次查询 1 号员工,若结果是从缓存中查询数据,且数据为更新后的缓存则测试通过
     * @param employee
     * @return
     */
    @CachePut(cacheNames = {"emp"}, key = "#result.id")
    public Employee updateEmp(Employee employee) {
        System.out.println("更新" + employee.getdId() + "号员工");
        employeeMapper.updateEmp(employee);
        return employee;
    }
    

    EmployeeController:

    @GetMapping("/emp")
    public Employee updateEmployee(Employee employee) {
        Employee emp = employeeService.updateEmp(employee);
        return emp;
    }
    

    测试:




    EmployeeService:

    /**
     * 清空缓存
     *      beforeInvocation:默认为 false 表示在方法调用之后清空缓存,
     *                        若为 true,则表示在方法调用之前清空缓存
     * @param id
     */
    @CacheEvict(cacheNames = {"emp"}, beforeInvocation = true/*, key = "#id"*/)
    public void deleteEmp(Integer id) {
        System.out.println("删除" + id + "号员工");
        //employeeMapper.deleteEmp(id); 只测试缓存的删除效果
    }
    

    EmployeeController:

    @GetMapping("/delemp")
    public String deleteEmp(Integer id){
        employeeService.deleteEmp(id);
        return "success";
    }
    

    7、使用 redis 缓存中间件

    Spring 的缓存默认使用的是 ConcurrentMapCacheManager 下的 ConcurrentMapCache,是将数据保存在 ConcurrentMap<Object, Object> 中。而在开发中是使用缓存中间件:redis、memcached、ehcache 等。

    1.使用 docker 安装 redis(阿里云服务器)

    [root@izwz9d74k4cznxtxjeeur9z ~]# docker run -d -p 6379:6379 --name redis_cache docker.io/redis
    44f9e905a7db0c0933f3d35ce65dd7041fd985d2da00895713d9765b20781011
    

    2.使用 Redis Desktop Manager 连接阿里云服务器

    注意:要在阿里云服务器控制台添加安全规则,确认开放 6379 端口。

    3.引入 redis starter 启动器

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

    application.properties:

    spring.redis.host=x.x.x.x #redis 服务器地址
    

    单元测试 redis 键值是字符串类型 :

    /**
     * 用于操作 key 和 value 都是字符串的键值
     */
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    
    /**
     * 用于操作 key 和 value 都是对象的键值
     */
    @Autowired
    RedisTemplate redisTemplate;
    
    /**
     * 测试保存字符串
     * Redis 常见的五大数据类型
     *  String(字符串)、List(列表)、Set(集合)、Hash(散列)、ZSet(有序集合)
     *  stringRedisTemplate.opsForValue()[String(字符串)]
     *  stringRedisTemplate.opsForList()[List(列表)]
     *  stringRedisTemplate.opsForSet()[Set(集合)]
     *  stringRedisTemplate.opsForHash()[Hash(散列)]
     *  stringRedisTemplate.opsForZSet()[ZSet(有序集合)]
     */
    @Test
    public void testStringRedis() {
        // 追加一个字符串类型的 value
        stringRedisTemplate.opsForValue().append("msg", "hello");
        //读取一个字符串类型
        String value = stringRedisTemplate.opsForValue().get("msg");
        System.out.println(value);
    }
    

    单元测试 redis 存储对象:

    package com.yunche.config;
    
    import com.yunche.bean.Employee;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    
    import java.net.UnknownHostException;
    
    /**
     * @ClassName: MyRedisConfig
     * @Description:
     * @author: yunche
     * @date: 2019/02/02
     */
    @Configuration
    public class MyRedisConfig {
    
        @Bean
        public RedisTemplate<Object, Employee> empRedisTemplate(
                RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
            RedisTemplate<Object, Employee> template = new RedisTemplate<>();
            template.setConnectionFactory(redisConnectionFactory);
            //改变默认的序列化器
            template.setDefaultSerializer(new Jackson2JsonRedisSerializer(Employee.class));
            return template;
        }
    }
    
    @Autowired
    RedisTemplate<Object, Employee> empRedisTemplate;
    /**
     * 测试保存对象
     *  1、使对象的类实现 Serializable 接口,表示该类的对象可以序列化
     *  2、使用自定义 RedisTemplate 改变默认的序列化机制(jdk)方便观察
     */
    @Test
    public void testObjectRedis() {
        Employee employee = employeeMapper.getEmpById(1);
        empRedisTemplate.opsForValue().set("emp-01", employee);
    }
    

    4.自定义 RedisCacheManager

    此时使用的缓存管理器为 RedisCacheManager,为了使缓存到 Redis 里面的数据达到如上图所示的效果,我们就需要自定义 RedisCacheManager 改变 RedisTemplate 的默认序列化机制(jdk)。

    MyRedisConfig:

        // Spring Boot 1.x
    //    @Bean
    //    public RedisCacheManager employeeCacheManager(RedisTemplate<Object, Employee> empRedisTemplate){
    //        RedisCacheManager cacheManager = new RedisCacheManager(empRedisTemplate);
    //        //key 多了一个前缀
    //
    //        //使用前缀,默认会将 CacheName 作为 key 的前缀
    //        cacheManager.setUsePrefix(true);
    //
    //        return cacheManager;
    //    }
        
        /**
         * Spring Boot 2.x 以后 RedisCacheManager 构造函数不再接受 RedisTemplate 参数
         * @param factory
         * @return
         */
        @Bean
        public RedisCacheManager empRedisCacheManager(RedisConnectionFactory factory) {
            RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer(Employee.class))); //使用 Jackson2JsonRedisSerialize
            RedisCacheManager redisCacheManager = RedisCacheManager.builder(factory)
                    .cacheDefaults(config)
                    .transactionAware()
                    .build();
                return redisCacheManager;
        }
    

    访问 http://localhost:8080/emp/1,Redis 缓存结果如下:


    五、参考资料

    尚硅谷.Spring Boot 高级篇

  • 相关阅读:
    C#文件操作
    C# 本地txt文件读取至comboBox下拉列表
    自动部署war包脚本
    设置eclipse包资源管理器的背景色
    Failed to connect to ourself!错误解决办法
    Keepalived+LVS+MysqlCluster(7.1.10)架构方案(一)
    Mysql Cluster在线添加数据节点
    在Linux下使用perl通过unixODBC连接SQLServer2000
    MySQL中四舍五入的实现
    关于Temporary error: 1218错误的思考
  • 原文地址:https://www.cnblogs.com/yunche/p/10349214.html
Copyright © 2020-2023  润新知