【SpringBoot 2學習筆記】《十二》SpringBoot2緩存之旅SpringCache

前面介紹了數據庫的使用,但是數據庫並不能完全高性能地解決所有任務,這個時候緩存就出現了。緩存是進行數據交換的緩衝區,一般將訪問量比較大的數據從數據庫中查詢出來放入緩存中,當下次需要數據的時候,直接從緩存中獲取。通常緩存會放入內存或硬盤中,方便開發者使用。

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”,導致存儲和檢索總是對不上。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章