1. 程式人生 > 實用技巧 >【SpringBoot1.x】SpringBoot1.x 快取

【SpringBoot1.x】SpringBoot1.x 快取

SpringBoot1.x 快取

文章原始碼

JSR107

Java Caching 定義了 5 個核心介面,分別為:

  • CachingProvider 定義了建立、配置、獲取、管理和控制多個 CacheManager。一個應用可以在執行期訪問多個 CachingProvider。
  • CacheManager 定義了建立、配置、獲取、管理和控制多個唯一命名的 Cache,這些 Cache 存在於 CacheManager 的上下文中。一個 CacheManager 僅被一個 CachingProvider 擁有。
  • Cache 是一個類似 Map 的資料結構並臨時儲存以 Key 為索引的值。一個 Chache 僅被一個 CacheManager 擁有。
  • Entry 是一個儲存在 Cache 中的 key-value 對。
  • Expiry 指每一個儲存在 Chche 中的條目有一個定義的有效期,一旦超過這個時間,條目就為過期的狀態。一旦過期,條目將不可訪問、更新和刪除。有效期可以通過 ExpiryPolicy 設定。

Spring 快取抽象

Spring3.1 後定義了 org.springframework.cache.Cacheorg.springframework.cache.CacheManager 介面來統一不同的快取技術,並支援使用 JCache(JSR107) 註解簡化我們開發。

  • Cache 介面 為快取的元件規範定義,包含快取的各種操作集合;
  • Cache 介面下 Spring 提供了各種 xxxCache 的實現,如 RedisCache、EhCacheCache、ConcurrentMapCache等;
  • 每次呼叫需要快取功能的方法時,Spring 會檢查指定引數的指定目標方法是否被呼叫過。如果有就直接從快取中獲取方法呼叫後的結果,如果沒有就呼叫方法就快取結果後返回給使用者。下次呼叫直接從快取中獲取。
  • 使用 Spring 快取抽象需注意:
    • 確定方法需要被快取以及它們的快取策略;
    • 從快取中讀取之前快取儲存的資料。

重要概念及快取註解

  • Chche 快取介面,定義快取操作。實現有 RedisCache、EhCacheCache、ConcurrentMapCache等。
  • ChacheManager 快取管理器,管理各種快取元件。
  • @EnableCaching 開啟基於註解的快取,用在主配置類上。
  • @Cacheable 能夠根據方法的請求引數對其結果進行快取
  • @CachePut 即呼叫方法,又更新快取資料
  • @CacheEvict 清除快取
  • @Caching 定義複雜的快取規則

@Cacheable@CachePut@Caching 等註解主要的引數:

  • cacheNames/value 快取的名字,即將方法的返回結果放在那個快取中,可以指定多個
  • key 快取的資料,為空時預設是使用方法引數的值,可以為 SpEL 表示式,例如 #id
  • keyGenerator key 的生成器,可以自己指定 key 的生成器的元件 id,它與 key 二選一
  • cacheManager 快取管理器
  • cacheResolver 快取解析器,它與 cacheManager 二選一
  • condition 執行符合條件才快取
  • unless 執行不符合條件才快取
  • sync 是否使用非同步模式
  • allEntries 是否清空所有快取,預設為 false,如果指定為 true,則方法呼叫後將立即清空所有快取
  • beforeInvocation 預設為 false,即快取清除操作是在方法之後執行,出現異常不會清除快取。如果指定為 true,即快取清除操作是在方法之前執行。無論是否出現異常,快取都會清除

SpEL 表示式

名字 位置 描述 示例
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 代表引數的索引 #id#a0#p0
result evaluation context 方法執行後的返回值,僅當方法執行之後的判斷有效 #result

快取使用

使用步驟

  • 第一步:建立相應表結構
  • 第二步:編寫相應的實體類
  • 第三步:整合 MyBatis
    • 配置資料來源資訊
    • 使用註解版的 Mybatis,即在主配置類上加上 @MapperScan
  • 第四步:使用快取
    • 在主配置類上加上 @EnableCaching
    • 在業務層方法加上 @Cacheable@CachePut@CacheEvict@Caching 等註解

快取配置原理

  • 使用 CacheAutoConfiguration 自動配置類
  • 掃描到各種快取的配置類:
    • 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.GuavaCacheConfiguration
    • org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration,這是預設生效的
    • org.springframework.boot.autoconfigure.cache.NoOpCacheConfiguration
  • 給容器中註冊了一個 CacheManager,預設是 ConcurrentMapCacheManager
  • 建立和獲取 ConcurrentMapCache 型別的快取元件,它的作用是將資料儲存在 ConcurrentMap 中

