java框架之SpringBoot(11)-緩存抽象及整合Redis

Spring緩存抽象

介紹

Spring 從 3.1 版本開始定義了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口來統一不一樣的緩存技術,並支持使用 JCache(JSR107)註解簡化咱們開發。html

關於 JSR107:

Java Caching 定義了 5 個核心接口,分別是 CachingProvider、CacheManager、Cache、Entry 和Expiry。java

  • CachingProvider :能夠用來建立、配置、獲取、管理和控制多個 CacheManager。一個應用能夠在運行期間訪問多個 CachingProvider。
  • CacheManager: 能夠用來建立、配置、獲取、管理和控制多個惟一命名的 Cache,這些 Cache 存在於 CacheManager 的上下文中。一個 CacheManager 僅被一個 CachingProvider 所擁有。
  • Cache :是一個相似 Map 數據結構並臨時存儲以 key 爲索引的值。一個 Cache 僅被一個 CacheManager 所擁有。
  • Entry :是一個存儲在 Cache 中的 key-value 對。
  • Expiry:每個存儲在 Cache 中的條目有一個定義的有效期。一旦超過這個時間,條目就變動爲過時的狀態。一旦過時,條目將不可訪問、更新和刪除。緩存有效期能夠經過 ExpiryPolicy 設置。
因爲 JSR107 的使用相對來講比較繁雜,因此這裏咱們使用 Spring 提供的緩存抽象。
  • Cache 接口爲緩存的組件規範定義,包含緩存的各類操做集合。
  • Cache 接口下 Spring 提供了各類 Cache 的實現,如 RedisCache、EhCacheCache、ConcurrentMapCache 等。
關於Spring緩存抽象的使用:

每次調用具備緩存功能的方法時,Spring 會檢查指定目標方法是否已經被調用過,若是有就直接從緩存中獲取方法調用後的結果,若是沒有就調用方法並緩存結果後返回,下次調用就直接從緩存中獲取。mysql

使用 Spring 緩存抽象時咱們須要關注如下兩點:web

  1. 肯定方法須要被緩存以及它們的緩存策略。
  2. 從緩存中讀取以前緩存存儲的數據。

概念&註解

Cache 緩存接口,定義緩存操做。實現有:RedisCache、EhCacheCache、ConcurrentMapCache 等。
@CacheManager 緩存管理器,管理各類緩存(Cache)組件。
@Cacheable 主要針對方法配置,可以根據方法的請求參數對其結果進行緩存。
@CacheEvict 清空緩存。
@CachePut 保證方法被調用,又但願結果被緩存。
@EnableCaching 開啓基於註解的緩存。
keyGenerator 緩存數據時 key 的生成策略。
serialize 緩存數據時 value 的序列化策略。

簡單使用

搭建測試環境

一、新建 SpringBoot 項目,引入以下場景啓動器:Cache、Web、Mysql、MyBatis。redis

二、初始化測試數據:spring

SET FOREIGN_KEY_CHECKS=0;

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(40) DEFAULT NULL,
  `gender` int(11) DEFAULT NULL COMMENT '0:女 1:男',
  `birthday` date DEFAULT NULL,
  `address` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', '張三', '1', '1997-02-23', '北京');
INSERT INTO `user` VALUES ('2', '李四', '0', '1998-02-03', '武漢');
INSERT INTO `user` VALUES ('3', '王五', '1', '1996-06-04', '上海');
user.sql

三、編寫與表對應的 JavaBean:sql

package com.springboot.bean;

import java.util.Date;

public class User {
    private Integer id;
    private String name;
    private Integer gender;
    private Date birthday;
    private String address;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getGender() {
        return gender;
    }

    public void setGender(Integer gender) {
        this.gender = gender;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", gender=" + gender +
                ", birthday=" + birthday +
                ", address='" + address + '\'' +
                '}';
    }
}
com.springboot.bean.User

四、配置數據庫鏈接信息:數據庫

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.202.136:3306/springboot_cache
    driver-class-name: com.mysql.jdbc.Driver
