写在前面

这一系列的博文初步都定下来包括SpringBoot介绍、入门、配置、日志相关、web开发、数据访问、结合docker、缓存、消息队列、检索、任务安全、分布式等等一系列的博文,工作量很大,是个漫长的过程,每一步我都尽量详细,配上截图说明,也希望对看的同学真的有用。
单纯就是想分享技术博文,还想说一句就是,如果觉得有用,请点个关注、给个赞吧,也算对我来说是个宽慰,毕竟也得掉不少头发,嘿嘿嘿

系列文章传送条

详细SpringBoot教程之入门(一)
详细SpringBoot教程之入门(二)
详细SpringBoot教程之配置文件(一)
详细SpringBoot教程之配置文件(二)
详细SpringBoot教程之日志框架
详细SpringBoot教程之Web开发(一)
详细SpringBoot教程之Web开发(二)
详细SpringBoot教程之Web开发(三)
详细SpringBoot教程之数据访问
详细SpringBoot教程之启动配置原理
详细SpringBoot教程之缓存开发

为啥用缓存

缓存在开发中是一个必不可少的优化点,关于缓存优化了很多点,比如在加载一些数据比较多的场景中,会大量使用缓存机制提高接口响应速度,间接提升用户体验。当然对于缓存的使用也有需要注意的地方,比如它如果处理不好,没有用好比如LRU这种策略,没有及时更新数据库的数据就会导致数据产生滞后,进而产生用户的误读,或者疑惑。不过关于这一切,SpringBoot已经提供给我们很便捷的开发工具。

JSR107

在正式讲解缓存之前呢,我想先说说JSR107注解标准,这是个啥呢,简单来说就是对于缓存的接口,Java Caching定义了5个核心接口,分别是CachingProvider, CacheManager, Cache, Entry 和 Expiry。

  • CachingProvider定义了创建、配置、获取、管理和控制多个CacheManager。一个应用可以在运行期访问多个CachingProvider。
  • CacheManager定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache 存在于CacheManager的上下文中。一个CacheManager仅被一个CachingProvider所拥有。
  • Cache是一个类似Map的数据结构并临时存储以Key为索引的值。一个Cache仅被一个 CacheManager所拥有。
  • Entry是一个存储在Cache中的key-value对。
  • Expiry 每一个存储在Cache中的条目有一个定义的有效期。一旦超过这个时间,条目为过期 的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。

他们的关系大致像下面这样
在这里插入图片描述

Spring缓存抽象

Spring从3.1开始定义了org.springframework.cache.Cache 和org.springframework.cache.CacheManager接口来统一不同的缓存技术,并支持使用JCache(JSR-107)注解简化我们开发。

  • Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;
  • Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache , ConcurrentMapCache等;
  • 每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户,下次调用直接从缓存中获取。
  • 使用Spring缓存抽象时我们需要关注两点,第一是确定方法需要被缓存以及他们的缓存策略,第二是从缓存中读取之前缓存存储的数据

在这里插入图片描述

SpringBoot开启注解

1.1:搭建SpringBoot环境

在idea中,搭建一个SpringBoot是很简单的。接下来我简单说一下步骤:

我们还是先使用Idea向导创建一个带web模块的项目,这个之前的博文有说,很基本的操作,然后我们就可以开始编写代码了。首先我们需要在主程序上加入可使用缓存注解,如下

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableAutoConfiguration
@EnableCaching
public class SpringbootcacheApplication {

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

主要是@EnableCaching用于开启缓存注解的驱动,否则后面使用的缓存都是无效的,开启了之后就可以使用了,没错,就是这么简单,我们只需要在我们需要用到缓存的地方使用缓存注解就可以了,下面我们来了解一下有哪些缓存注解。

常用缓存注解

@CacheConfig

这个注解的的主要作用就是全局配置缓存,比如配置缓存的名字(cacheNames),只需要在类上配置一次,下面的方法就默认以全局配置为主,不需要二次配置,节省了部分代码。

@Cacheable

这个注解是最重要的,主要实现的功能再进行一个读操作的时候。就是先从缓存中查询,如果查找不到,就会走数据库的执行方法,这是缓存的注解最重要的一个方法,基本上我们的所有缓存实现都要依赖于它。

@CacheEvict

这个注解主要是配合@Cacheable一起使用的,它的主要作用就是清除缓存,当方法进行一些更新、删除操作的时候,这个时候就要删除缓存。如果不删除缓存,就会出现读取不到最新缓存的情况,拿到的数据都是过期的。它可以指定缓存的key和conditon,它有一个重要的属性叫做allEntries默认是false,也可以指定为true,主要作用就是清除所有的缓存,而不以指定的key为主。

@CachePut

这个注解它总是会把数据缓存,而不会去每次做检查它是否存在,相比之下它的使用场景就比较少,毕竟我们希望并不是每次都把所有的数据都给查出来,我们还是希望能找到缓存的数据,直接返回,这样能提升我们的软件效率。

@cache

这个注解它是上面的注解的综合体,包含上面的三个注解(cacheable、cachePut、CacheEvict),可以使用这一个注解来包含上面的所有的注解,看源码如下
在这里插入图片描述
上面的注解总结如下表格:
在这里插入图片描述

我这里把上面介绍到的注解的相关属性,列一张表放出来放,这样方便理解
在这里插入图片描述

使用实例

首先我们在前文博文的基础上,建立数据库(如何建看前面博文,记得创建项目的时候勾选Mybatis),我们来新建一个表,含义为文章,下面的示例将会在这张表中进行操作,所使用的框架为SSM+SpringBoot,下面是创建数据库的表。

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE Artile (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`title` varchar(30) CHARACTER SET gbk COLLATE gbk_chinese_ci NULL DEFAULT NULL ,
`author` varchar(30) CHARACTER SET gbk COLLATE gbk_chinese_ci NULL DEFAULT NULL ,
`content` mediumtext CHARACTER SET gbk COLLATE gbk_chinese_ci NULL ,
`file_name` varchar(30) CHARACTER SET gbk COLLATE gbk_chinese_ci NULL DEFAULT NULL ,
`state` smallint(2) NULL DEFAULT 1 COMMENT '状态' ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=gbk COLLATE=gbk_chinese_ci
AUTO_INCREMENT=11
ROW_FORMAT=COMPACT

接着我们创建Mapper层,主要就是对Article进行增删改查的业务操作,映射到具体的xml的sql里(映射的原理我再前面博文的数据访问中有说过,当然,我们也可以直接在mapper层中写入@Select注解来写sql语句),然后用service去调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public interface ArticleMapper {
/**
* 插入一篇文章
* @param title
* @param author
* @param content
* @param fileName
* @return
*/
public Integer addArticle(@Param("title") String title,@Param("author")String author,
@Param("content")String content,@Param("fileName")String fileName);
/**
* 根据id获取文章
* @param id
* @return
*/
public Article getArticleById(@Param("id") Integer id);

/**
* 更新content
* @param content
*/
public Integer updateContentById(@Param("content")String content,@Param("id")Integer id);

/**
* 根据id删除文章
* @param id
* @return
*/
public Integer removeArticleById(@Param("id")Integer id);

/**
* 获得上一次插入的id
* @return
*/
public Integer getLastInertId();

}

然后就是service层,主要需要注意的是我们上述讲述的缓存注解都是基于service层(不能放在contoller和dao层),首先我们在类上配置一个CacheConfig,然后配置一个cacheNames,那么下面的方法都是以这个缓存名字作为默认值,他们的缓存名字都是这个,不必进行额外的配置。当进行select查询方法的时候,我们配置上@Cacheable,并指定key,这样除了第一次之外,我们都会把结果缓存起来,以后的结果都会把这个缓存直接返回。而当进行更新数据(删除或者更新操作)的时候,使用@CacheEvict来清除缓存,防止调用@Cacheabel的时候没有更新缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@Service
@CacheConfig(cacheNames = "articleCache")
public class ArticleService {

private AtomicInteger count =new AtomicInteger(0);

@Autowired
private ArticleMapper articleMapper;


/**
* 增加一篇文章 每次就进行缓存
* @return
*/
@CachePut
public Integer addArticle(Article article){
Integer result = articleMapper.addArticle(article.getTitle(), article.getAuthor(), article.getContent(), article.getFileName());
if (result>0) {
Integer lastInertId = articleMapper.getLastInertId();
System.out.println("--执行增加操作--id:" + lastInertId);
}
return result;
}

/**
* 获取文章 以传入的id为键,当state为0的时候不进行缓存
* @param id 文章id
* @return
*/
@Cacheable(key = "#id",unless = "#result.state==0")
public Article getArticle(Integer id) {
try {
//模拟耗时操作
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
final Article artcile = articleMapper.getArticleById(id);
System.out.println("--执行数据库查询操作"+count.incrementAndGet()+"次"+"id:"+id);
return artcile;
}

/**
* 通过id更新内容 清除以id作为键的缓存
*
* @param id
* @return
*/
@CacheEvict(key = "#id")
public Integer updateContentById(String contetnt, Integer id) {
Integer result = articleMapper.updateContentById(contetnt, id);
System.out.println("--执行更新操作id:--"+id);
return result;
}

/**
* 通过id移除文章
* @param id 清除以id作为键的缓存
* @return
*/
@CacheEvict(key = "#id")
public Integer removeArticleById(Integer id){
final Integer result = articleMapper.removeArticleById(id);
System.out.println("执行删除操作,id:"+id);
return result;
}

}

接着编写controller层,主要是接受客户端的请求,我们配置了@RestController表示它是一个rest风格的应用程序,在收到add请求会增加一条数据,get请求会查询一条数据,resh会更新一条数据,rem会删除一条数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@RestController
@ComponentScan(basePackages = {"com.wyq.controller", "com.wyq.service"})
@MapperScan(basePackages = {"com.wyq.dao"})
public class ArticleController {

@Autowired
private ArticleService articleService;

@Autowired
ArticleMapper articleMapper;

@PostMapping("/add")
public ResultVo addArticle(@RequestBody Article article) {

System.out.println(article.toString());
Integer result = articleService.addArticle(article);

if (result >= 0) {
return ResultVo.success(result);
}
return ResultVo.fail();
}


@GetMapping("/get")
public ResultVo getArticle(@RequestParam("id") Integer id) {

Long start = System.currentTimeMillis();
Article article = articleService.getArticle(id);
Long end = System.currentTimeMillis();
System.out.println("耗时:"+(end-start));

if (null != article)
return ResultVo.success(article);
return ResultVo.fail();
}


/**
* 更新一篇文章
*
* @param contetnt
* @param id
* @return
*/
@GetMapping("/resh")
public ResultVo update(@RequestParam("content") String contetnt, @RequestParam("id") Integer id) {
final Integer result = articleService.updateContentById(contetnt, id);
if (result > 0) {
return ResultVo.success(result);
} else {
return ResultVo.fail();
}
}

/**
* 删除一篇文章
*
* @param id
* @return
*/
@GetMapping("/rem")
public ResultVo remove(@RequestParam("id") Integer id) {

final Integer result = articleService.removeArticleById(id);
if (result > 0) {
return ResultVo.success(result);
} else {
return ResultVo.fail();
}
}

}

测试实例

这里使用postman模拟接口请求,首先我们来增加一篇文章:请求add接口:
在这里插入图片描述
后台返回表示成功:
在这里插入图片描述
我看到后台数据库已经插入了数据,它的id是11
在这里插入图片描述
执行查询操作,在查询操作中,getArticle,我使用线程睡眠的方式,模拟了5秒的时间来处理耗时性业务,第一次请求肯定会查询数据库,理论上第二次请求,将会走缓存,我们来测试一下:首先执行查询操作
在这里插入图片描述
接口响应成功,再看一下后台打印:表示执行了一次查询操作,耗时5078秒
在这里插入图片描述
好,重点来了,我们再次请求接口看看会返回什么?理论上,将不会走数据库执行操作,并且耗时会大大减少:与上面的比对,这次没有打印执行数据库查询操作,证明没有走数据库,并且耗时只有5ms,成功了!缓存发挥作用,从5078秒减小到5秒!大大提升了响应速度,哈哈!
在这里插入图片描述
更新操作,当我们进行修改操作的时候,我们希望缓存的数据被清空:看接口返回值成功了,再看数据库
在这里插入图片描述
在这里插入图片描述
后台控制台打印:

1
--执行更新操作id:--11

趁热打铁,我们再次请求三次查询接口,看看会返回什么?每次都会返回这样的结果,但是我的直观感受就是第一次最慢,第二次、第三次返回都很快
在这里插入图片描述
再看看后台打印了什么?执行id为11的数据库查询操作,这是因为缓存被清空了,所以它又走数据库了(获得最新数据),然后后面的查询都会走缓存!很明显,实验成功!
在这里插入图片描述
删除操作。同理,在删除操作中,执行了一次删除,那么缓存也会被清空,查询的时候会再次走数据库,这里就不给具体实验效果了。

整合redis实现缓存

SpringBoot整合redis其实非常简单(好吧SpringBoot整合啥都很简单),我们只要引入spring-boot-starter-data-redis,然后在application.yml中配置redis连接地址,接着通过使用RestTemplate操作redis,下面介绍Redis操作缓存的相关方法

  • redisTemplate.opsForValue();//操作字符串
  • redisTemplate.opsForHash();//操作hash
  • redisTemplate.opsForList();//操作list
  • redisTemplate.opsForSet();//操作set
  • redisTemplate.opsForZSet();//操作有序set
  • 配置缓存、CacheManagerCustomizers
  • 测试使用缓存、切换缓存、 CompositeCacheManager

下一篇

本篇博客介绍了springBoot中缓存的一些使用方法,如何在开发中使用缓存?怎样合理的使用都是值得我们学习的地方,本篇博客只是探讨的spring的注解缓存,相对来说比较简单。希望起到抛砖引玉的作用,下一篇博文我们将要介绍SpringBoot使用消息队列。