SpringBoot缓存管理
一、默认缓存管理
Spring框架支持透明地向应用程序添加缓存对缓存进行管理,其管理缓存的核心是将缓存应用于 操作数据的方法,从而减少操作数据的执行次数,同时不会对程序本身造成任何干扰。
Spring Boot继承了Spring框架的缓存管理功能,通过使用@EnableCaching注解开启基于注解的缓存支 持,Spring Boot就可以启动缓存管理的自动化配置。
基础环境搭建
1. 数据准备
# 创建数据库
CREATE DATABASE springbootdata;
# 选择使用数据库
USE springbootdata;
# 创建表t_article并插入相关数据
DROP TABLE IF EXISTS t_article;
CREATE TABLE t_article (
id INT ( 20 ) NOT NULL AUTO_INCREMENT COMMENT '文章id',
title VARCHAR ( 200 ) DEFAULT NULL COMMENT '文章标题',
content LONGTEXT COMMENT '文章内容',
PRIMARY KEY ( id )
) ENGINE = INNODB AUTO_INCREMENT = 2 DEFAULT CHARSET = utf8;
INSERT INTO t_article VALUES ('1', 'Spring Boot基础入门', '从入门到精通讲解...');
INSERT INTO t_article VALUES ('2', 'Spring Cloud基础入门', '从入门到精通讲解...');
# 创建表t_comment并插入相关数据
DROP TABLE IF EXISTS t_comment;
CREATE TABLE t_comment (
id INT ( 20 ) NOT NULL AUTO_INCREMENT COMMENT '评论id',
content LONGTEXT COMMENT '评论内容',
author VARCHAR ( 200 ) DEFAULT NULL COMMENT '评论作者',
a_id INT ( 20 ) DEFAULT NULL COMMENT '关联的文章id',
PRIMARY KEY ( id )
) ENGINE = INNODB AUTO_INCREMENT = 3 DEFAULT CHARSET = utf8;
INSERT INTO t_comment VALUES ('1', '很全、很详细', 'luccy', '1');
INSERT INTO t_comment VALUES ('2', '赞一个', 'tom', '1');
INSERT INTO t_comment VALUES ('3', '很详细', 'eric', '1');
INSERT INTO t_comment VALUES ('4', '很好,非常详细', '张三', '1');
INSERT INTO t_comment VALUES ('5', '很不错', '李四', '2');
2. 创建项目、编写功能
创建时需要选择web-Spring Web和SQL-MySQL Driver
(1) 在Dependencies依赖选择项中,添加SQL模块中的JPA依赖、MySQL依赖和Web模块中的Web依赖
(2) 编写数据库表对应的实体类,并使用JPA相关注解配置映射关系
@Entity(name = "t_comment") // 设置ORM实体类,并指定映射的表名
public class Comment {
@Id // 表明映射对应的主键id
@GeneratedValue(strategy = GenerationType.IDENTITY) // 设置主键自增策略
private Integer id;
private String content;
private String author;
@Column(name = "a_id") //指定映射的表字段名
private Integer aId;
// get/set/toString()
}
(3) 编写数据库操作的Repository接口文件
public interface CommentRepository extends JpaRepository<Comment,Integer> {
//根据评论id修改评论作者
@Transactional
@Modifying
@Query(value = "update t_comment c set c.author = ?1 where c.id=?2",nativeQuery = true)
public int updateComment(String author,Integer id);
}
(4) 编写service层
为了方便,不再编写接口,直接编写Service类
@Service
public class CommentService {
@Autowired
private CommentRepository commentRepository;
public Comment findCommentById(Integer id) {
Optional<Comment> comment = commentRepository.findById(id);
if (comment.isPresent()) {
Comment comment1 = comment.get();
return comment1;
}
return null;
}
}
(5) 编写Controller层
@RestController
public class CommentController {
@Autowired
private CommentService commentService;
// http://localhost:8080/findCommentById?id=1
@RequestMapping(value = "/findCommentById")
public Comment findCommentById(Integer id) {
Comment comment = commentService.findCommentById(id);
return comment;
}
}
(6) 编写配置文件
# MySQL数据库连接配置
spring.datasource.url=jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
#显示使用JPA进行数据库查询的SQL语句
spring.jpa.show-sql=true
#开启驼峰命名匹配映射
mybatis.configuration.map-underscore-to-camel-case=true
#解决乱码
spring.http.encoding.force-response=true
(7) 测试
访问url:http://localhost:8080/findCommentById?id=1 ,测试每次访问,都会查询数据库
默认缓存体验
(1)使用@EnableCaching注解开启基于注解的缓存支持
@EnableCaching // 开启Spring Boot基于注解的缓存管理支持
@SpringBootApplication
public class Springboot05CacheApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot05CacheApplication.class, args);
}
}
(2)使用@Cacheable注解对数据操作方法进行缓存管理。
//@Cacheable: 将该方法查询结果comment存放在springboot默认缓存中
//cacheNames: 起一个缓存命名空间 对应缓存唯一标识
@Cacheable(cacheNames = "comment")
public Comment findCommentById(Integer id) {
Optional<Comment> comment = commentRepository.findById(id);
if (comment.isPresent()) {
Comment comment1 = comment.get();
return comment1;
}
return null;
}
(3)测试访问
此时,多次访问,控制台只会查询一次数据库
底层结构 : 在诸多的缓存自动配置类中, SpringBoot默认装配的是 SimpleCacheConfiguration , 他使用的 CacheManager 是 ConcurrentMapCacheManager, 使用 ConcurrentMap 当底层的数据结构,按照Cache的名字查询出Cache, 每一个Cache中存在多个k-v键值对,缓存值
(4)缓存注解介绍
@EnableCaching注解
@EnableCaching是由spring框架提供的,springboot框架对该注解进行了继承,该注解需要配置在类上(在中,通常配置在项目启动类上),用于开启基于注解的缓存支持
@Cacheable注解
@Cacheable注解也是由spring框架提供的,可以作用于类或方法(通常用在数据查询方法上)
@Cacheable注解提供了多个属性
| 属性名 | 说明 |
|---|---|
| value/cacheNames | 指定缓存空间的名称,必配属性。这两个属性二选一使用 |
| key | 指定缓存数据的key,默认使用方法参数值,可以使用SpEL表达式 |
| keyGenerator | 指定缓存数据的key的生成器,与key属性二选一使用 |
| cacheManager | 指定缓存管理器 |
| cacheResolver | 指定缓存解析器,与cacheManager属性二选一使用 |
| condition | 指定在符合某条件下,进行数据缓存 |
| unless | 指定在符合某条件下,不进行数据缓存 |
| sync | 指定是否使用异步缓存。默认false |
方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定的名字获取,(CacheManager先获取相应的缓存),第一次获取缓存如果没有Cache组件会自动创建;
去Cache中查找缓存的内容,使用一个key,默认就是方法的参数,如果多个参数或者没有参数,是按 照某种策略生成的,默认是使用KeyGenerator生成的,使用SimpleKeyGenerator生成key, SimpleKeyGenerator生成key的默认策略:
| 参数个数 | key |
|---|---|
| 没有参数 | new SimpleKey() |
| 有一个参数 | 参数值 |
| 多个参数 | new SimpleKey(params) |
@CachePut注解
目标方法执行完之后生效, @CachePut被使用于修改操作比较多,哪怕缓存中已经存在目标值了,但是这个注解保证这个方法依然会执行,执行之后的结果被保存在缓存中
@CacheEvict注解
@CacheEvict注解是由Spring框架提供的,可以作用于类或方法(通常用在数据删除方法上),该注解的作用是删除缓存数据。@CacheEvict注解的默认执行顺序是,先进行方法调用,然后将缓存进行清除。
二、整合Redis缓存实现
Spring Boot支持的缓存组件
在Spring Boot中,数据的缓存管理存储依赖于Spring框架中cache相关的 org.springframework.cache.Cache和org.springframework.cache.CacheManager缓存管理器接口。
如果程序中没有定义类型为CacheManager的Bean组件或者是名为cacheResolver的CacheResolver缓存解析器,Spring Boot将尝试选择并启用以下缓存组件(按照指定的顺序):
(1) Generic
(2) JCache (JSR-107) (EhCache 3、Hazelcast、Infinispan等) (3)EhCache 2.x
(4) Hazelcast
(5) Infinispan
(6) Couchbase
(7) Redis
(8) Caffeine
(9) Simple
基于注解的Redis缓存实现
(1)添加Spring Data Redis依赖启动器。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
(2)Redis服务连接配置
#Redis服务地址
spring.redis.host=127.0.0.1
#Redis服务器连接端口
spring.redis.port=6379
#Redis服务器连接密码
spring.redis.password=
(3)对CommentService类中的方法进行修改使用@Cacheable、@CachePut、@CacheEvict三个注解定制缓存管理,分别进行缓存存储、缓存更新和缓存删除的演示
@Service
public class CommentService {
@Autowired
private CommentRepository commentRepository;
//@Cacheable: 将该方法查询结果comment存放在springboot默认缓存中
//cacheNames: 起一个缓存命名空间 对应缓存唯一标识
// value: 缓存结果 key:默认在只有一个参数的情况下,key值默认就是方法参数值 如果没有参数或者多个参数的情况:simpleKeyGenerate
@Cacheable(cacheNames = "comment" ,unless = "#result == null")
public Comment findCommentById(Integer id) {
Optional<Comment> comment = commentRepository.findById(id);
if (comment.isPresent()) {
Comment comment1 = comment.get();
return comment1;
}
return null;
}
//更新方法
@CachePut(cacheNames = "comment",key = "#result.id")
public Comment updateComment(Comment comment){
commentRepository.updateComment(comment.getAuthor(),comment.getId());
return comment;
}
//删除方法
@CacheEvict(cacheNames = "comment")
public void deleteComment(Integer id){
commentRepository.deleteById(id);
}
}
在查询缓存@Cacheable注解中,定义了 “unless = "#result==null"”表示查询结果为空不进行缓存
(4)将缓存对象实现序列化。
(5)启动测试,查询测试
查询后,Redis中存储了缓存值
(6) 基于注解的Redis缓存更新测试。
http://localhost:8080/updateComment?id=1&author=aaabb 执行后,数据库发生变化
日志打印中只有更新语句,没有查询语句了。
可以看出,执行updateComment()方法更新id为1的数据时执行了一条更新SQL语句,后续调用 findById()方法查询id为1的用户评论信息时没有执行查询SQL语句,且浏览器正确返回了更新后的结 果,表明@CachePut缓存更新配置成功
(7)基于注解的Redis缓存删除测试
访问:http://localhost:8080/deleteComment?id=1数据被删除,同时Redis缓存也被删除
此外,还可以设置缓存有效期
# 对基于注解的Redis缓存数据统一设置有效期为1分钟,单位毫秒
spring.cache.redis.time-to-live=60000
上述代码中,在Spring Boot全局配置文件中添加了“spring.cache.redis.time-to-live”属性统一配 置Redis数据的有效期(单位为毫秒),但这种方式相对来说不够灵活
基于API的Redis缓存实现
(1)使用Redis API进行业务数据缓存管理。
修改Service方法
@Service
public class ApiCommentService {
@Autowired
private CommentRepository commentRepository;
@Autowired
private RedisTemplate redisTemplate;
// 先使用API的方式进行缓存,先去缓存查,没有 再查数据库
public Comment findCommentById(Integer id) {
Object o = redisTemplate.opsForValue().get("comment_" + id);
if(o != null){
// 缓存中查到就返回
return (Comment) o;
}
// 缓存没有,去数据库查询
Optional<Comment> comment = commentRepository.findById(id);
if (comment.isPresent()) {
Comment comment1 = comment.get();
//将查询结果存到缓存中,同时还可以设置有效期为1天
redisTemplate.opsForValue().set("comment_" + id,comment1,1, TimeUnit.DAYS);
return comment1;
}
return null;
}
// 更新
public Comment updateComment(Comment comment){
commentRepository.updateComment(comment.getAuthor(),comment.getId());
redisTemplate.opsForValue().set("comment_"+comment.getId(),comment);
return comment;
}
public void deleteComment(Integer id){
commentRepository.deleteById(id);
redisTemplate.delete("comment_"+id);
}
}
(2)编写Web访问层Controller文件
@RestController
@RequestMapping("api")
public class ApiCommentController {
@Autowired
private ApiCommentService commentService;
// http://localhost:8080/api/findCommentById?id=1
@RequestMapping(value = "/findCommentById")
public Comment findCommentById(Integer id) {
Comment comment = commentService.findCommentById(id);
return comment;
}
@RequestMapping(value = "/updateComment")
public Comment updateComment(Comment comment) {
Comment commentById = commentService.findCommentById(comment.getId());
commentById.setAuthor(comment.getAuthor());
Comment res = commentService.updateComment(commentById);
return res;
}
@RequestMapping(value = "/deleteComment")
public void deleteComment(Integer id) {
commentService.deleteComment(id);
}
}
基于API的Redis缓存实现的相关配置。基于API的Redis缓存实现不需要@EnableCaching注解开启 基于注解的缓存支持,所以这里可以选择将添加在项目启动类上的@EnableCaching进行删除或者注释
三、自定义缓存序列化机制
自定义RedisTemplate
Redis API默认序列化机制
打开 RedisTemplate类,查看该类的源码信息可以得到两个结论:
(1)使用RedisTemplate进行Redis数据缓存操作时,内部默认使用的是 JdkSerializationRedisSerializer序列化方式,所以进行数据缓存的实体类必须实现JDK自带的序列化接 口(例如Serializable);
(2)使用RedisTemplate进行Redis数据缓存操作时,如果自定义了缓存序列化方式 defaultSerializer,那么将使用自定义的序列化方式。
缓存数据key、value的各种序列化类型都是 RedisSerializer而默认提供的实现类有下面六种:
自定义RedisTemplate序列化机制
在项目中引入Redis依赖后,Spring Boot提供的RedisAutoConfiguration自动配置会生效。打开 RedisAutoConfiguration类,查看内部源码中关于RedisTemplate的定义方式
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
从上述RedisAutoConfiguration核心源码中可以看出,在Redis自动配置类中,通过Redis连接工厂 RedisConnectionFactory初始化了一个RedisTemplate;该类上方添加了 @ConditionalOnMissingBean注解(顾名思义,当某个Bean不存在时生效),用来表明如果开发者自定义了一个名为redisTemplate的Bean,则该默认初始化的RedisTemplate不会生效。
并按照上述思路自定义名为redisTemplate的Bean组件
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
// 使用JSON格式序列化对象,对缓存数据key和value进行转换
Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 设置RedisTemplate模板API的序列化方式为JSON template.setDefaultSerializer(jacksonSeial);
return template;
}
}
通过@Configuration注解定义了一个RedisConfig配置类,并使用@Bean注解注入了一个默认名 称为方法名的redisTemplate组件(注意,该Bean组件名称必须是redisTemplate)。在定义的Bean组 件中,自定义了一个RedisTemplate,使用自定义的Jackson2JsonRedisSerializer数据序列化方式;在 定制序列化方式中,定义了一个ObjectMapper用于进行数据转换设置
测试效果
启动并访问:http://localhost:8080/api/findCommentById?id=3 结果以json的方式缓存在Redis,如下:
自定义RedisCacheManager
对于注解的模板,一般使用以下固定模板:
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 分别创建String和JSON格式序列化对象,对缓存数据key和value进行转换
RedisSerializer<String> strSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jacksonSeial =
new Jackson2JsonRedisSerializer(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 定制缓存数据序列化方式及时效
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(strSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(jacksonSeial))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager
.builder(redisConnectionFactory).cacheDefaults(config).build();
return cacheManager;
}
启动并访问:http://localhost:8080/findCommentById?id=4