application.yml

五、編寫測試Mapper:apache

package com.springboot.mapper;

import com.springboot.bean.User;
import org.apache.ibatis.annotations.*;

import java.util.List;

public interface UserMappper {
    @Select("select * from user")
    public List<User> getAll();

    @Select("select * from user where id=#{id}")
    public User getById(Integer id);

    @Delete("delete from user where id=#{id}")
    public void deleteById(Integer id);

    @Options(useGeneratedKeys = true,keyProperty = "id")
    @Insert("insert into user(name,gender,birthday,address) values(#{name},#{gender},#{birthday},#{address})")
    public void add(User user);

    @Update("update user set name=#{name},gender=#{gender},birthday=#{birthday},address=#{address} where id=#{id}")
    public void update(User user);

    @Select("select * from user where name=#{name}")
    public User getByName(String name);
}
com.springboot.mapper.UserMappper

六、編寫 Service 並使用註解緩存功能:json

package com.springboot.service;

import com.springboot.bean.User;
import com.springboot.mapper.UserMappper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {
    @Autowired
    private UserMappper userMappper;

    @Cacheable(cacheNames = {"users"})
    public List<User> getAll(){
        System.out.println("UserService.getAll() 執行了");
        return userMappper.getAll();
    }
}
com.springboot.service.UserService

七、配置 mapper 包掃描,開啓註解緩存:

package com.springboot;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching // 開啓註解緩存
@MapperScan("com.springboot.mapper") // mapper 包掃描
public class CacheTestApplication {

    public static void main(String[] args) {
        SpringApplication.run(CacheTestApplication.class, args);
    }

}
com.springboot.CacheTestApplication

八、編寫 Controller 並測試:

package com.springboot.controller;

import com.springboot.bean.User;
import com.springboot.mapper.UserMappper;
import com.springboot.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/users")
    public List<User> getAll(){
        return userService.getAll();
        /*
        首次訪問會輸出如下內容:
            UserService.getAll() 執行了
        後續訪問不會輸出,說明實際上並無執行 userService.getAll() 方法,即未查詢數據庫從中獲取數據,而是從緩存中獲取
        緩存成功
         */
    }
}
com.springboot.controller.UserController

CacheManager 是用來管理多個 Cache 組件的,對緩存的 CRUD 操做實際上仍是經過 Cache 組件,而每個 Cache 組件有本身惟一名稱。

@Cacheable

主要針對方法配置,可以根據方法的請求參數對其結果進行緩存。

屬性

  • cacheNames/value:指定 Cache 組件名稱。
  • key:指定緩存數據所使用的 key,默認 key 使用參數值。好比 id 爲 1,那麼在 Cache 組件中的 key 就爲 1。

    key 還能夠經過 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"}) 就有兩個緩存組件 #root.caches[0].name
    argument name evaluation context 能夠直接使用 #參數名 來獲取對應參數值,也可使用 #p0 或 #a0 的形式,0 表明參數索引 #id、#a0、#p0
    result evaluation context 獲取方法執行後的返回值 #result
  • keyGenerator:經過 IoC 容器中的生成器組件 id 來指定 key 的生成器,key 與 keyGenerator 二選一使用。
  • cacheManager:指定緩存管理器 bean。
  • cacheSolver:指定緩存解析器 bean。
  • condition:指定條件,當符合指定條件時才進行緩存,也可以使用 SpEL 表達式同 key。
  • unless:指定條件,當符合指定條件時不進行緩存,也可以使用 SpEL 表達式同 key。
  • sync:是否使用異步模式,若是啓用,那麼 unless 屬性失效。

示例

