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 高級篇


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM