JavaEE——Spring Boot+Redis

缓存

缓存简介

缓存概要

缓存(cache)

  • 就是保存在服务器内存中的数据

缓存的作用:

  • 若没有缓存,每次查询都到数据库中查询
  • 若有缓存,第一次查询到数据库中查询,然后将查询的对象缓存,再次查询时从缓存查询
  • 从缓存查询(内存)的效率远远高于从数据库(外存)查询的效率
  • 因此缓存提升了查询效率
  • 当数据被更新时,同步清除缓存,防止脏数据

使用缓存的场景:

  • 查询数据(如:商品浏览,新闻浏览)
  • 临时存储的短信验证码(5分钟有效期,这种临时数据没必要存储在数据库中)

不适合使用缓存的场景:
股票

JSR107规范

为了统一缓存组件的规范,提供系统的扩展性,JavaEE提出了JSR107规范。
JSR(Java Specification Requests的缩写,意思是Java 规范提案)
2012年10月26日JSR规范委员会发布了JSR 107(JCache API)的首个早期草案
JSR 107将成为2013年第2季度发布的JavaEE 7的一部分。
简而言之,就是该规范告诉我们如何在java中使用缓存。和JDBC类似,JSR-107也有多种实现厂商,比如redis、EhCache、ConcurrentMap。

缓存组件

JSR107只是规范,在使用缓存时,还要选择该规范的实现。

符合JSR107规范的缓存组件有
Redis缓存组件
EhCache缓存组件
ConcurrentMap缓存组件

Spring boot默认使用ConcurrentMap组件作为缓存组件。

JSR107规范中的5个核心接口

JSR107定义了5个核心接口,分别是CachingProvider, CacheManager, Cache, Entry , Expiry。

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

Spring缓存

  • Spring从3.1开始定义了org.springframework.cache.Cache
    和org.springframework.cache.CacheManager接口来统一不同的缓存技术
  • 支持使用JSR-107注解简化开发
  • Cache接口为缓存的组件规范定义,包含缓存的各种操作集合
  • Spring支持Cache接口的多种实现,如Redis,EhCache , ConcurrentMap等

在这里插入图片描述

缓存注解

在这里插入图片描述

缓存案例:(1)数据查询环境准备

本案例使用redis缓存组件,关于redis缓存服务器的安装,参见redis.md文档

第一步:创建项目

创建项目选择web模块
在这里插入图片描述

第二步:在 pom.xml 中添加相关 jar 依赖

<dependencies>
<!-- 添加ConcurrentMap cache起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- 添加mybatis-spring整合起步依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<!-- 添加mysql驱动jar包 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 添加web起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 添加测试起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 添加DRUID数据源依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
</dependencies>

第三步:配置数据源,日志输出sql语句

SpringBoot2.6.0版本默认使用Hikari数据源

#配置数据库的连接信息
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/eshop?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=
#切换成Druid数据源
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
#配置日志输出SQL语句
logging.level.com.ltw.mapper=debug

第四步:编写Model类

在com.ltw.model包中创建实体类

实体类必须序列化,否则缓存失败

public class Goods implements Serializable {
private Integer id;
private String goodsname;
private Double price;
private String memo;
private String pic;
private Date createtime;
//省略部分代码
}

第五步:编写Controller

在com.ltw.controller包中创建控制器
该控制器已经注入业务对象,提供了根据id查询商品的方法selectById()

@Controller
@RequestMapping("/goods")
public class GoodsController {
@Autowired
private IGoodsService goodsService;
@RequestMapping("/selectById/{id}")
public @ResponseBody Object selectById(@PathVariable("id") Integer id){
Goods goods = goodsService.selectById(id);
return goods;
}
public IGoodsService getGoodsService() {
return goodsService;
}
public void setGoodsService(IGoodsService goodsService) {
this.goodsService = goodsService;
}
}

第六步:编写Service接口和实现类

在com.ltw.service包中创建业务接口
该业务接口定义了根据主键id查询商品的selectById方法,返回Goods对象

public interface IGoodsService {
Goods selectById(Integer id);
}

在com.ltw.service.impl包中创建业务实现类GoodsService
该业务实现完成了根据主键id查询商品的selectById方法,返回Goods对象

@Service
public class GoodsService implements IGoodsService {
@Autowired
private IGoodsMapper goodsMapper;
@Override
public Goods selectById(Integer id) {
return goodsMapper.selectById(id);
}
public IGoodsMapper getGoodsMapper() {
return goodsMapper;
}
public void setGoodsMapper(IGoodsMapper goodsMapper) {
this.goodsMapper = goodsMapper;
}
}

第七步:编写Mapper接口