@Cacheable(cacheNames = {"users"})
public User getById(Integer id) {
    System.out.println("UserService.getById(" + id + ") 執行了");
    return userMappper.getById(id);
}
com.springboot.service.UserService#getById
@GetMapping("/user/{id}")
public User getById(@PathVariable Integer id) {
    return userService.getById(id);
    /*
    首次訪問 http://localhost:8080/user/1 會輸出一下內容:
        UserService.getById(1) 執行了
    後續訪問不會輸出,說明實際上並無執行 userService.getById(1) 方法,即未查詢數據庫從中獲取數據,而是從緩存中獲取
    而若是訪問 http://localhost:8080/user/2 會輸出一下內容:
        UserService.getById(2) 執行了
    後續訪問也不會輸出,即會自動根據參數的不一樣進行緩存
    OK 緩存成功
     */
}
com.springboot.controller.UserController#getById

@CachePut

主要針對方法配置,目標方法正常執行完成後將其結果更新到緩存。

屬性

參考 @Cacheable 註解屬性

示例

/*
注意:這裏要保證 key 與 getById 方法所使用 key 的生成後的值相同,不然會出現更新緩存後經過 getById 獲取數據依舊爲舊數據的狀況
 */
@CachePut(cacheNames = {"users"}, key = "#user.id")
public User update(User user) {
    System.out.println("UserService.update(" + user + ") 執行了");
    userMappper.update(user);
    return user;
}
com.springboot.service.UserService#update
@PutMapping("/user")
public User update(User user){
    return userService.update(user);
    /*
    一、以 get 方式訪問 localhost:8080/user/1 會輸出一下內容:
            UserService.getById(1) 執行了
        後續訪問不會輸出
    二、以 put 方式訪問 localhost:8080/user,修改數據,輸出如下內容:
            UserService.update(User{id=1, name='張三new', gender=0, birthday=Wed Apr 03 00:00:00 CST 1996, address='深圳'}) 執行了
        每次訪問都會輸出
    三、以 get 方式訪問 localhost:8080/user/1 不會輸出內容,返回數據是更新後的數據
     */
}
com.springboot.controller.UserController#update

@CacheEvict

主要針對方法配置,默認在目標方法正常執行完成後清除緩存。

屬性

  • beforeInvocation:默認爲 false,標識是否在目標方法執行以前清除緩存。
  • allEntries:默認爲 false,標識是否清除緩存組件中全部內容。

更多可參考 @Cacheable 註解屬性

示例

/*
刪除緩存,key 能夠不指定,由於 key 默認就是以 id 爲基礎生成的
 */
@CacheEvict(cacheNames = {"users"}, key = "#id")
public void delete(Integer id) {
    System.out.println("UserService.delete(" + id + ") 執行了");
}
com.springboot.service.UserService#delete
@DeleteMapping("/user/{id}")
public void delete(@PathVariable Integer id){
    userService.delete(id);
    /*
    一、以 get 方式訪問 localhost:8080/user/1 會輸出如下內容
            UserService.getById(1) 執行了
        後續訪問不會輸出
    二、以 delete 方式訪問 localhost:8080/user/1 會輸出如下內容
            UserService.delete(1) 執行了
        每次訪問都會輸出
    三、以 get 方式再次訪問 localhost:8080/user/1 會輸出如下內容
            UserService.getById(1) 執行了
    即緩存被刪除了從新執行方法獲取了數據
     */
}
com.springboot.controller.UserController#delete

@Caching

主要針對方法配置,爲 @Cacheable、@CachePut、CacheEvict 的組合註解,經常使用於定製較複雜的緩存策略。

屬性

  • cacheable:放置 @Cacheable 註解數組。
  • put:放置 @CachePut 註解數組。
  • evict:放置 @CacheEvict 註解數組。

示例

@Caching(
        cacheable = {
                // 以 name 生成 key 進行緩存
                @Cacheable(value = "users", key = "#name")
        },
        put = {
                // 以 id 生成 key ,且執行結果不爲空時更新緩存
                @CachePut(value = "users", key = "#result.id", condition = "#result != null")
        },
        evict = {
                // name 爲 deleteAll 清除全部緩存
                @CacheEvict(value = "users", condition = "#name=='deleteAll'", allEntries = true)
        }
)
public User getByName(String name) {
    System.out.println("UserService.getByName(" + name + ") 執行了");
    return userMappper.getByName(name);
}
com.springboot.service.UserService#getByName
@GetMapping("/user/name/{name}")
public User getByName(@PathVariable String name){
    return userService.getByName(name);
    /*
    一、以 get 方式請求 localhost:8080/user/name/李四,輸出如下內容:
            UserService.getByName(李四) 執行了
        每次訪問都會輸出,由於 @CachePut 須要執行結果更新緩存
    二、以 get 方式請求 localhost:8080/user/2,沒有輸出內容,由於經過 [1] 已經完成了 id 爲 2 的緩存,直接從緩存中取出結果返回了
    三、以 get 方式請求 localhost:8080/user/name/deleteAll,輸出如下內容:
            UserService.getByName(deleteAll) 執行了
        每次訪問都會輸出,由於 @CachePut 須要執行結果更新緩存
    四、以 get 方式請求 localhost:8080/user/2,輸出如下內容:
            UserService.getById(2) 執行了
        後續訪問不會輸出,此次輸出的緣由是經過 [3] 清楚了全部緩存
     */
}
com.springboot.controller.UserController#getByName

@CacheConfig

一般用來標註在類上,用來定義公共緩存配置。

屬性

具備以下屬性:cachenames、keyGenerator、cacheManager、cacheResolver,可參考 @Cacheable 註解屬性

示例

package com.springboot.service;

import com.springboot.bean.User;
import com.springboot.mapper.UserMappper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;

/**
 * 由於下面方法都是使用同一個 Cache 組件,因此能夠在類上一次性指定全部方法使用的 Cache 組件名稱
 */
@CacheConfig(cacheNames = {"users"} )
@Service
public class UserService {
    @Autowired
    private UserMappper userMappper;

    @Cacheable(keyGenerator = "myKeyGenerator")
    public User getById(Integer id) {
        System.out.println("UserService.getById(" + id + ") 執行了");
        return userMappper.getById(id);
    }

    @CachePut(key = "#user.id")
    public User update(User user) {
        System.out.println("UserService.update(" + user + ") 執行了");
        userMappper.update(user);
        return user;
    }

    @CacheEvict(key = "#id")
    public void delete(Integer id) {
        System.out.println("UserService.delete(" + id + ") 執行了");
    }

    @Caching(
            cacheable = {
                    @Cacheable(key = "#name")
            },
            put = {
                    @CachePut(key = "#result.id", condition = "#result != null")
            },
            evict = {
                    @CacheEvict(condition = "#name=='deleteAll'", allEntries = true)
            }
    )
    public User getByName(String name) {
        System.out.println("UserService.getByName(" + name + ") 執行了");
        return userMappper.getByName(name);
    }
}
com.springboot.service.UserService

原理及總結

依舊是從自動配置類入手以 @Cacheable 註解執行流程進行分析:

 1 package org.springframework.boot.autoconfigure.cache;
 2 
 3 @Configuration
 4 @ConditionalOnClass(CacheManager.class)
 5 @ConditionalOnBean(CacheAspectSupport.class)
 6 @ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
 7 @EnableConfigurationProperties(CacheProperties.class)
 8 @AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
 9 @AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
