前面介紹了數據庫的使用,但是數據庫並不能完全高性能地解決所有任務,這個時候緩存就出現了。緩存是進行數據交換的緩衝區,一般將訪問量比較大的數據從數據庫中查詢出來放入緩存中,當下次需要數據的時候,直接從緩存中獲取。通常緩存會放入內存或硬盤中,方便開發者使用。
12.1 使用Spring Cache
Spring Cache是Spring3.1版本開始引入的新技術。其核心思想是:當我們調動一個緩存方法時,會把該方法參數和返回結果作爲一個鍵值對存放到緩存中,等下次利用同樣參數來調用該方法時將不再執行,而是直接從緩存中取得結果並返回,從而實現緩存的功能。Spring Chache的使用方法也是基於註解或者基於XML配置的方式。我們下面的內容主要基於註解方式進行說明。
我們接下里重點關於如下三個註解:@Cacheable、@CachePut、@CacheEvict
12.1.1 @Cacheable &@CachePut &@CacheEvict
@Cacheable註解用於標記緩存,可以標記在方法或者類上,當對方法進行標記時,表示該方法支持緩存;當標記在類上時,表示該類的所有方法都支持緩存。對於支持Spring Cache的程序中,Spring在每次調用時,會對標記了@Cacheable的方法,先根據Key去查詢當前的緩存中是否有返回結果,如果存在則直接將該結果返回,不在進行數據庫檢索。如果緩存內沒有相應的數據,則繼續執行方法,並把返回的結果存入緩存中並同時返回給客戶端。
-
value:緩存的名稱。在使用@Cacheable註解時,該屬性是必須指定的,表明當前緩存的返回值用在哪個緩存上。
-
key:緩存的 key,可以爲空。當我們沒有指定key時,Spring會爲我們使用默認策略生成一個key。通常我們使用緩存方法的參數作爲key,一般爲:“#參數名”。如果參數爲對象,也可以使用對象的屬性作爲key。
-
condition:主要用於指定當前緩存的觸發條件。很多情況下可能並不需要使用所有緩序的方法進行緩存,所以Spring Cache爲我們提供了這種屬性來排除些特定的情況。 以屬性指定key爲user.id爲例,比如我們只需要id爲偶數才進行緩存,進行配置condition屬性配置如下:
@Cacheable(value="user", key="#user.id", condition="user.id%2 == 0")
@CachePut註解標誌的方法,每次都會執行,並將結果存入指定的緩存中。其他方法可以直接從響應的緩存中讀取緩存數據,而不需要再去查詢數據庫。主要在添加方法使用較多。
@CacheEvict註解標誌的方法,會清空指定的緩存。主要用於更新或者刪除方法。
12.1.2 配置和代碼
POM.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
項目啓動類上加入**@EnableCaching**註解,表明啓動緩存。
package com.gavinbj.confmng;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
/**
* @SpringBootApplication 標註這是SpringBoot項目的主程序類
*/
@MapperScan("com.gavinbj.confmng.persistence.mapper")
@SpringBootApplication
@EnableCaching
public class ConfManageApplication {
public static void main(String[] args) {
SpringApplication.run(ConfManageApplication.class, args);
}
}
用戶接口Controller:
package com.gavinbj.confmng.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.gavinbj.common.bean.BaseResult;
import com.gavinbj.common.bean.EnumCodeMsg;
import com.gavinbj.common.bean.ResponseUtils;
import com.gavinbj.common.exception.SysException;
import com.gavinbj.confmng.persistence.entity.UserInfo;
import com.gavinbj.confmng.service.UserInfoService;
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
UserInfoService userInfoService;
/**
* 檢索用戶信息
*
*/
@GetMapping("/users/{userId}")
public UserInfo getUserById(@PathVariable("userId") String userId) {
UserInfo userInfo = userInfoService.getUserByPK(userId);
if(userInfo == null) {
// 沒有檢索到對應的用戶信息
throw new SysException(EnumCodeMsg.SERARCH_NO_RESULT, "用戶信息");
}
return userInfo;
}
/**
* 保存用戶註冊信息
*
*/
@PostMapping("/users")
public BaseResult<UserInfo> saveUserInfo(@RequestBody UserInfo user){
this.userInfoService.saveUserInfo(user);
return ResponseUtils.makeOKRsp(user);
}
/**
* 刪除用戶信息
*/
@DeleteMapping("/users/{userId}")
public BaseResult<String> delUserInfo(@PathVariable("userId") String userId){
this.userInfoService.delUserInfo(userId);
return ResponseUtils.makeOKRsp("OK");
}
}
接口對應的業務層(UserInfoServiceImpl.java)
package com.gavinbj.confmng.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import com.gavinbj.confmng.persistence.entity.UserInfo;
import com.gavinbj.confmng.persistence.entity.UserInfoExample;
import com.gavinbj.confmng.persistence.mapper.UserInfoMapper;
import com.gavinbj.confmng.service.UserInfoService;
/**
* 用戶信息
*
* @author gavinbj
*
*/
@Service
public class UserInfoServiceImpl implements UserInfoService {
@Autowired
private UserInfoMapper userInfoMapper;
/**
* 根據用戶ID進行主鍵檢索
*/
@Override
@Cacheable(value ="user", key="#userId")
public UserInfo getUserByPK(String userId) {
return this.userInfoMapper.selectByPrimaryKey(userId);
}
/**
* 保存用戶信息
*/
@Override
@CachePut(value="user", key="#user.userId" )
public UserInfo saveUserInfo(UserInfo user) {
UserInfo userExist = this.userInfoMapper.selectByPrimaryKey(user.getUserId());
if(userExist == null) {
// 該用戶ID不存在可以插入用戶
this.userInfoMapper.insertSelective(user);
}
return user;
}
/**
* 刪除用戶信息
*
*/
@Override
@CacheEvict(value="user", key="#userId")
public void delUserInfo(String userId) {
this.userInfoMapper.deleteByPrimaryKey(userId);
}
}
DB操作利用MyBatis自動生成的代碼。
12.1.3 驗證緩存
1、驗證:@CachePut註解將信息保存到緩存中
驗證工具:Postman (UserInfoServiceImpl.saveUserInfo
)
POST
http://localhost:9003/gavin/api/users
JSON Body:
{
"userId" : "lijing",
"userName" : "李靜",
"introduce" : "美女主播",
"mobilephone" : "13948474647",
"email": "[email protected]"
}
執行結果:
{
"status": 0,
"code": 1003,
"msg": "處理成功!",
"data": {
"userId": "lijing",
"userName": "李靜",
"introduce": "美女主播",
"mobilephone": "13948474647",
"email": "[email protected]",
"birthday": null,
"gender": null
}
}
控制檯輸出:
2020-02-13 18:34:48.133 DEBUG 2692 --- [nio-9003-exec-4] c.g.c.p.m.U.selectByPrimaryKey : ==> Preparing: select user_id, user_name, introduce, mobilephone, email, birthday, gender from user_info where user_id = ?
2020-02-13 18:34:48.134 DEBUG 2692 --- [nio-9003-exec-4] c.g.c.p.m.U.selectByPrimaryKey : ==> Parameters: lijing(String)
2020-02-13 18:34:48.182 DEBUG 2692 --- [nio-9003-exec-4] c.g.c.p.m.U.selectByPrimaryKey : <== Total: 0
2020-02-13 18:34:48.207 DEBUG 2692 --- [nio-9003-exec-4] c.g.c.p.m.U.insertSelective : ==> Preparing: insert into user_info ( user_id, user_name, introduce, mobilephone, email ) values ( ?, ?, ?, ?, ? )
2020-02-13 18:34:48.244 DEBUG 2692 --- [nio-9003-exec-4] c.g.c.p.m.U.insertSelective : ==> Parameters: lijing(String), 李靜(String), 美女主播(String), 13948474647(String), [email protected](String)
2020-02-13 18:34:48.342 DEBUG 2692 --- [nio-9003-exec-4] c.g.c.p.m.U.insertSelective : <== Updates: 1
此時在數據庫中已經保存了該數據,接下來我們用檢索數據的方式,驗證數據是直接從緩存取得相應數據。因爲我們開啓了SQL執行日誌的打印,所以,如果檢索時,有沒有打印輸出檢索SQL使我們檢驗是否走緩存的依據。
GET
http://localhost:9003/gavin/api/users/lijing
執行結果:
{
"status": 0,
"code": 1003,
"msg": "處理成功!",
"data": {
"userId": "lijing",
"userName": "李靜",
"introduce": "美女主播",
"mobilephone": "13948474647",
"email": "[email protected]",
"birthday": "",
"gender": ""
}
}
此時看控制檯,沒有輸出新的檢索SQL,可以驗證標記了@CachePut的註解的方法,將相應的結果已經保存到了Spring的緩存中。
2、驗證@CacheEvict註解清除緩存
基於第一步中,我們在向數據庫保存用戶信息時,同時將該數據保存到了Spring Cache中。現在,我們調用標記了@CacheEvict註解的刪除方法,看看是否同時將緩存的用戶信息刪除掉。驗證步驟如下:
-
調用用戶刪除方法
-
調用用戶信息查詢
如果緩存刪除後,在調用用戶信息查詢,應該去數據庫檢索,控制檯能夠輸出相應的檢索語句。
DELETE
http://localhost:9003/gavin/api/users/lijing
執行結果
{
"status": 0,
"code": 1003,
"msg": "處理成功!",
"data": "OK"
}
控制檯信息:
2020-02-13 18:44:56.581 DEBUG 2692 --- [nio-9003-exec-8] c.g.c.p.m.U.deleteByPrimaryKey : ==> Preparing: delete from user_info where user_id = ?
2020-02-13 18:44:56.583 DEBUG 2692 --- [nio-9003-exec-8] c.g.c.p.m.U.deleteByPrimaryKey : ==> Parameters: lijing(String)
2020-02-13 18:44:56.703 DEBUG 2692 --- [nio-9003-exec-8] c.g.c.p.m.U.deleteByPrimaryKey : <== Updates: 1
從上面可以看出,已經從數據庫刪除了數據並打印了刪除語句。
接下來執行檢索用的接口
GET
http://localhost:9003/gavin/api/users/lijing
執行結果
{
"status": 1,
"code": 5003,
"msg": "用戶信息檢索結果爲空",
"data": ""
}
我們看控制檯輸出:
2020-02-13 18:47:13.719 DEBUG 2692 --- [nio-9003-exec-3] c.g.c.p.m.U.selectByPrimaryKey : ==> Preparing: select user_id, user_name, introduce, mobilephone, email, birthday, gender from user_info where user_id = ?
2020-02-13 18:47:13.720 DEBUG 2692 --- [nio-9003-exec-3] c.g.c.p.m.U.selectByPrimaryKey : ==> Parameters: lijing(String)
2020-02-13 18:47:13.768 DEBUG 2692 --- [nio-9003-exec-3] c.g.c.p.m.U.selectByPrimaryKey : <== Total: 0
可見執行檢索時,沒有從緩存中區的數據,接下里直接檢索數據庫,數據庫檢索結果也沒有。所以返回用戶信息爲空。可見註解@CacheEvict也已經生效。
3、驗證@Cacheable註解同時可以存儲緩存和查詢緩存
通過如上兩步,現在緩存中沒有緩存的用戶信息,我們此時執行兩次檢索方法,檢索一個數據庫中存在的用戶。第一次,因爲緩存中沒有該用戶,所以會輸出檢索用SQL。第二次在執行該用戶的檢索,此時應該直接使用緩存中的檢索結果,不去數據庫重新檢索,控制檯中不輸出SQL。
GET
http://localhost:9003/gavin/api/users/zhangsanfeng
連續執行兩次結果:
{
"status": 0,
"code": 1003,
"msg": "處理成功!",
"data": {
"userId": "zhangsanfeng",
"userName": "張三丰",
"introduce": "一代宗師",
"mobilephone": "13948474647",
"email": "[email protected]",
"birthday": "",
"gender": ""
}
}
控制檯輸出:
2020-02-13 18:54:35.465 DEBUG 2692 --- [nio-9003-exec-6] c.g.c.p.m.U.selectByPrimaryKey : ==> Preparing: select user_id, user_name, introduce, mobilephone, email, birthday, gender from user_info where user_id = ?
2020-02-13 18:54:35.465 DEBUG 2692 --- [nio-9003-exec-6] c.g.c.p.m.U.selectByPrimaryKey : ==> Parameters: zhangsanfeng(String)
2020-02-13 18:54:35.515 DEBUG 2692 --- [nio-9003-exec-6] c.g.c.p.m.U.selectByPrimaryKey : <== Total: 1
從上可以看出第一次檢索zhangsanfeng時,直接檢索了數據庫。第二次檢索使用了緩存。如上功能好用。
12.1.4 注意事項
其中使用Value沒有問題,使用key時,注意使用參數裏面對應的屬性的值作爲Key。單獨傳遞一個變量和一個對象時,上面的Key是不一樣的。我開始都按照統一的使用:“#對象.userId”,導致存儲和檢索總是對不上。