前面介绍了数据库的使用,但是数据库并不能完全高性能地解决所有任务,这个时候缓存就出现了。缓存是进行数据交换的缓冲区,一般将访问量比较大的数据从数据库中查询出来放入缓存中,当下次需要数据的时候,直接从缓存中获取。通常缓存会放入内存或硬盘中,方便开发者使用。
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”,导致存储和检索总是对不上。