默认缓存管理

Spring框架支持透明地向应用程序添加缓存对缓存进行管理,其管理缓存的核心是将缓存应用于操作数据的方法,从而减少操作数据的执行次数,同时不会对程序本身造成任何干扰。 Spring Boot继承了Spring框架的缓存管理功能,通过使用@EnableCaching注解开启基于注解的缓存支持,Spring Boot就可以启动缓存管理的自动化配置

1 基础环境搭建

1.准备数据

使用创建的springbootdata的数据库,该数据库有两个表t_article和t_comment

2.创建项目,功能编写

创建一个Controller,创建一个Service,创建一个dao。

1、创建项目并选择依赖

在Dependencies依赖选择项中添加SQL模块中的JPA依赖、MySQL依赖、Web模块中的Web依赖

image.png
image.png
image.png
image.png
然后Finish,创建项目

2、编写实体类

数据库t_comment表对应的实体类Comment,并使用JPA相关注解配置映射关系 Controller 层。

  1. @Entity(name = "t_comment") // 设置实体类,并指定映射的表名
  2. public class Comment {
  3. @Id // 表明映射对应的主键id
  4. @GeneratedValue(strategy = GenerationType.IDENTITY)// 设置主键自增策略
  5. private Integer id;
  6. private String content;
  7. private String author;
  8. @Column(name = "a_id") //指定映射的表字段名
  9. private Integer aId;
  10. // get/set/toString
  11. }

3、编写数据库操作的Repository接口文件Dao层

public interface CommentRepository extends JpaRepository<Comment,Integer> {
    //根据评论id修改评论作者author
    @Transactional //把事物放在这里了
    @Modifying // 删、改操作都是需要添加这个注解
    @Query("update t_comment c set c.author = ?1 where c.id=?2")
    public int updateComment(String author,Integer id);

}

4、编写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;

    @RequestMapping("/findCommentById")
    public Comment findCommentById(Integer id){

        Comment commentById = commentService.findCommentById(id);
        return  commentById;
    }
}

6、编写配置文件

在项目全局配置文件application.properties中编写对应的数据库连接配置

# MySQL数据库连接配置
spring.datasource.url=jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=000000
#显示使用JPA进行数据库查询的SQL语句
spring.jpa.show-sql=true
#开启驼峰命名匹配映射
mybatis.configuration.map-underscore-to-camel-case=true
#解决乱码
server.servlet.encoding.force-response=true

7、启动然后测试。

返回成功。
image.png

控制台发起一条sql查询请求
image.png
发现多请求几次就多几条查询(他们是到数据库去查询的)
image.png

因为没有在Spring Boot项目中开启缓存管理。在没有缓存管理的情况下,虽然数据表中 的数据没有发生变化,但是每执行一次查询操作(本质是执行同样的SQL语句),都会访问一次数据库 并执行一次SQL语句

3、默认缓存体验

在前面搭建的Web应用基础上

1、使用@EnableCaching注解开启基于注解的缓存支持

image.png

2、使用@Cacheable注解对数据操作方法进行缓存管理

将@Cacheable注解标注在Service类的查 询方法上,对查询结果进行缓存

是将查询结果Comment存放在Spring Boot默认缓存中名称为comment 的名称空间(namespace)中,对应缓存唯一标识 (即缓存数据对应的主键k)默认为方法参数comment_id的值
image.png

3、测试访问

在配置了Spring Boot默认注解后,重复进行同样的查询操作,发现,数据库只执行了一次SQL查询语句说明项目开启的默认 缓存支持已经生效

底层结构:

在诸多的缓存自动配置类中, SpringBoot默认装配的是 SimpleCacheConfiguration , 他使用的 CacheManager 是 ConcurrentMapCacheManager, 使用 ConcurrentMap 当底层的数据结构,按照Cache的名字查询出Cache, 每一个Cache中存在多个k-v键值对,缓存值

1.@EnableCaching注解

@EnableCaching是由spring框架提供的,springboot框架对该注解进行了继承,该注解需要配置在类 上(在中,通常配置在项目启动类上),用于开启基于注解的缓存支持

2.@Cacheable注解

@Cacheable注解也是由spring框架提供的,可以作用于类或方法(通常用在数据查询方法上),用于 对方法结果进行缓存存储。注解的执行顺序是,先进行缓存查询,如果为空则进行方法查询,并将结果 进行缓存;如果缓存中有数据,不进行方法查询,而是直接使用缓存数据

3.@CachePut注解

是在目标方法执行完之后生效, @CachePut被使用于修改操作比较多,哪怕缓存中已经存在目标值了,但是这个 注解保证这个方法依然会执行,执行之后的结果被保存在缓存中

4.@CacheEvict注解

@CacheEvict注解是由Spring框架提供的,可以作用于类或方法(通常用在数据删除方法上),该注解 的作用是删除缓存数据。@CacheEvict注解的默认执行顺序是,先进行方法调用,然后将缓存进行清 除。

整合Redis缓存实现

使用基于注解的方式进行缓存管理

在Spring Boot默认缓存管理的基础上引入Redis缓存组件