EmployeeService:

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : 員工業務層
 */

@Service
public class EmployeeService {

    @Autowired
    EmployeeMapper employeeMapper;

    /**
     *
     * 根據 Id 查詢員工資訊
     *
     * @Cacheable 執行流程:
     * 1. 方法執行之前,先去查詢 Cache 快取元件,按照 cacheNames 指定的名字獲取,第一次獲取快取時如果沒有 Cache 元件會自動建立
     * 2. 去 Cache 中查詢快取的內容,使用一個 key,預設是方法的引數。也可以按照某種策略生成,預設使用 SimpleKeyGenerator 生成 key
     *    SimpleKeyGenerator 生成 key 的預設策略為:
     *      如果沒有引數,key = new SimpleKey()
     *      如果有一個引數,key = 引數的值
     *      如果有多個引數,key = new SimpleKey(params)
     * 3. 有查詢到快取,則直接使用快取;沒有查詢到快取,則呼叫目標方法並將目標方法返回的結果放進快取中
     */
    @Cacheable(cacheNames = {"emp"})
    public Employee getEmp(Integer id) {
        return employeeMapper.getEmpById(id);
    }

    /**
     *
     * 更新員工資訊
     *
     * @CachePut 執行流程
     * 1. 先呼叫目標方法
     * 2. 將目標方法的結果快取起來
     * 3. 比較適用與修改了資料庫某個資料後,更新快取
     */
    @CachePut(value = {"emp"}, key = "#result.id")
    public Employee updateEmp(Employee employee) {
        employeeMapper.updateEmp(employee);
        return  employee;
    }

    /**
     *
     * 刪除員工資訊
     *
     */
    @CacheEvict(value = {"emp"}, key = "#id")
    public void deleteEmp(Integer id) {
        employeeMapper.deleteEmpById(id);
    }

    /**
     *
     * 根據 lastName 查詢員工資訊
     *
     * @Caching 定義複雜的快取規則
     */
    @Caching(
            cacheable = {
                    @Cacheable(value = {"emp"}, key = "#lastName")
            },
            put = {
                    @CachePut(value = {"emp"}, key = "#result.id"),
                    @CachePut(value = {"emp"}, key = "#result.email")
            }
    )
    public Employee getEmp(String lastName) {
        List<Employee> employees = employeeMapper.getEmpByName(lastName);
        if (employees.isEmpty()) {
            return null;
        }
        return employees.get(0);
    }
}

EmployeeController:

/**
 * @Author : parzulpan
 * @Time : 2020-12
 * @Desc : 員工控制器
 */

@RestController
public class EmployeeController {

    @Autowired
    EmployeeService employeeService;

    // http://localhost:8080/emp/1
    @GetMapping("/emp/{id}")
    public Employee getEmp(@PathVariable("id") Integer id) {
        return employeeService.getEmp(id);
    }

    // http://localhost:8080/emp?id=1&lastName=ha&[email protected]&gender=0&dId=1001
    @GetMapping("/emp")
    public Employee updateEmp(Employee employee) {
        return employeeService.updateEmp(employee);
    }

    // http://localhost:8080/empDel?id=1
    @GetMapping("/empDel")
    public String deleteEmp(Integer id) {
        employeeService.deleteEmp(id);
        return "success";
    }

    // http://localhost:8080/emp/lastName/parzulpan
    @GetMapping("/emp/lastName/{lastName}")
    public Employee getEmp(@PathVariable("lastName") String lastName) {
        return employeeService.getEmp(lastName);
    }
}

開啟 debug 配置後,可以觀察快取的作用:

logging:
  level:
    cn.parzulpan.mapper: debug

可以使用 @CacheConfig,它指定這個類的快取配置,通常用於抽取公共配置。

@CacheConfig(cacheNames = {"emp"})
@Service
public class EmployeeService {}

整合 Redis