在com.ltw.mapper包中创建Mapper接口
该Mapper接口定义了根据主键id查询商品的selectById方法,返回Goods对象

public interface IGoodsMapper {
Goods selectById(Integer id);
}

第八步:编写Mapper映射

在com.ltw.mapper包中创建Mapper映射配置文件
该映射文件配置了根据id查询商品的select语句

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ltw.mapper.IGoodsMapper">
<resultMap id="goodsMap" type="com.ltw.model.Goods">
<id column="id" property="id" />
<result column="goodsname" property="goodsname" />
<result column="price" property="price"/>
<result column="memo" property="memo"/>
<result column="pic" property="pic"/>
<result column="createtime" property="createtime"/>
</resultMap>
<select id="selectById" resultType="int" resultMap="goodsMap">
select * from goods where id=#{id}
</select>
</mapper>

第九步:配置Mapper映射文件编译目录

在pom文件中配置

<resources>
<!-- :默认情况下,Mybatis 的 xml 映射文件不会编译到 target 的 class 目录下,
所以我们需要在 pom.xml 文件中配置 resource,将映射文件编译到resource目录下 -->
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>

第十步:配置Mapper映射文件存储位置

#配置mapper映射文件存储位置
mybatis.mapper-locations=classpath:**/mapper/*.xml

第十一步:打开@MapperScan注解

@SpringBootApplication
@MapperScan(basePackages = "com.ltw.mapper")
public class Springboot007Application {
public static void main(String[] args) {
SpringApplication.run(Springboot005Application.class, args);
}
}

到此为止,数据查询的准备工作完成了。下面准备缓存环境

缓存案例:(2)缓存环境准备

第一步:导入redis起步依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

第二步:设置redis服务器

spring.redis.host=127.0.0.1

第三步:开启缓存

@SpringBootApplication
//扫描Mapper接口包,实现IoC
@MapperScan(basePackages = "com.ltw.mapper")
//开启事务支持(可选项,但@Transactional 必须添加)
@EnableTransactionManagement
//开启缓存
@EnableCaching
public class Springboot007Application {
public static void main(String[] args) {
SpringApplication.run(Springboot007Application.class, args);
}
}

在启动类上标注@EnableCaching表示当前项目开启了缓存。这里的开启缓存是指当前项目允许使用缓存,具体要缓存哪些数据不是@EnableCaching 的任务,要缓存的具体数据是其他注解的任务。
到此为止,缓存环境准备好了,下面开始测试缓存。

缓存测试

不使用缓存查询商品

查询id为3的商品

在这里插入图片描述
控制台日志输出了查询语句

DEBUG com.ltw.mapper.IGoodsMapper.selectById -> Preparing: select *from goods where id=?
DEBUG com.ltw.mapper.IGoodsMapper.selectById -> Parameters: 3(String)
DEBUG com.ltw.mapper.IGoodsMapper.selectById -> Total: 1

说明本次查询是连接数据库进行的查询
多次刷新该查询,每次查询控制台日志都输出了查询语句
说明每次查询都是连接数据库进行的查询,因此大量并发查询时查询效率低。

使用缓存查询商品@Cacheable

使用@Cacheable注解查询商品。
在业务类的查询方法selectById()上标注@Cacheable注解,该方法启用查询缓存。

/**
* @Cacheable注解
* 该注解将方法的查询结果缓存,缓存后数据查询规则为
* (1)每次查询数据时,从缓存中读取数据,
* (2)若从缓存中读到了数据就直接返回数据,不在调用该方法
* (3)若从缓存中未读到数据,则调用该方法,并将方法返回值缓存
* cacheNames|value属性
* (1)设置缓存名称
* (2)通常使用表名称作为该属性的值
* key属性:
* (1)被缓存对象的键,默认是方法参数的值作为键
* condition属性:
* (1)表示缓存条件,满足缓存条件的对象才被缓存
* (2)本例中表示方法参数id的值>0时被缓存
*/
@Cacheable(cacheNames={"goods"},key = "#id",condition = "#id>0")
@Override
public Goods selectById(Integer id) {
logger.info("查询了主键为"+ id +"的商品");
return goodsMapper.selectById(id);
}

重新启动项目,首次查询id为3的商品,控制台输出了查询输出了查询语句,日志如下:
在这里插入图片描述
查看redis客户端,发现该商品已经被缓存到redis中
在这里插入图片描述
反复刷新页面查询,发现只有第一次查询时控制台输出了查询日志,以后的查询都是在缓存中查询。

修改数据同步更新缓存@CachePut

(1)IGoodsMapper接口添加update方法

void update(Goods goods);

(2)IGoodsMapper映射文件,添加update方法

