缓存
缓存简介
缓存概要
缓存(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)测试更新
- 首先查询id为3的商品,查看日志输出了查询商品语句
- 然后更新id为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)测试删除
- 首先查询id为3的商品,查看日志输出了查询商品语句
- 然后删除id为3的商品,查看日志输出了删除商品语句,查看redis中删除了key=3的对象
- 再次查询id为3的商品,查看日志输出了查询商品语句,但是界面上没有显示商品信息
keyGenerator
@Cacheable注解的key是用方法参数作为缓存的键
如果方法没有参数,如何指定缓存的键呢?
例如:selectList方法没有参数
public List<Goods> selectList() {
return goodsMapper.selectList();
}
此时可以使用@Cacheable注解的keyGenerator属性类自定义缓存的键
思路是:
- 自定义一个Bean对象,该Bean必须实现KeyGenerator接口
- 实现KeyGenerator接口的generate()的方法,方法的返回值就是缓存的key
- 注意:这个自定义的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。