10         RedisAutoConfiguration.class })
11 @Import(CacheConfigurationImportSelector.class)
12 public class CacheAutoConfiguration {
13 
14     // 註冊緩存管理器定製器
15     @Bean
16     @ConditionalOnMissingBean
17     public CacheManagerCustomizers cacheManagerCustomizers(
18             ObjectProvider<List<CacheManagerCustomizer<?>>> customizers) {
19         return new CacheManagerCustomizers(customizers.getIfAvailable());
20     }
21 
22     @Bean
23     public CacheManagerValidator cacheAutoConfigurationValidator(
24             CacheProperties cacheProperties, ObjectProvider<CacheManager> cacheManager) {
25         return new CacheManagerValidator(cacheProperties, cacheManager);
26     }
27 
28     @Configuration
29     @ConditionalOnClass(LocalContainerEntityManagerFactoryBean.class)
30     @ConditionalOnBean(AbstractEntityManagerFactoryBean.class)
31     protected static class CacheManagerJpaDependencyConfiguration
32             extends EntityManagerFactoryDependsOnPostProcessor {
33 
34         public CacheManagerJpaDependencyConfiguration() {
35             super("cacheManager");
36         }
37 
38     }
39 
40     static class CacheManagerValidator implements InitializingBean {
41 
42         private final CacheProperties cacheProperties;
43 
44         private final ObjectProvider<CacheManager> cacheManager;
45 
46         CacheManagerValidator(CacheProperties cacheProperties,
47                 ObjectProvider<CacheManager> cacheManager) {
48             this.cacheProperties = cacheProperties;
49             this.cacheManager = cacheManager;
50         }
51 
52         @Override
53         public void afterPropertiesSet() {
54             Assert.notNull(this.cacheManager.getIfAvailable(),
55                     "No cache manager could "
56                             + "be auto-configured, check your configuration (caching "
57                             + "type is '" + this.cacheProperties.getType() + "')");
58         }
59 
60     }
61 
62     static class CacheConfigurationImportSelector implements ImportSelector {
63 
64         @Override
65         public String[] selectImports(AnnotationMetadata importingClassMetadata) {
66             CacheType[] types = CacheType.values();
67             String[] imports = new String[types.length];
68             for (int i = 0; i < types.length; i++) {
69                 imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
70             }
71             return imports;
72         }
73     }
74 }
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration

看到第 11 行,該行是經過 @Import(CacheConfigurationImportSelector.class) 導入緩存配置類, CacheConfigurationImportSelector 爲自動配置類中的內部類,對應的導入方法爲 65 行 selectImports 方法。經過調試會發現它實際導入了以下配置類:

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

那麼實際生效的是哪一個配置類呢?能夠在配置文件中開啓 debug 模式,接着在控制檯中就能夠看到:

SimpleCacheConfiguration matched:
  - Cache org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration automatic cache type (CacheCondition)
  - @ConditionalOnMissingBean (types: org.springframework.cache.CacheManager; SearchStrategy: all) did not find any beans (OnBeanCondition)

即生效的配置類爲 org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration 。查看該配置類:

 1 package org.springframework.boot.autoconfigure.cache;
 2 
 3 @Configuration
 4 @ConditionalOnMissingBean(CacheManager.class)
 5 @Conditional(CacheCondition.class)
 6 class SimpleCacheConfiguration {
 7 
 8     private final CacheProperties cacheProperties;
 9 
10     private final CacheManagerCustomizers customizerInvoker;
11 
12     SimpleCacheConfiguration(CacheProperties cacheProperties,
13             CacheManagerCustomizers customizerInvoker) {
14         this.cacheProperties = cacheProperties;
15         this.customizerInvoker = customizerInvoker;
16     }
17 
18     @Bean
19     public ConcurrentMapCacheManager cacheManager() {
20         ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
21         List<String> cacheNames = this.cacheProperties.getCacheNames();
22         if (!cacheNames.isEmpty()) {
23             cacheManager.setCacheNames(cacheNames);
24         }
25         return this.customizerInvoker.customize(cacheManager);
26     }
27 }
org.springframework.boot.autoconfigure.cache.SimpleCacheConfiguration

在第 19 行能夠看到往 IoC 容器中註冊了一個 org.springframework.cache.concurrent.ConcurrentMapCacheManager ,該類實現了 org.springframework.cache.CacheManager 接口,可使用它的 getCache(String) 方法來獲取緩存組件:

 1 public Cache getCache(String name) {
 2     // this.cacheMap : private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap(16);
 3     Cache cache = (Cache)this.cacheMap.get(name);
 4     if (cache == null && this.dynamic) {
 5         synchronized(this.cacheMap) {
 6             // 從 ConcurrentMap 對象中獲取 Cache 組件
 7             cache = (Cache)this.cacheMap.get(name);
 8             if (cache == null) { // 若是獲取的 Cache 組件爲空
 9                 // 新建立一個 ConcurrentMapCache 組件
10                 cache = this.createConcurrentMapCache(name);
11                 // 將其放入 ConcurrentMap 對象中
12                 this.cacheMap.put(name, cache);
13             }
14         }
15     }
16     return cache;
17 }
org.springframework.cache.concurrent.ConcurrentMapCacheManager#getCache

