Spring Boot學習(十):Spring Boot 與快取
快取,我們應該已經很熟悉了。那麼今天就來學習一下Spring Boot中怎麼使用快取。
1、說起快取,先來了解下JSR107
首先什麼是JSR?
JSR是Java Specification Requests 的縮寫 ,Java規範請求,故名思議提交Java規範,大家一同遵守這個規範的話,會讓大家‘溝通’起來更加輕鬆。
什麼是JSR107?
JSR107就是如何使用快取的規範。
JSR107都有哪些內容?
核心API:
- CachingProvider:定義了建立,配置,得到,管理和控制0個或多個CacheManager,一個應用在執行時可能訪問0個或者多個CachingProvider。
-
CacheManager:它定義了建立,配置,得到,管理和控制0個或多個有著唯一名字的Cache ,一個CacheManager被包含在單一的CachingProvider。
-
Cache:Cache是一個Map型別的資料結構,用來儲存基於鍵的資料,很多方面都像java.util.Map資料型別。一個Cache 存在在單一的CacheManager。
-
Entry:Entry是一個存在在Cache的鍵值對。
-
ExpiryPolicy:不是所有的資料都一直存在快取中不改變的,為快取的資料新增過期的策略會讓你的快取更加靈活和高效。
相應的關係可以參考下圖:
2、Spring Boot的快取機制
2.1、建立專案
參考上一篇文章,Spring Boot整合mybatis,快速建立一個Spring Boot專案(除web, mysql, mybatis之外多新增一個cache模組),如下:
建立好之後,看一下pom檔案,會看到引入了cache模組:
專案結構:
2.2、完善專案結構並測試
這個專案我們不使用mapper配置檔案,我們基於mapper註解的方式進行訪問。
建立表user,並插入兩條資料:
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `age` int(4) NOT NULL, `name` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
實體類User:
package com.example.cache.domain;
import org.springframework.stereotype.Component;
import java.io.Serializable;
/**
* @author pavel
* @date 2018/11/19 0019
*/
@Component
public class User implements Serializable {
private static final long serialVersionUID = -1274433079373420955L;
private Long id;
private Integer age;
private String name;
public User() {
}
public User(Long id, Integer age, String name) {
this.id = id;
this.age = age;
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", age=" + age +
", name='" + name + '\'' +
'}';
}
}
mapper介面:
package com.example.cache.mapper;
import com.example.cache.domain.User;
import org.apache.ibatis.annotations.*;
/**
* 基於註解的mapper配置
* @author pavel
* @date 2018/11/19 0019
*/
@Mapper
public interface UserMapper {
@Select("select * from user where id = #{id}")
User getUser(Long id);
@Update("update user set name = #{name},age = #{age} where id = #{id}")
void updateUser(User user);
@Delete("delete from user where id = #{id}")
void deleteUser(Long id);
@Insert("insert into user(age,name) values(#{age},#{name}) ")
void insertUser(User user);
}
service:
package com.example.cache.service;
import com.example.cache.domain.User;
import com.example.cache.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
/**
* @author pavel
* @date 2018/11/22 0022
*/
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User getUser(Long id) {
System.out.println("查詢" + id + "號員工");
return userMapper.getUser(id);
}
}
啟動類上新增mapper的包掃描:
package com.example.cache;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.cache.mapper")
public class SpringBootCacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootCacheApplication.class, args);
}
}
主配置檔案中的資料庫連線配置:
### database ###
spring.datasource.url=jdbc:mysql://localhost:3306/springboot_test?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=yjx941001
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
### 控制檯列印sql ###
logging.level.com.example.cache.mapper = debug
controller:
package com.example.cache.controller;
import com.example.cache.domain.User;
import com.example.cache.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author pavel
* @date 2018/11/19 0019
*/
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/user/{id}")
public User getUser(@PathVariable("id") Long id) {
User user = userService.getUser(id);
System.out.println("查詢結果: " + user);
return user;
}
}
控制檯:
可以發現"查詢1號員工"字樣會列印兩次,說明第二次訪問再次呼叫了查詢方法,訪問資料庫,此時沒有任何快取機制。
2.3、加入快取機制
修改啟動類,新增@EnableCaching註解,開啟快取機制:
package com.example.cache;
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.example.cache.mapper")
public class SpringBootCacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootCacheApplication.class, args);
}
}
修改service,給getUser方法加上@Cacheable註解(下面再詳細介紹這個註解的作用),如下:
package com.example.cache.service;
import com.example.cache.domain.User;
import com.example.cache.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
/**
* @author pavel
* @date 2018/11/22 0022
*/
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Cacheable(cacheNames = "user")
public User getUser(Long id) {
System.out.println("查詢" + id + "號員工");
return userMapper.getUser(id);
}
}
再次請求:
看控制檯輸出,第一次請求,打印出“查詢1號員工”字樣、查詢sql以及查詢結果,但第二次請求,只打印了查詢結果,並沒有“查詢1號員工”字樣以及查詢sql,說明我們配置的快取是生效的,第二次請求是直接從快取中獲取user物件。
3、工作原理及執行流程
3.1、工作原理
快取我們引入了cache模組, 還是之前講到過的Spring Boot自動配置原理,有一系類的xxxAutoConfiguration配置類,那麼肯定會有cache相關的自動配置,即:CacheAutoConfiguration,所以我們就進去看一下。
看到會給容器匯入一個CacheConfigurationImportSelector選擇器,debug看一下:
這裡就是匯入的所有的快取配置。但不是都生效,又興趣的話可以每一個點進去看一下,看在什麼情況下那個配置才會生效。
可以在配置檔案中配置: debug=true
然後看到控制檯輸出中,預設是 SimpleCacheConfiguration會生效。所以我們就之間去看這個快取配置類:
如上圖,可以看出,這個配置類,就是給容器註冊了一個CacheManager:ConcurrentMapCacheManager
我們再看下這個ConcurrentMapCacheManager:
實現了CacheManager介面,那麼就有了關於Cache相關的操作方法,比如getCache()
所以說,ConcurrentMapCacheManager的作用是建立和獲取ConcurrentMapCache型別的快取元件。
我們繼續看,怎麼建立的Cache,如下,直接new ConcurrentMapCache();
再往下走,進到ConcurrentMapCache類,這個就是快取元件類,
類中有兩個方法,put(Object key, @Nullable Object value) 以及lookup(Object key)方法,如下:
可見,key的值就是1,
lookup()方法返回值為null,說明快取中沒有這個物件,再往下走,就進入到了getUser()方法進行查詢,然後再進到put()方法,將查詢結果以key-value的方式存到快取中,key是請求引數1,而value就是查詢結果-User物件:
進入到lookup()方法,可以看到,this.store中有值,key是1,value是一個User物件,正是上一步訪問後存入到快取中的User物件,所以,再次訪問就能夠直接從快取中獲取到值了。
快取的工作原理就是這個樣子了,可以自己debug一步一步的看。
3.2、執行流程
@Cacheable的執行流程:
(1) 該註解時作用於方法之上的,在方法執行之前,會先去查詢Cache(快取元件),按照cacheNames/value指定的名字獲取,(cacheManager先獲取相應的快取),第一次獲取快取如果沒有Cache元件會自動建立。
(2) 去Cache中查詢快取的內容,使用一個key,key預設值就是方法的引數。
key是按照某種策略生成的;預設是使用keyGenerator介面的實現類SimpleKeyGenerator生成;
SimpleKeyGenerator的生成key的預設策略(debug一步一步可以看到的):
如果沒有引數: key = new SimpleKey()
如果有一個引數:key = 引數的值
如果有多個引數:key = new SimpleKey(params)
(3) 沒有查到快取就呼叫目標方法 (也就是上面例子中的getUser()方法)
(4) 將目標方法返回的結果放到快取中
4、@Cacheable註解詳解。
註解@Cacheable的相關屬性:
cacheNames/value: 指定快取元件的名字;將結果放到哪個快取元件中,可以用陣列的形式指定多個快取元件
key:快取資料使用的key, 預設使用的是方法引數的值 id-方法返回值
keyGenerator: key的生成器,可以自己指定key的生成器的元件id
key/keyGenerator:二選一使用
cacheManager:指定快取管理器, 或者指定cacheResovler指定獲取解析器
condition: 指定符合條件的情況下才快取
unless: 否定快取,當unless指定的條件為true時,方法的返回值不回被快取。
sync:是否使用非同步模式
接下來一個一個來嘗試:
cacheNames:我們在上面的例子已經使用到了,用value是一樣的效果,都是給快取元件指定一個名字。
key: 快取資料使用的key,不設定的話預設是使用方法引數,上面已經說過了。
我們還可以自己設定key的值,利用SpEL表示式,那麼快取中可以寫的SpEL如下圖所示:
下面就來自己定義一個key的值,比如我想將key設定為:方法名[引數值],則如下拼接,根據上圖看到,#root.methodName就是方法名,#id就是引數值:
可以看到,生成的key就是我們自己設定的:方法名[引數值]
keyGenerator: 這個就是一個key生成器,自己寫一個就是,如下:
package com.example.cache.config;
import org.springframework.cache.interceptor.KeyGenerator;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* @author pavel
* @date 2018/11/22 0022
*/
@Configuration
public class MyCacheConfig {
@Bean("myKeyGenerator")
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
return method.getName() + "[" + Arrays.asList(params).toString() + "]";
}
};
}
}
然後在方法上指定keyGenerator:
如上圖,生成的key為:getUser()[ [1] ],多一層[]是Arrays.asList()產生的。
cacheManager: 指定快取管理器,這個後面我們使用多個快取管理器時再討論。
condition: 指定符合條件的情況下才快取;比如我指定當引數id大於1的時候才快取:condition = "#id>1",如下:
可以看到,連續兩次請求,獲取id為1 的使用者,都會呼叫方法傳送sql語句查詢,第一次的查詢結果並沒有被快取,
可見,第一次請求的結果進行的快取。
unless: 否定快取,當unless指定的條件為true,方法的返回值就不會被快取。例如:unless = "#id == 2", 當引數id為2時就不進行快取。
控制檯兩次請求都呼叫方法併發送sql進行查詢。
只是第一次請求呼叫了方法併發送sql進行查詢。
sync:是否使用非同步模式。sync=true
看原始碼,預設是false,使用非同步模式就不支援unless了,這個就自己試一下吧。如下:
5、@Cacheput註解
作用:修改資料庫資料,並同步更新快取。 這就避免了更新了資料庫的資料(資料已加入快取)後再次查詢還是查到更新前的資料。
執行時機:
1、先呼叫目標方法
2、將目標方法的結果快取起來
下面就來使用一下這個註解。給上面的UserService中增加一個updateUser方法,方法上使用@Cacheput註解,並返回修改後的User物件, 如下:
package com.example.cache.service;
import com.example.cache.domain.User;
import com.example.cache.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
/**
* @author pavel
* @date 2018/11/22 0022
*/
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Cacheable(cacheNames = "user")
public User getUser(Long id) {
System.out.println("查詢" + id + "號員工");
return userMapper.getUser(id);
}
@CachePut
public User updateUser(User user) {
System.out.println("更新" + user.getId() + "號員工");
userMapper.updateUser(user);
return user;
}
}
UserController中增加updateUser方法:
package com.example.cache.controller;
import com.example.cache.domain.User;
import com.example.cache.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* @author pavel
* @date 2018/11/19 0019
*/
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/user/{id}")
public User getUser(@PathVariable("id") Long id) {
User user = userService.getUser(id);
System.out.println("查詢結果: " + user);
return user;
}
@GetMapping("/user")
public User updateUser(User user) {
User u = userService.updateUser(user);
System.out.println("更新後結果:" + u);
return u;
}
}
下面我們來測試一下,先說一下測試流程:
2、再次查詢1號使用者,還是之前的結果
4、再次查詢1號使用者,那麼,會不會查詢資料庫?返回的是更新前的使用者還是更新後的使用者?
啟動專案,按照以上測試流程逐步進行,控制檯輸出結果如下:
上體可以看到,步驟3確實是更新了資料庫,但是步驟4再次查詢時,沒有傳送查詢sql,但是查詢結果確實更新前的資料,這是 怎麼回事?難道@Cacheput沒起作用?
回頭看一下我們UserService中的兩個方法,查詢和更新,我們都沒有設定快取資料的key,所以都預設以引數為key,
那麼上面步驟1執行之後,快取中:key = id value = User物件
步驟3執行之後,快取中:key = 傳入的User物件 value = 返回的User物件
這下明白了吧,我們更新操作之後,返回的更新後的資料其實是存入到了快取中,但是存入的key同樣是一個User物件而不是id。
下面將updateUser()方法做下修改,設定key值為傳入User物件的id或者是返回User物件的id:
@CachePut(cacheNames = "user", key = "#user.id") // 或者key = "#result.id"
public User updateUser(User user) {
System.out.println("更新" + user.getId() + "號員工");
userMapper.updateUser(user);
return user;
}
這樣修改之後,查詢和更新的方法,key值都是User物件的id了。
再次測試以上四個步驟,此時資料庫中id為1的資料是:【name=xinanxin, age=25】,將其修改為【name=curry, age=31】
控制檯輸出如下:
可以看到,步驟4查詢出來的就是修改後的user物件了。有興趣的話,可以debug一步步的看一下原始碼,是怎麼進行快取中資料更新的。
6、@CacheEvict註解
作用:快取清除。一般用在刪除的方法上,刪除資料後,進行快取清除
相關屬性:
key: 指定要清除的資料
allEntries: 預設是false,若設定為true,則清除該Cache中的所有快取資料。
beforeInvocation: 預設是false, 代表快取的清除是在方法呼叫之後進行的,如果方法出現異常,則快取不會被清除,若設定為true,則代表快取的清除是在方法呼叫之前進行的,不論該方法的執行是否會出現異常,快取都會被清除。
這個註解,案例就不詳細寫了啊,可以參考上面的查詢和修改,寫上一個delete方法測試。
7、@Caching註解
作用:定義複雜的快取規則。是一個組合註解,裡面可以包括以上介紹的三個註解。如下:
我們同樣來寫一個案例,使用一下這個註解.
在UserMapper介面增加方法,getUser=ByName()
@Select("select * from user where name = #{name}")
User getUserByName(String name);
在UserService中增加方法,getUserByUserName():
@Caching(
cacheable = {
@Cacheable(cacheNames = "user")
},
put = {
@CachePut(cacheNames = "user", key = "#result.id"),
@CachePut(cacheNames = "user", key = "#result.age")
}
)
public User getUserByName(String name) {
System.out.println("通過name查詢User");
return userMapper.getUserByName(name);
}
在這個方法上,加上了@Caching註解,裡面包含了caccheable和put,cacheable給快取中新增的資料key為name,value為User物件, put給快取新增的資料key是 id和 age,value為User物件。
等於說,呼叫了這個方法之後,再根據id,age去查詢,就不用查詢資料庫了,直接從快取中取。
但是根據name查詢,還是會發送sql進行資料庫查詢,因為使用了@CachePut註解,使用這個註解每次都會呼叫方法。
在UserController中增加getUserByUserName()
@GetMapping("/user/find-by-name")
public User getUserByName(@RequestParam("name") String name) {
User user = userService.getUserByName(name);
System.out.println("getUserByName()查詢結果: " + user);
return user;
}
下面來測試一下,測試流程:
重啟,然後依次執行上面步驟,控制檯輸出如下:
通過控制檯輸出可以看到,步驟1通過name查詢,傳送sql查詢,步驟2再通過id查詢(或者是自己寫的通過age查詢),都不會發送sql查資料庫,而是直接從快取中取的,步驟3再次通過name查詢時,還是傳送了sql查詢,說明加上@CachePut後,每次呼叫都會查詢資料庫。
8、@CacheConfig註解
這是全域性快取配置,作用在類上面,對整個類生效。看下這個註解都有哪些內容:
可以設定快取名稱,key生成器,快取管理器,以及快取解析器
比如在上面講@Caching註解的案例中,我們將快取資料都新增到了名為user的Cache中,所以我們配置了三次 cacheNames = "user",這樣很繁瑣,那麼可以在類上面使用@CacheConfig(cacheNames = "user") 來簡化,如下:
package com.example.cache.service;
import com.example.cache.domain.User;
import com.example.cache.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
/**
* @author pavel
* @date 2018/11/22 0022
*/
@Service
@CacheConfig(cacheNames = "user")
public class UserService {
@Autowired
private UserMapper userMapper;
@Cacheable()
public User getUser(Long id) {
System.out.println("查詢" + id + "號員工");
return userMapper.getUser(id);
}
@CachePut(key = "#user.id") // 或者key = "#result.id"
public User updateUser(User user) {
System.out.println("更新" + user.getId() + "號員工");
userMapper.updateUser(user);
return user;
}
@CacheEvict()
public void deleteUser(Long id) {
System.out.println("刪除"+ id + "號員工");
userMapper.deleteUser(id);
}
@Caching(
cacheable = {
@Cacheable()
},
put = {
@CachePut(key = "#result.id"),
@CachePut(key = "#result.age")
}
)
public User getUserByName(String name) {
System.out.println("通過name查詢User");
return userMapper.getUserByName(name);
}
}
這樣,效果是一樣的,可以自己嘗試一下。
好了,我關於Spring Boot的快取相關內容的學習就是如上這些了,以後再深入學習的再補充過來。
希望對剛研究這塊的小夥伴能有一點點幫助。