使用步驟:

  • 引入 Redis 啟動器依賴

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
    
  • 配置 Redis

    spring:
    # 配置 Redis
    redis:
        host: localhost
    
  • 測試 Redis

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class IntegrationCacheApplicationTests {
    
        @Autowired
        RedisTemplate redisTemplate;    // k-v 都是物件
    
        @Autowired
        StringRedisTemplate stringRedisTemplate;    // k-v 都是字串
    
        @Test
        public void testRedisString() {
            // 字串操作
            // String 型別 是 Redis 中最基本的資料型別,一個 key 對應一個 value 。
    
            stringRedisTemplate.opsForValue().set("stringMsg", "hello");
            stringRedisTemplate.opsForValue().append("stringMsg", "world");
    
            String msg = stringRedisTemplate.opsForValue().get("stringMsg");
            System.out.println(msg);
        }
    
        @Test
        public void testRedisList() {
            // 列表操作
            // List 型別 是簡單的字串列表,按照插入順序排序。可以新增一個元素到列表的頭部(左邊)或者尾部(右邊)。
            ListOperations<String, String> ops = redisTemplate.opsForList();
            ops.leftPush("listMsg", "hello");
            ops.leftPushAll("listMsg", "world", "parzulpan");
            List<String> listMsg = ops.range("listMsg", 0, 2);// 索引 0 到2的 listMsg
            System.out.println(listMsg.toString());
        }
    
        @Test
        public void testRedisSet() {
            // 集合操作
            // Set 型別 是 String 型別 的無序集合。它的特點是無序且唯一,它是通過雜湊表實現的,所以新增、刪除、查詢的複雜度都是 O(1)。
            SetOperations<String, String> ops = redisTemplate.opsForSet();
            ops.add("setMsg", "hello");
            ops.add("setMsg", "world", "parzulpan");
            Set<String> setMsg = ops.members("setMsg"); //  取 set
            System.out.println(setMsg.toString());
        }
    
        @Test
        public void testRedisZSet() {
            // 有序集合操作
            // ZSet 型別 和 Set 型別 一樣,也是 String 型別元素的集合,且不允許有重複的成員。
            // 不同的是每個元素都會關聯一個 double 型別 的分數,它正是通過分數來為集合中的成員進行從小到大的排序。
            // ZSet 型別的成員是唯一的,但分數(score) 卻可以重複。
            ZSetOperations<String, String> ops = redisTemplate.opsForZSet();
            ops.add("zsetMsg", "hello", 1);
            ops.add("zsetMsg", "parzulpan", 3);
            ops.add("zsetMsg", "world", 2);
            Set<String> zsetMsg = ops.range("zsetMsg", 0, 2);
            System.out.println(zsetMsg.toString());
        }
    
        @Test
        public void testRedisHash() {
            // 雜湊操作
            // Hash 型別 是一個鍵值對的集合。它是一個 String 型別 的 field 和 value 組合的對映表,它特別適合用於儲存物件。
            HashOperations<String, String, String> ops = redisTemplate.opsForHash();
            ops.put("hashMsg", "key1", "hello");
            ops.put("hashMsg", "key2", "world");
            ops.put("hashMsg", "key3", "parzulpan");
            String key2 = ops.get("hashMsg", "key2");
            System.out.println(key2);
        }
    }
    
  • 對於上面的測試,Redis 預設儲存物件,使用 JDK 序列化機制,序列化後的資料儲存到 redis 中。可以使用自定義的序列化器。值得注意的是,無論是 json 序列化還是 jdk 序列化,redis 接受的都是字串的文字,而 jdk 的序列化方式字串會把 json 序列化方式字串大幾倍,效能較差,所以一般都使用自定義的序列化器。

    /**
     * @Author : parzulpan
     * @Time : 2021-01
     * @Desc : 自定義 Redis 配置類
     */
    
    @Configuration
    public class CustomRedisConfig {
    
        // 使用 Jackson 序列化器,不使用預設的 JDK 的
        @Bean
        public RedisTemplate<Object, Employee> employeeRedisTemplate(RedisConnectionFactory rcf){
            RedisTemplate<Object, Employee> template = new RedisTemplate<>();
            template.setConnectionFactory(rcf);
            Jackson2JsonRedisSerializer<Employee> jrs = new Jackson2JsonRedisSerializer<>(Employee.class);
            template.setDefaultSerializer(jrs);
            return template;
        }
    }
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class IntegrationCacheApplicationTests {
        @Autowired
        RedisTemplate employeeRedisTemplate;    // 使用自定義的 RedisTemplate
    
        @Test
        public void testEmployeeRedisTemplate() {
            ValueOperations ops = employeeRedisTemplate.opsForValue();
            Employee employee = employeeMapper.getEmpById(1);
            ops.set("emp-01", employee);
        }
    }
    
  • 使用 Redis 快取,它的原理是:

    • CacheManager,生成一個 Cache 快取元件來實際給快取中存取資料

    • 引入 redis 的 starter,容器中儲存的是 RedisCacheManager;

    • RedisCacheManager 幫我們建立 RedisCache 來作為快取元件,RedisCache 通過操作 redis 快取資料

    • 預設儲存資料 k-v 都是 Object。利用序列化儲存,所以實體類需要繼承 Serializable。它預設使用的是 RedisTemplate<Object, Object>,它是 jdk 預設的序列化機制

    • 可以通過自定義 CacheManager,更改序列化機制:

      /**
       * @Author : parzulpan
       * @Time : 2021-01
       * @Desc : 自定義 Redis 配置類
       */
      
      @Configuration
      public class CustomRedisConfig {
      
          // 使用 Jackson 序列化器,不使用預設的 JDK 的
          @Bean
          public RedisTemplate<Object, Employee> employeeRedisTemplate(RedisConnectionFactory rcf){
              RedisTemplate<Object, Employee> template = new RedisTemplate<>();
              template.setConnectionFactory(rcf);
              Jackson2JsonRedisSerializer<Employee> jrs = new Jackson2JsonRedisSerializer<>(Employee.class);
              template.setDefaultSerializer(jrs);
              return template;
          }
      
          // 自定義快取管理器
          @Bean
          public RedisCacheManager employeeCacheManager(RedisTemplate<Object, Employee> employeeRedisTemplate) {
              RedisCacheManager redisCacheManager = new RedisCacheManager(employeeRedisTemplate);
      
              // 使用字首,預設將 CacheName 作為 key 的字首
              redisCacheManager.setUsePrefix(true);
      
              return redisCacheManager;
          }
      }
      
  • 使用示例:

    /**
     * @Author : parzulpan
     * @Time : 2021-01
     * @Desc : 部門業務層
     */
    
    @Service
    public class DepartmentService {
    
        @Autowired
        DepartmentMapper departmentMapper;
    
        @Autowired
        RedisCacheManager departmentCacheManager;
    
        // 註解的方式
        @Cacheable(cacheNames = "dept", cacheManager = "departmentCacheManager")
        public Department getDeptById(Integer id) {
            return departmentMapper.getDeptById(id);
        }
    
        // api 呼叫的方式
        public Department getDeptById2(Integer id) {
            Department department = departmentMapper.getDeptById(id);
    
            // 獲取某個快取
            Cache dept = departmentCacheManager.getCache("dept");
            dept.put("dept2:" + id, department);
    
            return department;
        }
    }
    