能夠看到該方法的做用實際上就是用來獲取一個 ConcurrentMapCache 類型的 Cache 組件,而咱們經過 Cache 組件獲取數據時是經過 get 方法,最終是經過該類的 get 方法調用 lookup(key) 方法:

1 @Override
2 protected Object lookup(Object key) {
3     // this.store : private final ConcurrentMap<Object, Object> store;
4     return this.store.get(key);
5 }
org.springframework.cache.concurrent.ConcurrentMapCache#lookup

而這個 key 默認是使用 org.springframework.cache.interceptor.SimpleKeyGenerator#generateKey 生成的:

 1 package org.springframework.cache.interceptor;
 2 
 3 import java.lang.reflect.Method;
 4 
 5 public class SimpleKeyGenerator implements KeyGenerator {
 6 
 7     @Override
 8     public Object generate(Object target, Method method, Object... params) {
 9         return generateKey(params);
10     }
11 
12     public static Object generateKey(Object... params) {
13         if (params.length == 0) {
14             //SimpleKey.EMPTY : public static final SimpleKey EMPTY = new SimpleKey();
15             return SimpleKey.EMPTY;
16         }
17         if (params.length == 1) {
18             Object param = params[0];
19             if (param != null && !param.getClass().isArray()) {
20                 return param;
21             }
22         }
23         return new SimpleKey(params);
24     }
25 }
org.springframework.cache.interceptor.SimpleKeyGenerator

能夠看到它的生成規則以下:

  • 當目標方法的參數個數爲空時,使用 new SimpleKey() 實例化一個 SimpleKey 做爲 key 值。
  • 當目標方法的參數個數爲 1 且該參數不爲空也不是數組類型時,直接使用該參數值做爲 key 值。
  • 當目標方法的參數不止一個時,使用 new SimpleKey(params) 實例化一個 SimpleKey 做爲 key 值。

即 ConcurrentMapCache 組件也是將實際緩存數據存放在 ConcurrentMap 對象中。

ConcurrentMapCache 類實現了 Cache 接口,Cache 接口中定義了 get 方法用來獲取數據、put 方法用來存放數據、evict 方法用來刪除指定數據、clear 方法用來清空全部數據。

上述運行流程以下:

  1. 目標方法運行以前會使用 CacheManager 先根據 cacheNames 獲取 Cache 組件,若是沒有,則會建立一個新的 Cache 組件返回。
  2. 經過返回的 Cache 組件使用生成的 key 獲取緩存內容。
  3. 若是獲取到的緩存內容爲空,就直接調用目標方法,而後將目標方法執行後的返回值經過 Cache 組件的 put 方法放入緩存(ConcurrentMap 對象)中;若是獲取到的緩存內容不爲空,就直接返回獲取到的緩存內容,不會執行目標方法。

默認狀況下:

  • 生效的自動配置類爲 SimpleCacheConfiguration。
  • CacheManager 的實現爲 ConcurrentMapCacheManager。
  • key 是使用 KeyGenerator 生成的,實現爲 SimpleKeyGenerator。

整合Redis

redis安裝

參考【Linux 下源碼安裝 Redis】或【Docker 安裝鏈接 Redis】。

準備環境

一、在上面簡單使用示例項目的基礎上再引入 redis 緩存的場景啓動器:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

二、配置 redis:

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.202.136:3306/springboot_cache
    driver-class-name: com.mysql.jdbc.Driver
  redis:
    # 指定 redis 主機地址
    host: 192.168.202.136
application.yml

三、測試:

訪問 localhost:8080/user/1,而後查看 Redis:


會發現 id 爲 1 的用戶已經被緩存到 redis。
test

RedisTemplate使用

package com.springboot;

import com.springboot.bean.User;
import com.springboot.service.UserService;
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.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisTests {

    @Autowired
    private RedisTemplate redisTemplate; // k-v 都爲字符串

    @Autowired
    private StringRedisTemplate stringRedisTemplate; // k-v 都爲 Object

    @Autowired
    private UserService userService;

    // 操做字符串
    @Test
    public void testString(){
        stringRedisTemplate.opsForValue().set("msg","hello 張三");
        String msg = stringRedisTemplate.opsForValue().get("msg");
        System.out.println(msg);
        /*
        hello 張三
         */
    }

    // 操做對象
    // 注意:序列化類型須要實現 Serializable 接口
    @Test
    public void testObject(){
        User user = userService.getById(2);
        redisTemplate.opsForValue().set("id_2", user);
        Object user2 = redisTemplate.opsForValue().get("id_2");
        System.out.println(user2);
        /*
        User{id=2, name='李四', gender=0, birthday=Tue Feb 03 00:00:00 CST 1998, address='武漢'}
         */
    }

    /*
    還可經過以下幾種方式操做列表、集合、有序集合、哈希
        RedisTemplate.opsForList()
        RedisTemplate.opsForSet()
        RedisTemplate.opsForZSet()
        RedisTemplate.opsForHash()
     */
}
test

通過上述測試咱們查看 redis 中數據會發現 redis 中保存的是 SpringBoot 以默認方式序列化後的數據,若是咱們想要以 json 方式序列化保存數據到 redis 咱們該怎麼作呢?

咱們使用的 RedisTemplate 和 StringRedisTemplate bean 都是在 redis 的自動配置類中註冊的,查看:

 1 @Configuration
 2 protected static class RedisConfiguration {
 3 
 4     @Bean
 5     @ConditionalOnMissingBean(name = "redisTemplate")
 6     public RedisTemplate<Object, Object> redisTemplate(
 7             RedisConnectionFactory redisConnectionFactory)
 8             throws UnknownHostException {
 9         RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
10         template.setConnectionFactory(redisConnectionFactory);
11         return template;
12     }
13 
14     @Bean
15     @ConditionalOnMissingBean(StringRedisTemplate.class)
16     public StringRedisTemplate stringRedisTemplate(
17             RedisConnectionFactory redisConnectionFactory)
18             throws UnknownHostException {
19         StringRedisTemplate template = new StringRedisTemplate();
20         template.setConnectionFactory(redisConnectionFactory);
21         return template;
22     }
23 
24 }
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration.RedisConfiguration

每一個 RedisTemplate 對象均可定製本身的序列化器,查看源碼會發現它默認使用的序列化器爲 org.springframework.data.redis.serializer.JdkSerializationRedisSerializer 。咱們只須要修改它默認的序列化器便可:

package com.springboot.config;

import com.springboot.bean.User;
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;

@Configuration
public class CacheConfig {

    @Bean
    public RedisTemplate<Object, User> userRedisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object, User> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer<User> serializer = new Jackson2JsonRedisSerializer<User>(User.class);
        template.setDefaultSerializer(serializer);
        return template;
    }
}
com.springboot.config.CacheConfig
package com.springboot;

import com.springboot.bean.Dept;
import com.springboot.bean.User;
import com.springboot.service.DeptService;
import com.springboot.service.UserService;
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.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisTests {

    @Autowired
    private UserService userService;

    @Autowired
    private RedisTemplate userRedisTemplate;

    @Test
    public void test(){
        User user = userService.getById(2);
        userRedisTemplate.opsForValue().set("id_2", user);
        Object user2 = userRedisTemplate.opsForValue().get("id_2");
        System.out.println(user2);
        /*
        User{id=2, name='李四', gender=0, birthday=Tue Feb 03 00:00:00 CST 1998, address='武漢'}
         */
    }
}

test

自定義CacheManager