1、在pom.xml文件中添加Spring Data Redis依赖启动器

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

添加了redis相关的启动器之后, SpringBoot会使用 RedisCacheConfigratioin 当做生效的自动 配置类进行缓存相关的自动装配,容器中使用的缓存管理器是 RedisCacheManager , 这个缓存管理器创建的Cache为 RedisCache , 进而操控redis进行数据的缓存

2、Redis服务连接配置 、启动

# Redis服务地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认
spring.redis.password=

image.png

3、对CommentService类中的方法进行修改

使用@Cacheable、@CachePut、@CacheEvict三个注解定制缓存管理,分别进行缓存存储、缓存更新和缓存删除

@Service
public class CommentService {

    @Autowired
    private CommentRepository commentRepository;

    //cacheNames = "comment"缓存的空间就是命名空间,缓存的key就是参数id
    //unless ="#return==null"如果返回的结果恒等于null,则不进行缓存
    @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进行缓存,再次把更新后的放到缓存中,那么就需要保持key值要一致
    @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(int comment_id) {
        commentRepository.deleteById(comment_id);
    }
}

查询缓存@Cacheable注解中没有标记key值,将会使用默认参数值comment_id作为key进行 数据保存,
在进行缓存更新时必须使用同样的key;
同时在查询缓存@Cacheable注解中,定义了 “unless = “#result==null””表示查询结果为空不进行缓存。

4、编写Controller

@RestController
public class CommentController {

    @Autowired
    private CommentService commentService;

    @RequestMapping("/findCommentById")
    public Comment findCommentById(Integer id){

        Comment commentById = commentService.findCommentById(id);
        return  commentById;
    }

    @RequestMapping("/updateComment")
    public Comment updateComment(Comment comment){
        Comment commentById = commentService.findCommentById(comment.getId());
        commentById.setAuthor(comment.getAuthor());
        Comment comment1 = commentService.updateComment(commentById);
        return comment1;
    }

    @RequestMapping("/deleteComment")
    public void deleteComment(Integer id){
        commentService.deleteComment(id);
    }
}

5、启动服务进行测试

image.png
image.png
原因是:实体类没有实现序列化。所以要给实体类实现序列化

image.png

6、启动测试

多次请求,在控制台都只有一条查询记录。说明是在缓存中取出的。
image.png
image.png

7、更新操作

image.png

基于API的Redis缓存实现

开发中常用的方式——基于API的Redis缓存实现。这种基于API的Redis缓存实现,需要在某种业务需求下通过 Redis提供的API调用相关方法实现数据缓存管理;同时,这种方法还可以手动管理缓存的有效期。

1、使用Redis API进行业务数据缓存管理。

在com.slin.service包下编写一个进行业务处理的 ApiCommentService

@Service
public class ApiCommentService {
    @Autowired
    private CommentRepository commentRepository;
    // 通过redisTemplate来调用redis提供的一些API方法
    @Autowired
    private RedisTemplate redisTemplate;

    // 使用API方式进行缓存,先去缓存中查找,缓存中有直接返回,如果没有就去数据库查
    public Comment findCommentById(Integer id){
        // 根据一个key去缓存中查询
        Object o = redisTemplate.opsForValue().get("comment_" + id);
        if(o!=null){
            // 查询到数据,直接返回
            return (Comment) o;
        }else {
            //缓存中没有,从数据库查询
            Optional<Comment> byId = commentRepository.findById(id);
            if(byId.isPresent()){
                Comment comment = byId.get();
                //将查询结果存入到缓存中,并设置有效期为1天
                redisTemplate.opsForValue().set("comment_"+id,comment,1, TimeUnit.DAYS);
                return comment;
            }else {
                return null;
            }
        }
    }

    public Comment updateComment(Comment comment) {
        commentRepository.updateComment(comment.getAuthor(), comment.getId());
        //更新数据后进行缓存更新(传递过来的comment作为缓存的值)
        redisTemplate.opsForValue().set("comment_"+comment.getId(),comment);
        return comment;
    }

    public void deleteComment(int comment_id) {
        commentRepository.deleteById(comment_id);
        redisTemplate.delete("comment_"+comment_id);
    }
}

2、编写Web访问层Controller文件

@RestController
@RequestMapping("/api")
public class ApiCommentController {

    @Autowired
    private ApiCommentService commentService;

    @RequestMapping("/findCommentById")
    public Comment findCommentById(Integer id){
        Comment commentById = commentService.findCommentById(id);
        return  commentById;
    }

    @RequestMapping("/updateComment")
    public Comment updateComment(Comment comment){
        Comment commentById = commentService.findCommentById(comment.getId());
        commentById.setAuthor(comment.getAuthor());
        Comment comment1 = commentService.updateComment(commentById);
        return comment1;
    }

    @RequestMapping("/deleteComment")
    public void deleteComment(Integer id){
        commentService.deleteComment(id);
    }
}

基于API的Redis缓存实现不需要@EnableCaching注解开启 基于注解的缓存支持,所以这里可以选择将添加在项目启动类上的@EnableCaching进行删除或者 注释
image.png

3、启动测试