總之,相對於預設的 Cache,使用 Redis,需要多寫如下的一個 Redis 配置類:

/**
 * @Author : parzulpan
 * @Time : 2021-01
 * @Desc : 自定義 Redis 配置類
 */

@Configuration
public class CustomRedisConfig {

    // 使用 Jackson 序列化器,不使用預設的 JDK 的
    @Bean
    public RedisTemplate<Object, Employee> employeeRedisTemplate(RedisConnectionFactory rcf){
        RedisTemplate<Object, Employee> template = new RedisTemplate<>();
        template.setConnectionFactory(rcf);
        Jackson2JsonRedisSerializer<Employee> jrs = new Jackson2JsonRedisSerializer<>(Employee.class);
        template.setDefaultSerializer(jrs);
        return template;
    }

    @Bean
    public RedisTemplate<Object, Department> departmentRedisTemplate(RedisConnectionFactory rcf){
        RedisTemplate<Object, Department> template = new RedisTemplate<>();
        template.setConnectionFactory(rcf);
        Jackson2JsonRedisSerializer<Department> jrs = new Jackson2JsonRedisSerializer<>(Department.class);
        template.setDefaultSerializer(jrs);
        return template;
    }

    // 自定義快取管理器
    @Primary    // 將其作為預設的
    @Bean
    public RedisCacheManager employeeCacheManager(RedisTemplate<Object, Employee> employeeRedisTemplate) {
        RedisCacheManager redisCacheManager = new RedisCacheManager(employeeRedisTemplate);

        // 使用字首,預設將 CacheName 作為 key 的字首
        redisCacheManager.setUsePrefix(true);

        return redisCacheManager;
    }

    @Bean
    public RedisCacheManager departmentCacheManager(RedisTemplate<Object, Department> departmentRedisTemplate) {
        RedisCacheManager redisCacheManager = new RedisCacheManager(departmentRedisTemplate);

        // 使用字首,預設將 CacheName 作為 key 的字首
        redisCacheManager.setUsePrefix(true);

        return redisCacheManager;
    }
}

練習和總結