上面說的是經過 RedisTemplate 操做保存 json 數據到 redis,若是要使用註解方式該怎麼作呢?

一旦咱們引入了 redis 緩存的場景啓動器,那麼默認生效的緩存配置類就變成了 org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration :

 1 package org.springframework.boot.autoconfigure.cache;
 2 
 3 @Configuration
 4 @AutoConfigureAfter(RedisAutoConfiguration.class)
 5 @ConditionalOnBean(RedisTemplate.class)
 6 @ConditionalOnMissingBean(CacheManager.class)
 7 @Conditional(CacheCondition.class)
 8 class RedisCacheConfiguration {
 9 
10     private final CacheProperties cacheProperties;
11 
12     private final CacheManagerCustomizers customizerInvoker;
13 
14     RedisCacheConfiguration(CacheProperties cacheProperties,
15             CacheManagerCustomizers customizerInvoker) {
16         this.cacheProperties = cacheProperties;
17         this.customizerInvoker = customizerInvoker;
18     }
19 
20     @Bean
21     public RedisCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
22         RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
23         cacheManager.setUsePrefix(true);
24         List<String> cacheNames = this.cacheProperties.getCacheNames();
25         if (!cacheNames.isEmpty()) {
26             cacheManager.setCacheNames(cacheNames);
27         }
28         return this.customizerInvoker.customize(cacheManager);
29     }
30 
31 }
org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration

緩存管理器也更換爲第 21 行的 RedisCacheManager,RedisCacheManager 幫咱們建立 RedisCache 來做爲緩存組件,而 RedisCache 組件就是經過操做 Redis 緩存數據的。

在第 22 行也能夠看到,建立 RedisCacheManager 實例時經過構造方法傳入的 RedisTemplate,因此咱們只須要本身定義一個 RedisCacheManager,讓其 RedisTemplate 是使用 json 序列化器便可:

package com.springboot.config;

import com.springboot.bean.Dept;
import com.springboot.bean.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;

@Configuration
public class CacheConfig {

    @Bean
    public RedisTemplate<Object, User> userRedisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object, User> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer<User> serializer = new Jackson2JsonRedisSerializer<User>(User.class);
        template.setDefaultSerializer(serializer);
        return template;
    }

    @Bean
    public RedisTemplate<Object, Dept> deptRedisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object, Dept> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer<Dept> serializer = new Jackson2JsonRedisSerializer<Dept>(Dept.class);
        template.setDefaultSerializer(serializer);
        return template;
    }

    @Primary // 默認
    @Bean
    public RedisCacheManager userCacheManager(RedisTemplate<Object, User> redisTemplate) {
        RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
        // key 使用 cacheName 做爲前綴
        cacheManager.setUsePrefix(true);
        return cacheManager;
    }

    @Bean
    public RedisCacheManager deptCacheManager(RedisTemplate<Object, Dept> redisTemplate) {
        RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
        // key 使用 cacheName 做爲前綴
        cacheManager.setUsePrefix(true);
        return cacheManager;
    }
}
com.springboot.config.CacheConfig

而後再使用註解時指定要使用的緩存管理器便可。

還能夠經過手動編碼方式獲取到緩存管理器對指定緩存組件進行操做:

package com.springboot.service;

import com.springboot.bean.Dept;
import com.springboot.bean.User;
import com.springboot.mapper.DeptMappper;
import com.springboot.mapper.UserMappper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.annotation.*;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.stereotype.Service;

@Service
@CacheConfig(cacheManager = "deptCacheManager")
public class DeptService {
    @Autowired
    private DeptMappper deptMappper;

    @Autowired
    private RedisCacheManager deptCacheManager;

    public Dept getById(Integer id) {
        System.out.println("DeptService.getById(" + id + ") 執行了");
        Dept dept = deptMappper.getById(id);
        // 獲取到緩存組件
        Cache cache = deptCacheManager.getCache("dept");
        cache.put(id, dept);
        return dept;
    }
}
com.springboot.service.DeptService
相關文章
相關標籤/搜索