Spring Boot缓存管理
缓存是分布式系统中的重要组件,主要解决数据库数据的高并发访问问题。在实际开发中,尤其是用户访问较大的网站,为了提高服务器访问性能、减少数据库的压力、提高用户体验,使用缓存显得尤为重要。Spring Boot对缓存提供了良好的支持。本章将针对Spring Boot的缓存管理进行介绍,并完成Spring Boot与Redis缓存中间件的整合使用。
Spring Boot默认缓存管理
Spring框架支持透明地向应用程序添加缓存并对缓存进行管理,其管理缓存的核心是将缓存应用于操作数据的方法中,从而减少操作数据的次数,同时不会对程序本身造成任何干扰。Spring Boot继承了Spring框架的缓存管理功能,通过使用@EnableCaching注解开启基于注解的缓存支持,Spring Boot可以启动缓存管理的自动化配置。
基础环境搭建
使用缓存的主要的目的是减少数据库的访问压力、提高用户体验,为此,治理我们结合数据库的访问操作对Spring Boot的缓存管理进行演示说明。下面我们先搭建演示Spring Boot缓存管理的基础环境。
准备数据
为了简便,这里使用第3章创建的springbootdata数据库,该数据库有两个表t_article和t_comment。这两个表预先插入了几条测试数据。
创建项目
1)创建Spring Boot项目,引入相关依赖。使用Spring Initlaizr方式创建一个名为chapter06的Spring Boot项目,在Dependencies依赖选项中添加SQL模块中的JPA依赖、MySQL依赖,web模块中的Web依赖。
2)编写数据库表对应的实体类。在chapter06中创建名为com.example.chapter06.domain的包,在该包下创建针对数据库表t_comment编写对应的实体类Comment,并使用JPA相关注解配置映射关系。
Comment.java
package com.example.chapter06.domain;
import javax.persistence.*;
@Entity(name = "t_comment")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String content;
private String author;
@Column(name = "a_id")
private Integer aId;
//省略属性getter和setter方法
//省略toString()方法
}
3)编写数据库操作的Repository接口文件。在chapter06中创建名为com.exmaple.chapter06.repository的包,并在该包下创建一个用于操作Comment实体的Repository接口,该接口继承自JapRepository,并且包含一个用于修改评论的方法updateComment()。
CommentRepository.java
package com.example.chapter06.Repository;
import com.example.chapter06.domain.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
public interface CommentRepository extends JpaRepository<Comment, Integer> {
@Transactional
@Modifying
@Query("update t_comment c set c.author=?1 where c.id=?2")
public int updateComment(String author,Integer id);
}
4)编写业务操作类Service文件。在chapter06中创建名为com.exmaple.chapter06.service的包,并在该包下创建一个用于Comment相关业务操作的Service实体类。
package com.example.chapter06.service;
import com.example.chapter06.Repository.CommentRepository;
import com.example.chapter06.domain.Comment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class CommentService {
@Autowired
private CommentRepository commentRepository;
public Comment findById(int comment_id){
Optional<Comment>optional=commentRepository.findById(comment_id);
if(optional.isPresent()){
return optional.get();
}
return null;
}
public Comment updateComment(Comment comment){
commentRepository.updateComment(comment.getAuthor(),comment.getaId());
return comment;
}
public void deleteComment(int comment_id){
commentRepository.deleteById(comment_id);
}
}
自定义了一个CommentService业务操作类,使用注入的CommentRepository实例对象完成对Comment评论数据的查询、修改和删除操作。
5)编写Web访问层Controller文件。在chapter06中创建名为com.exmaple.chapter06.controller的包,并在该包下创建一个用于Comment访问控制的Controller实体类。
CommentController.java
package com.example.chapter06.controller;
import com.example.chapter06.domain.Comment;
import com.example.chapter06.service.CommentService;
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 CommentController {
private final CommentService commentService;
@Autowired
public CommentController(CommentService commentService) {
this.commentService = commentService;
}
@GetMapping("/get/{id}")
public Comment findById(@PathVariable("id") int comment_id){
return commentService.findById(comment_id);
}
@GetMapping("/update/{id}/{author}")
public Comment updateComment(@PathVariable("id") int comment_id,@PathVariable("author") String author){
Comment comment=commentService.findById(comment_id);
comment.setAuthor(author);
return commentService.updateComment(comment);
}
@GetMapping("/delete/{id}")
public void deleteComment(@PathVariable("id") int comment_id){
commentService.deleteComment(comment_id);
}
}
编写配置文件
application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
先对MySQL的连接进行了配置,然后配置了spring.jpa.show-sql=true用于展示操作的SQL语句,方便后续开启缓存时进行效果演示。
项目测试
启动chapter06项目,项目启动成功后,在浏览器*问
查询id为1的用户评论信息,不论浏览器刷新多少次,访问同一个用户评论信息,页面的查询结果都会显示同一条数据,但是,浏览器每刷新一次,控制台会新输出一条SQL语句。
这是因为没有在Spring Boot项目中开启缓存管理。在没有缓存管理的情况下,虽然数据表中的数据没有发生变化,但是每执行一次查询操作,都会访问一次数据库并执行一次SQL语句。随着时间的积累,系统的二哟弄个胡不断增加,数据规模越来越大,数据库的操作会直接影响用户的使用体验,此时使用缓存往往是解决这一问题非常好的一种手动。
Spring Boot默认缓存体验
在前面搭建的Web应用基础上,开启Spring Boot默认支持的缓存,体验Spring Boot默认缓存的使用效果。
1)使用@EnableCaching注解开启基于注解的缓存支持,该注解通常会添加在项目启动类上。
Chapter06Application.java
package com.example.chapter06;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching //开启Spring Boot基于注解的缓存管理支持
public class Chapter06Application {
public static void main(String[] args) {
SpringApplication.run(Chapter06Application.class, args);
}
}
2)使用@Cacheable注解对数据操作方法进行缓存管理。将@Cacheable注解标注在Service类的查询方法上,对查询结果进行缓存。
package com.example.chapter06.service;
import com.example.chapter06.Repository.CommentRepository;
import com.example.chapter06.domain.Comment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Cacheable(cacheNames = "comment")
@Service
public class CommentService {
private final CommentRepository commentRepository;
@Autowired
public CommentService(CommentRepository commentRepository) {
this.commentRepository = commentRepository;
}
public Comment findById(int comment_id){
Optional<Comment>optional=commentRepository.findById(comment_id);
return optional.orElse(null);
}
public Comment updateComment(Comment comment){
commentRepository.updateComment(comment.getAuthor(),comment.getaId());
return comment;
}
public void deleteComment(int comment_id){
commentRepository.deleteById(comment_id);
}
}
上述代码中,在CommentService类中的findById(int comment_id)方法上添加了查询缓存注解@Cacheable,该注解的作用是将查询结果comment存放在Spring Boot默认缓存中名称为comment的名称空间中,对应缓存的唯一标识默认为方法参数comment_id的值。
3)Spring Boot默认缓存测试。启动chapter06项目,浏览器访问,刷新几次。
无论刷新几次只有一句查询语句。
Spring Boot缓存注解介绍
@EnableCaching注解
@EnableCaching是由Spring框架提供的,Spring Boot框架对该注解进行了继承,该注解需要配置在类上(在Spring Boot,通过配置在项目启动类上),用于开启基于注解的缓存支持。
@Cacheable注解
Cacheable注解也是由Spring框架提供的,可以作用于类或方法,用于对方法的查询结果进行缓存存储。@Cacheable注解的执行顺序是,先进行缓存查询,如果为空则进行方法查询,并将结果进行缓存;如果缓存中有数据,不进行方法查询,而是直接使用缓存数据。
属性名 | 说明 |
---|---|
value/cacheNames | 制定缓存空间的名称,必配属性。这两个属性二选一使用 |
key | 制定缓存数据的key,默认使用方法参数值,可以使用SpEL表达式 |
keyGenerator | 制定缓存数据的key的生成器,与key属性二选一使用。 |
cacheManager | 指定缓存管理器 |
cacheResolver | 指定缓存解析器,与cacheManager属性二选一使用 |
condition | 指定在符合某条件下,进行数据缓存 |
unless | 指定在符合某条件下,不进行数据缓存 |
sync | 指定是否使用异步缓存。默认false |
下面我们针对@Cacheable注解的属性进行具体讲解
1)value/cacheNames属性
value和cacheNames属性作用相同,用于指定缓存的名称空间,可以同时指定多个名称空间(例如@Cacheable(cacheable=(“comment1”,“comment2”)))。如果注解只配置value或cacheNames的一个属性,那么这两个属性名可以省略。
2)key属性
key属性的作用是指定缓存数据对应的唯一标志,默认使用注解标记的方法参数值,也可以使用SpEL表达式。缓存数据的本质是Map类型的数据,key用于指定唯一的标识,value用于指定缓存的数据。
如果缓存数据时,没有指定key属性,Spring Boot默认提供的配置类SimpleKeyGenerator会通过generateKey(Object…params)方法参数生成key值。默认情况下,如果generateKey()方法用一个参数,参数值就是key属性的值;如果generateKey()方法没有参数,那么key属性是一个空参的SimpleKey[]对象,如果由多个参数,那么key属性是一个带参的SimpleKey[Params1,[Params2]]对象。
Cache缓存支持的SpEL表达式及说明
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
methodName | root对象 | 当前被调用的方法名 | #root.methodName |
method | root对象 | 当前被调用的方法 | #root.method.name |
target | root对象 | 当前被调用的目标对象实例 | #root.target |
args | root对象 | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root对象 | 当前被调用的方法的缓存列表 | #root.caches[0].name |
ArgumentName | 执行上下文 | 当前被调用的方法参数,可以用#参数名或者#a0、#p0的形式标识(0表示参数索引,从0开始) | #comment_id,#a0,#p0 |
result | 执行上下文 | 当前方法执行后的返回结果 | #result |
3)keyGenerator属性
keyGenerator属性与key属性本质作用相同,都是用于指定缓存数据的key,只不过keyGenerator属性指定的不是具体的key值,而是key值的生成器规则,由其中指定的生成器生成具体的key。使用时,keyGenertor属性与key属性要二者选一。
4)cacheManager/cacheResolver属性
cacheManager和cacheResolver属性分别用于指定缓存管理器和缓存解析器,这两个属性也是二选一使用,默认情况不需要配置,如果存在多个缓存管理器,可以使用两个属性分别制定。
5)condition属性
condition属性用于对数据进行有条件的选择性存储,只有当指定条件为true时才会对查询结果进行缓存,可以使用SpEL表达式制定属性值。例如@Cacheable(cacheNames=“comment”,condition="#comment_id>10")表示方法参数comment_id的值大于10才会对结果进行缓存。
6)unless属性
unless属性的作用与condition属性相反,当制定的条件为true时,方法的返回值不会被缓存。unless属性可以使用SpEL表达式指定。例如@Cacheable(cacheNames=“comment”,unless="#result==null")表示只有查询结果不为空才会对结果数据进行缓存存储。
7)sync属性
sync属性表示数据缓存过程中是否使用异步模式,默认值false
CachePut注解
@CachePut注解是由Spring框架提供的,可以作用于类或方法(通常用在数据更新方法上),该注解的作用是更新缓存数据。@CachePut注解的执行顺序是,先进行方法调用,然后将方法结果更新到缓存中。
@CachePut注解也提供了多个属性,这些属性与@Cacheable注解的属性完全相同。
CacheEvict注解
CacheEvict注解是由Spring框架提供的,可以作用于类或方法(通常用数据删除方法上),该注解的作用是删除缓存数据。@CacheEvict注解的默认执行顺序是,先进行方法调用,然后清除缓存。
@CacheEvict注解提供了多个属性,这些属性于@Cacheable注解的属性基本相同。除此之外,@CacheEvict额外提供了两个特殊属性allEntrier和beforeInvacation
1)allEntries属性
表示是否清除指定缓存空间中的所有缓存数据,默认值为false(即默认只删除指定key对应的缓存数据)。例如@CacheEvict(cacheNames=“comment”,allEntrier=true)表示方法执行后会删除缓存空间comment中所有的数据。
2)beforInvocation属性
表示是否在方法执行之前进行缓存清除,默认值为false(即默认在执行方法后再进行缓存清除)。例如@CacheEvict(cacheNames=“comment”,beforInvacation=true)表示会在方法执行之前进行缓存清除。
需要注意的是,如果将@CacheEvict注解的beforeInvocation属性设置为true,会存在一定弊端。例如在进行数据删除的方法中发生了异常,这会导致实际数据并没有被删除,但是缓存数据却被提前清除了。
Caching注解
如果处理复杂规则的数据缓存可以使用@Caching注解,该注解作用于类或者方法。@Caching注解包含cacheable、put和evict三个属性,它们的作用等同于@Cacheable、@CachePut和@CacheEvict。
@Caching(cacheable={@Cacheable(cacheNames="comment",key="#id"}),put={@CachePut(cacheNames="comment",key="#result.author")})
public Comment getComment(int comment_id){
return commentRepository.findById(comment_id).get();
}
上述代码中,根据id执行查询操作,并将查询到的Comment对象进行缓存管理。从代码中可以看出,@Caching注解作用域getComment()方法上,并在@Caching注解中使用了cacheable和put两个属性,并且cacheable和put两个属性嵌套引入@Cacheable和@CachePut两个注解,在两个注解中分别使用#id和@result.author缓存key的值。
CacheConfig注解
@CacheConfig注解作用于类,注意用于统筹管理类中所有使用@Cacheable、@CachePut和@CacheEvict注解的方法中的公共属性,这些公共属性包括cacheNames、keyGenerator、cacheManager和cacheResolver
@CacheConfig(cacheNames="comment")
@Service
public class CommentService{
@Autowired
private CommentRepository commentRepository;
@Cacheable
public Comment findById(int comment_id){
Comment comment=commentRepository.findById(comment_id).get();
return comment;
}
}
上述代码中,CommentService类上标注了@CacheConfig注解,同时使用cacheName属性将缓存空间同一设置为comment,这样在该类中所有方法上使用缓存注解时可以省略cacheName属性。
注:如果在类上使用了@CacheConfig注解定义了某个属性,同时又在该类方法中使用缓存注解定义了相同的属性,那么该属性值会使用就近验证,以方法上注解为准。
Spring Boot整合Redis缓存实现
Spring Boot支持的缓存组件
在Spring Boot中,数据的管理存储依赖于Spring框架中cache相关的org.springframework.cache.Cache
和org.springframework.cache.CacheManager
缓存管理器接口。如果程序中没有定义类型为cacheManager的Bean组件或者是名为cacheResolver的cacheResolver缓存解析器,Spring Boot将尝试选择并启用以下缓存组件。
- Generic
- JCache(JSR-107)
- EhCache 2.x
- Hazelcast
- Infinispan
- Couchbase
- Redis
- Caffeine
- Simple
上面我们按照Spring Boot缓存组件的加载顺序列举了支持的9种缓存组件,在项目中添加某个缓存管理组件后,Spring Boot项目会选择并启用对应的缓存管理器。如果项目中同时添加了多个缓存组件,且没有指定缓存管理器或者缓存解析器,那么Spring Boot会优先启动指定的缓存组件并进行缓存管理。
之前我们讲解Spring Boot默认缓存管理中,没有添加任何缓存管理组件却能实现缓存管理。这是因为开启缓存管理后,Spring Boot会按照上述列表顺序查找有效的缓存组件进行缓存管理,如果没有任何缓存组件,会默认使用最后一个Simple缓存组件进行管理。Simple缓存组件是Spring Boot默认的缓存管理组件,它默认使用内存中的ConcurrentHashMap进行缓存存储,所以在没有任何第三方缓存组件的情况下也可以实现内存中的缓存管理。
基于注解的Redis缓存实现
1)添加Spring Data Redis依赖启动器。在chapter06项目的pom.xml文件中添加Spring Data Redis依赖启动器。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2)Redis服务连接配置。使用类似Redis的第三方缓存组件进行缓存管理时,缓存数据并不是想Spring Boot默认缓存管理那样存储在内存中,而是需要预先搭建类似Redis服务的数据仓库进行缓存存储。所以首先需要安装并开启Redis服务。配置全局配置文件。
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
3)使用@Cacheable、@CachePut、@CacheEvict注解定制缓存管理。对CommentService类中的方法进行修改,使用@Cacheable、@CachePut、@CacheEvict3个注解定制缓存管理,分别演示数据的存储、更新、删除。
CommentService.java
package com.example.chapter06.service;
import com.example.chapter06.Repository.CommentRepository;
import com.example.chapter06.domain.Comment;
import net.bytebuddy.build.CachedReturnPlugin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.Optional;
@CacheConfig(cacheNames = "comment")
@Service
public class CommentService {
private final CommentRepository commentRepository;
@Autowired
public CommentService(CommentRepository commentRepository) {
this.commentRepository = commentRepository;
}
@Cacheable(unless = "#result==null")
public Comment findById(int comment_id){
Optional<Comment>optional=commentRepository.findById(comment_id);
return optional.orElse(null);
}
@CachePut(key = "result.id")
public Comment updateComment(Comment comment){
commentRepository.updateComment(comment.getAuthor(),comment.getaId());
return comment;
}
@CacheEvict
public void deleteComment(int comment_id){
commentRepository.deleteById(comment_id);
}
}
4)基于注解的Redis查询缓存测试。通过前面的操作,我们已经在项目中添加了Redis缓存的依赖和Redis服务的连接配置,并且项目中已经使用@EnableCaching开启了基于注解的缓存管理,下面就可以直接启动项目进行缓存测试。
启动chapter06项目,项目启动成功后,通过浏览器"http://localhost:8080/get/1"查询id为1的用户评论信息,会发现浏览器数据响应错误,同时控制台出现异常信息。
查询用户评论信息Comment时执行了相应的SQL语句,但是在进行缓存存储时出现了IllegalArgumentException非法参数异常,提示信息要求对应Comment实体类必须实现序列化。
5)将缓存对象实现序列化。通过前面的异常错误提示发现,在对实体类对象进行缓存存储时必须先实现序列化(一些基本数据类型不需要序列化,因为内部已经默认实现了序列化接口),否则会出现缓存异常,导致程序无法正常执行。下面我们将进行缓存存储的Comment类进行改进,实现JDK自带的序列化接口Serializable。
Comment.java
package com.example.chapter06.domain;
import javax.persistence.*;
import java.io.Serializable;
@Entity(name = "t_comment")
public class Comment implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String content;
private String author;
@Column(name = "a_id")
private Integer aId;
}
执行findById()方法查询出的用户评论信息Comment正确存储到了Redis缓存库中名为comment的命名空间下。其中缓存数据的唯一标识key值是以“名称空间comment::+参数值”(comment::1)的字符串形式体现的,而value值则是以经过JDK默认序列格式化后的HEX格式存储。这种JDK默认序列化后的数据显然不方便缓存数据的可视化查看和管理,所以在实际开发中,通常会自定义数据的序列化格式。
7)基于注解的Redis缓存更新测试。先通过浏览器访问"http://localhost:8080/update/1/shitou",更新id为1的评论作者名为shitou;接着继续访问"http://localhost:8080/get/1",查询id为1的用户评论信息。
8)基于注解的Redis缓存删除测试。
#对基于注解的Redis缓存数据统一设置有效期为1分钟,单位毫秒
spring.cache.redis.time-to-live=60000
基于API的Redis缓存实现
在SpringBoot整合Redis缓存实现中,除了基于注解形式的Redis缓存实现外,还有一种开发中常用的方式—基于API的Redis缓存实现。
1)使用Redis API进行业务数据缓存管理。在chapter06项目的基础上,在com.example.chpater06.service包下编写一个进行业务处理的类ApiCommentService
ApiCommentService.java
package com.example.chapter06.service;
import com.example.chapter06.Repository.CommentRepository;
import com.example.chapter06.domain.Comment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Service
public class ApiCommentService {
private final RedisTemplate<String,Comment> redisTemplate;
private final CommentRepository commentRepository;
@Autowired
public ApiCommentService(RedisTemplate redisTemplate, CommentRepository commentRepository) {
this.redisTemplate = redisTemplate;
this.commentRepository = commentRepository;
}
public Comment findById(int comment_id){
Object object=redisTemplate.opsForValue().get("comment_"+comment_id);
if(object!=null){
return (Comment) object;
}else {
Optional<Comment>optional= commentRepository.findById(comment_id);
if(optional.isPresent()){
Comment comment=optional.get();
redisTemplate.opsForValue().set("comment_"+comment_id,comment,1, TimeUnit.DAYS);
return comment;
}else{
return null;
}
}
}
public Comment updateComment(Comment comment){
commentRepository.updateComment(comment.getAuthor(),comment.getaId());
redisTemplate.opsForValue().set("comment_"+comment.getId(),comment);
return comment;
}
public void deleteComment(int comment_id){
commentRepository.deleteById(comment_id);
redisTemplate.delete("comment_"+comment_id);
}
}
- RedisTemplate是Spring Data Redis提供的直接进行Redis操作的Java API,可以直接注入使用,相对于传统的Jedis更加简便。
- RedisTemplate可以操作
<Object,Object>
对象类型数据,而其子类StringRedisTemplate则是专门针对<String,String>
字符串类型的数据进行操作。 - RedisTemplate类中提供了很多进行数据缓存操作的方法,可以进行数据缓存查询、缓存更新、缓存修改、缓存删除以及设置缓存有效期等。
- redisTemplate.opsForValue().set(“comment_”+comment_id,comment,1,TimeUnit.DAYS)设置缓存数据的同时,将缓存有效期设置为1天时间;当然可以设置缓存有效期,再设置缓存数据。
redisTemplate.opsForValue().set("comment_"+comment_id,comment);
redisTemplate.expire("comment_"+comment_id,90,TimeUnit,SECONDS);
2)编写Web访问层Controller文件。
ApiCommentController.java
package com.example.chapter06.controller;
import com.example.chapter06.domain.Comment;
import com.example.chapter06.service.ApiCommentService;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class ApiCommentController {
private final ApiCommentService apiCommentService;
@Autowired
public ApiCommentController(ApiCommentService apiCommentService) {
this.apiCommentService = apiCommentService;
}
@GetMapping("/get/{id}")
public Comment findById(@PathVariable("id") int comment_id){
return apiCommentService.findById(comment_id);
}
@GetMapping("/delete/{id}")
public void deleteComment(@PathVariable("id") int comment_id){
apiCommentService.deleteComment(comment_id);
}
}
3)基于API的Redis缓存实现的相关配置。基于API的Redis缓存实现不需要@EnableCaching注解开启基于注解的缓存支持,所以这里可以选择将添加在项目启动类上的@EnableCaching进行删除或者注释。
另外,基于API的Redis缓存实现需要在Spring Boot项目的pom.xml文件中引入Redis依赖启动器,并在配置文件中进行Redis服务连接配置,同时为进行数据存储的COmment实体类实现序列化接口,这些配置于注解的Redis缓存实现操作步骤相同,并且已经实现,这里不再重复。
在Spring Boot项目中,完成基于API的Redis缓存配置后,下面就可以进行缓存查询、缓存更新和缓存删除的相关测试了。
相对使用注解的方式,使用Redis API进行缓存管理更加灵活,例如,手机验证码进行验证时,可以在缓存中设置验证等待时间。相比使用注解的方式进行缓存管理,使用Redis API的方式编写的代码量可能会更多。
自定义Redis缓存序列化机制
自定义RedisTemplate
Redis API默认序列化机制
基于Redis API的Redis缓存实现是使用RedisTemplate模板进行数据缓存操作的,这里打开RedisRemplate类,查看源码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
//声明了key,value的各种序列化方式,初始值为空
@Nullable
private RedisSerializer keySerializer = null;
@Nullable
private RedisSerializer valueSerializer = null;
@Nullable
private RedisSerializer hashKeySerializer = null;
@Nullable
private RedisSerializer hashValueSerializer = null;
...
//进行默认序列化方法设置,设置JDK序列化方式
public void afterPropertiesSet() {
super.afterPropertiesSet();
boolean defaultUsed = false;
if (this.defaultSerializer == null) {
this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader());
}
...
从上述RedisTemplate核心源码可以看出,在RedisTemplate内部声明了缓存数据key,value的各种序列化方式,且初始值都为空;在afterPropertiesSet()方法中,如果序列号参数defaultSerializer为null,则数据序列化方式为JdkSerializationRedisSerializer。
1)使用RedisTemplate对Redis数据进行缓存操作时,内部使用的JdkSerializationRedisSerializer序列化方式要求被序列化的实体类继承Serializable接口。
2)使用RedisRemplate时,如果没有特殊的设置,key和value都是使用defaultSerializer=new JdkSerializationRedisSerializer()进行序列化的。
另外,在RedisTemplate类源码中,看到的缓存数据key、value的各种序列化类型都是RedisSerializer。进入RedisSerializer源码查看RedisSerializer支持的序列化方法。
可以看出,RedisSerializer是一个Redis序列化接口,默认有6个实现类,这6个实现类代表了6中不同的数据序列化方式。其中,JdkSerializationRedisSerializer是JDK自带的,也是RedisTemplate内部默认使用的数据序列化方式,我们可以根据需要选择其他支持的序列化方式。
自定义RedisTemplate序列化机制
在项目中引入Redis依赖后,Spring Boot提供的RedisAutoConfiguration自动配置会生效。打开RedisAutoConfiguration类,查看内部源码中关于RedisTemplate的定义方式,核心代码。
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
...
}
从上述RedisAutoConfiguration核心源码中可以看出,在Redis自动配置类中,通过Redis连接工厂RedisConnectionFactory初始化了一个RedisTemplate;RedisTemplate类上方添加了@ConditionalOnMissingBean注解(顾名思义,当某个Bean不存在时生效),用来表明如果开发着自定义了一个名为redisTemplate的Bean,则RedisTemplate会使用自定义的Bean。
如果想要使用自定义序列化方式的RedisTemplate进行数据缓存操作,可以参考上述核心代码创建一个名为redisTemplate的Bean组件,并在该组件中设置对应的序列化方式即可。
接下来我们在chapter06项目中创建名为com.example.chapter06.config的包,在该包下创建一个Redis自定义配置类RedisConfig。
package com.example.chapter06.Config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object,Object>redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<Object,Object>template=new RedisTemplate<Object, Object>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om=new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setDefaultSerializer(jackson2JsonRedisSerializer);
return template;
}
}
使用@Configuration注解将RedisConfig标注为一个配置类,使用@Bean注解注入一个默认名称为redisTemplate的组件。在Bean组件中,使用自定义的Jackson2JsonRedisSerializer数据序列化方式自定义一个RedisTemplate,在定制序列化方式中,定义一个ObjectMapper用于进行数据转换设置。
效果测试
启动chapter06项目,项目启动成功后,通过浏览器"http://localhost:8080/api/get/3"查询id为3的用户评论信息,并重复刷新浏览器查看同一条数据信息。
自定义RedisCacheManager
Spring Boot 1.X版本中,RedisCacheManager是在RedisTemplate的基础上进行构建的,而Spring Boot2.X版本中,RedisCacheManager是单独进行构建的。因此,在Spring Boot2.X版本中,对RedisTemplate进行自定义序列化机制构建后,仍然无法对RedisCacheManager内部默认序列化机制进行覆盖(这就解释了基于注解的Redis缓存实现仍然会使用JDK默认序列化机制的原因),想要基于注解的Redis缓存实现也使用自定义序列化机制,需要自定义RedisCacheManager。
自定义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 configuration=RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofDays(1)).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(strSerializer)).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jacksonSeial)).disableCachingNullValues();
RedisCacheManager cacheManager=RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(configuration).build();
return cacheManager;
}
上述代码中,在RedisConfig配置类中使用@Bean注解注入了一个默认名称为方法名的cacheManager组件。在定义的Bean组件中,通过RedisCacheConfiguration对缓存数据的key和value分别进行了序列化方式的定制,其中缓存数据的key定制为StringRedisSerializer(即String格式),而value定制为了Jackson2JsonRedisSerializer(即JSON格式),同时还使用entryTtl(Duration.ofDays(1))方法将缓存数据有效期设置为1天。