SpringBoot笔记十二:缓存
讲解缓存这一块,我建议新建一个项目,勾选如下图内容
项目创建好之后,我们先来一个没有缓存的项目,这里我们会使用到注解开发
非缓存项目
数据库大家自己设计吧,随意,我的是这样的
/*
Navicat MySQL Data Transfer
Source Server : shuyunquan
Source Server Version : 80014
Source Host : localhost:3306
Source Database : test
Target Server Type : MYSQL
Target Server Version : 80014
File Encoding : 65001
Date: 2019-02-22 13:58:00
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for message
-- ----------------------------
DROP TABLE IF EXISTS `message`;
CREATE TABLE `message` (
`ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`COMMAND` varchar(16) DEFAULT NULL COMMENT '指令名称',
`DESCRIPTION` varchar(32) DEFAULT NULL COMMENT '描述',
`CONTENT` varchar(2048) DEFAULT NULL COMMENT '内容',
PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of message
-- ----------------------------
INSERT INTO `message` VALUES ('1', '查看', '精彩内容', '精彩内容');
INSERT INTO `message` VALUES ('2', '段子', '精彩段子', '如果你的月薪是3000块钱,请记得分成五份,一份用来买书,一份给家人,一份给女朋友买化妆品和衣服,一份请朋友们吃饭,一份作为同事的各种婚丧嫁娶的份子钱。剩下的2999块钱藏起来,不要告诉任何人');
INSERT INTO `message` VALUES ('3', '新闻', '今日头条', '7月17日,马来西亚一架载有298人的777客机在乌克兰靠近俄罗斯边界坠毁。另据国际文传电讯社消息,坠毁机型为一架波音777客机,机载约280名乘客和15个机组人员。\r\n乌克兰空管部门随后证实马航MH17航班坠毁。乌克兰内政部幕僚表示,这一航班在顿涅茨克地区上空被击落。马来西亚航空公司确认,该公司从阿姆斯特丹飞往吉隆坡的MH17航班失联,并称最后与该客机取得联系的地点在乌克兰上空。图为马航客机坠毁现场。');
INSERT INTO `message` VALUES ('4', '娱乐', '娱乐新闻', '昨日,邓超在微博分享了自己和孙俪的书法。夫妻同样写幸福,但差距很大。邓超自己都忍不住感慨字丑:左边媳妇写的。右边是我写的。看完我再也不幸福了。');
INSERT INTO `message` VALUES ('5', '电影', '近日上映大片', '《忍者神龟》[2]真人电影由美国派拉蒙影业发行,《洛杉矶之战》导演乔纳森·里贝斯曼执导。 \r\n片中四只神龟和老鼠老师都基于漫画和卡通重新绘制,由动作捕捉技术实现。\r\n其中皮特·普劳泽克饰演达芬奇(武器:武士刀),诺尔·费舍饰演米开朗基罗(武器:双节棍),阿伦·瑞奇森饰演拉斐尔(武器:铁叉),杰瑞米·霍华德饰演多拉泰罗(武器:武士棍)。\r\n该片计划于2014年8月8日在北美上映。');
INSERT INTO `message` VALUES ('6', '彩票', '中奖号码', '查啥呀查,你不会中奖的!');
然后很自然的,我们需要一个Java Bean与之对应
package com.example.bean;
public class Message {
private String id;
private String command;
private String description;
private String content;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getCommand() {
return command;
}
public void setCommand(String command) {
this.command = command;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return "Message{" +
"id='" + id + '\'' +
", command='" + command + '\'' +
", description='" + description + '\'' +
", content='" + content + '\'' +
'}';
}
}
我们开始使用Mybatis的注解方式进行开发了,不会的学学Mybatis
package com.example.mapper;
import com.example.bean.Message;
import org.apache.ibatis.annotations.*;
@Mapper
public interface MessageMapper {
@Select("select * from Message where id=#{id}")
public Message getMessageById(Integer id);
@Update("update message set COMMAND=#{command},DESCRIPTION=#{description},CONTENT=#{content} WHERE ID=#{id}")
public Message updateMessage(Message message);
@Delete("delete from Message where id=#{id}")
public void deleteMessageById(Integer id);
@Insert("INSERT message VALUES(#{command},#{description},#{content})")
public void insertMessage(Message message);
}
根据这个Mapper,新建一个Service,来实现具体的操作
package com.example.service;
import com.example.bean.Message;
import com.example.mapper.MessageMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class MessageService {
@Autowired
MessageMapper messageMapper;
public Message getMessage(Integer id){
System.out.println("查询" + id + "号数据");
Message message=messageMapper.getMessageById(id);
return message;
}
}
最后,新建Controller
package com.example.controller;
import com.example.bean.Message;
import com.example.service.MessageService;
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;
@RestController
public class MessageController {
@Autowired
MessageService messageService;
@GetMapping("/msg/{id}")
public Message getMessage(@PathVariable("id") Integer id)
{
Message message=messageService.getMessage(id);
return message;
}
}
最后的最后,我们的配置文件写一下,我这里使用的还是yml
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC
username: root
password: 123456
logging:
level:
com:
example:
mapper: debug
这里需要讲解一下,下面的logging.level在下面直接到mapper,设置为了debug,这个是为了在控制台输出sql语句,Mybatis输出sql语句就是这样写的
运行项目,浏览器输入 http://localhost:8080/msg/3
我们会发现控制台打印出了sql了,如图
这个时候,你可以在浏览器刷新几下,发现控制台每次刷新都会输出一次
这表明了,我们每次访问都会和数据库进行交互 |
这哪行啊,每次都和数据库进行交互,一个项目里面那么多地方需要和数据库交互,一个系统那么多人在用,数据库表示,我好累😭
为了解决数据库的负担,缓存,来了
缓存
缓存分为3种,JSR-107,Spring抽象缓存,整合Redis,接下来会分别介绍,其中,Spring抽象缓存是重头戏。
JSR-107
这个JSR-107缓存操作比较复杂,所以这个不使用,也不学习,有兴趣的可以自己了解一下
Spring缓存抽象
开始讲解重头戏了,这里我们要先了解一下必要的知识,一定要掌握记熟,如下表格:
名称 | 简介 |
---|---|
Cache | 缓存接口,有Redis和EhCache等实现,主要用于对缓存的增删改查 |
CacheManager | 缓存管理器,管理各种Cache组件 |
@Cacheable | 对方法进行缓存,例如查询用户的方法加上注解之后,查询结果存进缓存中,下次再查相同用户便会在缓存中查找 |
@CacheEvict | 清空缓存,一般用于删除方法,例如删除用户方法,加上注解顺便把缓存里的也删了 |
@CachePut | 更新缓存,例如更新用户方法,顺便把缓存中的用户信息也更新 |
@EnableCaching | 开启基于注解的缓存 |
keyGenerator | 缓存数据时key生成策略 |
serialize | 缓存数据时value序列化策略 |
接下来我们实际写一个例子来讲解和运用上面的知识,首先我们要开启基于注解的缓存,在主方法写一个@EnableCaching
@MapperScan("com.example.mapper")
@SpringBootApplication
@EnableCaching
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
加上了@EnableCaching之后,我们项目的缓存就已经可以使用注解方式了,我们在Service的获取方法上写一个缓存注解,3种操作注解@Cacheable是获取的,@CacheEvint是删除的,@CachePut是更新的,所以我们这里肯定使用@Cacheable啊
@Cacheable(cacheNames = "Msg")
public Message getMessage(Integer id){
System.out.println("查询" + id + "号数据");
Message message=messageMapper.getMessageById(id);
return message;
}
可以看到,我写了一个属性,叫cacheNames,这个是把缓存的数据存到Msg中去,当然这个名字是随意起的,缓存区的缓存可以分为不同的块,就比如春秋战国时期,天下就是缓存区,秦国是缓存块,楚国是缓存块,魏国是缓存块,你的缓存数据想存哪里,就起个名字就行了
我们这个时候已经开启了基于注解的缓存了,也在获取消息的方法上加了@Cacheable注解了,这个时候我们重启一下项目,在浏览器再次输入我们的获取地址 http://localhost:8080/msg/1
你第一次,控制台会输出sql信息,清空信息。你刷新浏览器发现,控制台什么消息都没有了,这就使用了缓存了,可以把数字换成2,3..第一次都会有sql信息输出的,之后都进了缓存区,无论你再怎么刷新浏览器,都会从缓存区里面取数据,数据库终于不累了。
@Cacheable
执行顺序:先查缓存,没有再查数据库
@Cacheable里面的属性有很多,例如
@Cacheable(cacheNames = "Msg",key = "#id",condition ="#id>0" ,unless = "#result == null")
cacheNames/value:指定缓存的名字
key:缓存存数据就是key-value形式,上面我们存数据了,怎么取呢?就是通过key来获取数据,默认情况下key=第一个参数,key="#id",这就是为什么我们上面的例子可以获取缓存数据的原因
condition:判断条件,例如"#id>0"我查询的信息的id必须是大于0的,我才去保存数据到缓存
unless:这个是除非,例如"#result==null" ,除非结果不是空,我才保存数据到缓存,#result就是查询返回的结果,可以把unless理解为if语句,if满足条件,不执行
@CachePut
执行顺序:先更新数据库,在更新缓存
@CachePut(cacheNames = "Msg",key = "#result.id")
public Message updateMessage(Message message){
System.out.println("更新" + message.getId() + "号数据");
Message message1 = messageMapper.updateMessage(message);
return message1;
}
因为更新之后会得到result,所以key可以写为result.id,也可以写为message.getId(),然后会同步更新到缓存的。如果不写key的话,key就是message,就不是查询缓存的id了,所以一定要写key
举例:我查找1号信息,描述为许嵩。我这时调用更新缓存的方法,把许嵩改为蜀云泉。再查找1号的信息,描述就变成蜀云泉了。所以@CachePut在更新数据库的时候会同步更新缓存。
@CacheEvict
执行顺序:这个由beforeInvocation属性决定,一般我们设置为true
/**
* 加一个allEntries = true,可以清除cacheName为Msg中的所有的缓存数据
* beforeInvocation=true,这个默认是false,设置为true就是在方法执行之前就清除缓存
* 举个例子,false的时候,万一方法执行出错了,数据库已经删了,但是缓存没删。设置为true之后,先删缓存
* @param id
*/
@CacheEvict(cacheNames = "Msg",key = "#id",beforeInvocation = true)
public void deleteMessage(Integer id){
System.out.println("删除" + id + "号数据");
messageMapper.deleteMessageById(id);
}
@Caching
@Caching这个缓存注解是一个整合注解,上面介绍的三个注解,都在这个的内部
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
Cacheable[] cacheable() default {};
CachePut[] put() default {};
CacheEvict[] evict() default {};
}
那么怎么使用这个整合注解呢?比如我还是写一个查询方法,我根据id进行查询,查询之后,我把这个消息根据id存入缓存,根据command存入缓存,根据description存入缓存
@Caching(
cacheable = {
@Cacheable(cacheNames = "Msg",key = "#id")
},
put = {
@CachePut(cacheNames = "Msg",key = "#result.command"),
@CachePut(cacheNames = "Msg",key = "#result.description")
}
)
public Message getMessageTest(Integer id){
System.out.println("查询" + id + "号数据");
return messageMapper.getMessageById(id);
}
这样,就实现了要求,我根据id查了1号信息,然后1号信息可以根据id,command,description这三种方式在缓存中查找。
@CacheConfig
这个就有点意思了啊,不知道你们发现了没,我们上面每一个缓存都需要写cacheNames="Msg",这样每个都写是有点重复了。所以可以使用@CacheConfig在类上面指定一下,那么这个类就都是这个缓存块下的缓存了
@Service
@CacheConfig(cacheNames = "Msg")
public class MessageService {
...
还是挺好用的
整合Redis
先在Docker里面开启我们的Redis容器
可以看到,我的Redis容器端口映射还是6379,这里我们使用Redis的客户端连接一下我的Redis容器(关于怎么安装Redis容器,我专门有一篇文章讲了Docker的使用)
下载Redis客户端,连接,输入我的Linux虚拟机的ip
项目引用Redis(牛逼的两个错误)
我们要在Maven里面引用Redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
引用了我们的Redis之后,上面的缓存的代码你运行一下试试,肯定报错了,这是因为Redis引用之后,RedisCacheConfiguration会替代我们原本的CacheConfiguration,所以你肯定报错。
怎么不报错呢?指定ip啊,你用Redis,却不告诉Redis在哪肯定报错啊,在application.yml里面写上我们Redis的IP,我这里是我的Linux里面的Redis容器
spring.redis.host=192.168.1.105
配置完成我们Redis的ip之后,你再执行,还是会报错
这个是因为,因为我这里调用的是上面Spring缓存讲的存入Message这个类,这个Object的方法,凡是Redis存储Object类型,必须序列化,不序列化就给你报错,我们给Message类加上序列化
public class Message implements Serializable {
Redis存储字符串
Redis有两个对象需要了解一下,一个是StringRedisTemplate,这个看名字就知道是操作字符串的。一个是RedisTemplate,这个是操作Object的,key-value都是Object。写一个测试类,先看测试一下。
@Autowired
StringRedisTemplate stringRedisTemplate; //操作字符串的
@Autowired
RedisTemplate redisTemplate; //k-v都是Object
@Test
public void redisTest(){
stringRedisTemplate.opsForValue().append("msg","Hello");
System.out.println(stringRedisTemplate.opsForValue().get("msg"));
}
我执行上面的测试代码,看看我的Redis客户端
非常好,我现在更改内容为Hello 许嵩,点击下面的保存按钮,然后上面的代码注释了写入那一行
stringRedisTemplate.opsForValue().append("msg","Hello");
只剩下一个get,执行一下,发现输出框显示的是Hello 许嵩,Nice啊!
Redis操作数据的结构
我上面使用了
stringRedisTemplate.opsForValue()
这是个啥?这个其实是操作String类型的数据的,类似的结构还有以下:
String字符串 stringRedisTemplate.opsForValue()
List列表 stringRedisTemplate.opsForList()
Set集合 stringRedisTemplate.opsForSet()
Hash散列 stringRedisTemplate.opsForHash()
ZSet有序集合 stringRedisTemplate.opsForZSet()
这几种数据结构的操作数据的方式如下:
String字符串:
stringRedisTemplate.opsForValue().append("msg","Hello");
System.out.println(stringRedisTemplate.opsForValue().get("msg"));
List列表:
stringRedisTemplate.opsForList().leftPush("mylist","1");
stringRedisTemplate.opsForList().leftPush("mylist","2");
stringRedisTemplate.opsForList().leftPop("mylist");
...Set,Hash,ZSet暂时不写
Redis存储Object类型
上面的存储字符串的尝试过了,现在来测试一下存储Object类型,这个在项目引用里面我已经讲过了,类必须序列化
还是我调用我的Controller
@Autowired
MessageService messageService;
@GetMapping("/msg/{id}")
public Message getMessage(@PathVariable("id") Integer id)
{
Message message=messageService.getMessageById(id);
return message;
}
我浏览器一输入,看看我的Redis
Nice啊!可以看到,我存储的Object类类型也是ok的,左边Object类型的像是一个文件夹一样的图标,打开我们的Msg 1,咋回事???为啥类序列化之后变成这狗样了?
这就是序列化类Object之后的结果。。。
当然,我们也可以使用Json来序列化Object,这样存到Redis的就是json数据,方便看了
Redis Object Json序列化
这个好像不会....我再查查....