多次请求,控制台只有一条sql语句,而且redis可视化工具有存数据。说明apiredis的缓存成功。
image.png
更新后,数据库也可以看到更新后的变化。

自定义Redis缓存序列化机制

要想自定义序列化机制,那么就要先了解默认的序列化机制。

1.Redis API默认序列化机制

基于API的Redis缓存实现是使用RedisTemplate模板进行数据缓存操作的,这里打开 RedisTemplate类,查看该类的源码信息

image.png
image.png

在RedisTemplate内部声明了缓存数据key、value的各 种序列化方式,且初始值都为空;在afterPropertiesSet()方法中,判断如果默认序列化参数 defaultSerializer为空,将数据的默认序列化方式设置为JdkSerializationRedisSerializer

(1)使用RedisTemplate进行Redis数据缓存操作时,内部默认使用的是 JdkSerializationRedisSerializer序列化方式,所以进行数据缓存的实体类必须实现JDK自带的序列化接 口(例如Serializable);
(2)使用RedisTemplate进行Redis数据缓存操作时,如果自定义了缓存序列化方式 defaultSerializer,那么将使用自定义的序列化方式。

image.png
RedisSerializer是一个Redis序列化接口,默认有6个实现类,这6个实现类代表了6种 不同的数据序列化方式。其中,JdkSerializationRedisSerializer是JDK自带的,也是RedisTemplate内 部默认使用的数据序列化方式,开发者可以根据需要选择其他支持的序列化方式(例如JSON方式)

2、自定义RedisTemplate序列化机制

在项目中引入Redis依赖后,Spring Boot提供的RedisAutoConfiguration自动配置会生效。打开 RedisAutoConfiguration类,查看内部源码中关于RedisTemplate的定义方式

image.png

如果要使用自定义序列化方式的RedisTemplate进行数据缓存操作,可以参考上述核心代码创建 一个名为redisTemplate的Bean组件,并在该组件中设置对应的序列化方式即可。

实现自定义配置

(将默认的jdk序列化方法,手动改为自定义的json序列化方式)
在项目中创建名为com.slin.config的包,在该包下创建一个Redis自定义配置类 RedisConfig,并按照上述思路自定义名为redisTemplate的Bean组件


@Configuration // 告诉Spring容器这个是配置类
public class RedisConfig {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);

        // 可以用到RedisSerializer 的6中序列化方式中的1中。
        // 创建JSON格式序列化对象,对缓存数据的key和value进行转换
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        // 解决查询缓存转换异常的问题(对象和json直接的转换)
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        //设置redisTemplate模板API的序列化方式为json(将默认是DefaultSerializer进行修改)
        template.setDefaultSerializer(jackson2JsonRedisSerializer);

        return template;
    }
}

启动服务,测试

image.png
多次请求,控制台只有一条sql。
image.png

3 自定义RedisCacheManager

刚刚针对基于 API方式的RedisTemplate进行了自定义序列化方式的改进,从而实现了JSON序列化方式缓存数据。
但是这种自定义的RedisTemplate对于基于注解的Redis缓存来说,是没有作用的
接下来,针对基于注解的Redis缓存机制和自定义序列化方式进行讲解。

Redis注解默认序列化机制

打开Spring Boot整合Redis组件提供的缓存自动配置类 RedisCacheConfiguration(org.springframework.boot.autoconfigure.cache包下的),查看该类的源码信息,其核心代码如下
image.png
从上述核心源码中可以看出,同RedisTemplate核心源码类似,RedisCacheConfiguration内部同样通 过Redis连接工厂RedisConnectionFactory定义了一个缓存管理器RedisCacheManager;同时定制 RedisCacheManager时,也默认使用了JdkSerializationRedisSerializer序列化方式。

如果想要使用自定义序列化方式的RedisCacheManager进行数据缓存操作,可以参考上述核心代 码创建一个名为cacheManager的Bean组件,并在该组件中设置对应的序列化方式即可

注意,在Spring Boot 2.X版本中,RedisCacheManager是单独进行构建的。因此,在Spring Boot 2.X版本中,对RedisTemplate进行自定义序列化机制构建后,仍然无法对 RedisCacheManager内部默认序列化机制进行覆盖(这也就解释了基 于注解的Redis缓存实现仍 然会使用JDK默认序列化机制的原因),想要基于注解的Redis缓存实现也使用自定义序列化机 制,需要自定义RedisCacheManager


自定义RedisCacheManager

在项目的Redis配置类RedisConfig中,按照上一步分析的定制方法自定义名为cacheManager的 Bean组件

  // Controller请求service时候,处理完数据调用缓存注解,缓存注解以json的方式存到缓存中。
    @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)) // 有效期为1天
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(strSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(jacksonSeial)) // 对缓存的value以json的形式缓存
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager
                .builder(redisConnectionFactory).cacheDefaults(config).build();
        return cacheManager;
    }

完成基于注解的Redis缓存管理器RedisCacheManager定制后,可以对该缓存管理器的效果进行 测试(使用自定义序列化机制的RedisCacheManager测试时,实体类可以不用实现序列化接口)

image.png

实现json存储。
image.png