<update id="update" parameterType="com.ltw.model.Goods">
update goods set goodsname=#{goodsname},price=#{price},memo=#{memo},
pic=#{pic},createtime=#{createtime} where id=#{id}
</update>

(3)业务接口添加update方法

Goods update(Goods goods);

(4)业务实现添加update方法

@CachePut注解实现更新数据时,同步更新缓存

/**
* @CachePut注解
* 该注解用于在调用了方法之后更新缓存中的数据,防止脏读。
* 执行流程
* (1)调用方法更新数据库中的数据
* (2)更新缓存中的数据,使缓存中的数据同步于数据库中的数据
* value属性:
* (1)设置缓存名称
* (2)通常使用表名称作为该属性的值
* key属性:
* (1)被缓存对象的键,默认是方法参数的值作为键。
* (2)本例中#goods.id表示方法参数对象goods的id属性作为key
*/
@CachePut(value = "goods",key = "#goods.id")
public Goods update(Goods goods){
logger.info("准备修改商品:"+goods.toString());
goodsMapper.update(goods);
return goods;
}

(5)控制器添加update请求
控制器方法传入要更新的商品主键值,然后从数据库中查询出该商品,将该商品的memo更新为“备注更新”。

@RequestMapping("/update/{id}")
public @ResponseBody Object update(@PathVariable("id") Integer id){
Goods goods = goodsService.selectById(id);
goods.setMemo("备注更新");
goodsService.update(goods);
return goods;
}

(6)测试更新

  1. 首先查询id为3的商品,查看日志输出了查询商品语句
  2. 然后更新id为3的商品,查看日志输出了更新商品语句
  3. 再次查询id为3的商品,查看日志输出了查询商品语句

删除商品同步清除缓存@CacheEvict

(1)IGoodsMapper接口添加delete方法

void delete(Integer id);

(2)IGoodsMapper映射文件,添加delete方法

<delete id="delete" parameterType="int">
delete from goods where id=#{id}
</delete>

(3)业务接口添加delete方法

Integer delete(Integer id);

(4)业务实现添加delete方法

@CacheEvict注解实现删除数据时,同步删除缓存

/**
* @CacheEvict注解
* 清除缓存中的数据
* value属性
* (1)设置缓存名称
* (2)通常使用表名称作为该属性的值
* key属性:
* (1)被缓存对象的键,默认是方法参数的值作为键,本例中#id表示方法参数id作为key
*/
@CacheEvict(value = "goods",key = "#id")
public Integer delete(Integer id){
goodsMapper.delete(id);
return id;
}

(5)控制器添加delete请求
控制器方法传入要删除的商品主键值,然后从数据库中删除该商品

@RequestMapping("/delete/{id}")
public @ResponseBody Object delete(@PathVariable("id") Integer id){
goodsService.delete(id);
return id;
}

(6)测试删除

  1. 首先查询id为3的商品,查看日志输出了查询商品语句
  2. 然后删除id为3的商品,查看日志输出了删除商品语句,查看redis中删除了key=3的对象
  3. 再次查询id为3的商品,查看日志输出了查询商品语句,但是界面上没有显示商品信息

keyGenerator

@Cacheable注解的key是用方法参数作为缓存的键
如果方法没有参数,如何指定缓存的键呢?
例如:selectList方法没有参数

public List<Goods> selectList() {
return goodsMapper.selectList();
}

此时可以使用@Cacheable注解的keyGenerator属性类自定义缓存的键
思路是:

  1. 自定义一个Bean对象,该Bean必须实现KeyGenerator接口
  2. 实现KeyGenerator接口的generate()的方法,方法的返回值就是缓存的key
  3. 注意:这个自定义的Bean要交给spring容器管理,只要在类上标注@Configuration,就表示该类
    中标注了@Bean方法的返回值交给spring容器。

第一步:定义key的生成器类

package com.ltw.config;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
public class RedisKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return target.getClass().getName()+method.getName();
}
}

第二步:将生成器类的对象交给spring容器管理
自定义个配置类,在配置类中定义RedisKeyGenerator对象。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedisConfigurator {
@Bean("redisKeyGenerator")
public RedisKeyGenerator redisKeyGenerator(){
return new RedisKeyGenerator();
}
}

第三步:在查询方法上使用redisKeyGenerator作为key

@Cacheable(value = "goods",keyGenerator ="redisKeyGenerator" )
public List<Goods> selectList() {
return goodsMapper.selectList();
}

总结

Spring Boot 开发实质上也是一个常规的 Spring 项目开发,只是利用了 SpringBoot 启动程序和自动配置简化开发过程,提高开发效率。
SpringBoot 项目开发代码的实现依然是使用 SpringMVC+ Spring + MyBatis。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Geek Li

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值