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序列化

这个好像不会....我再查查....

posted @ 2019-02-22 14:39  蜀云泉  阅读(504)  评论(0编辑  收藏  举报