SpringBoot缓存管理

1,093 阅读11分钟

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

image20210116225607473.png

(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 ,测试每次访问,都会查询数据库

image20210116233550920.png

默认缓存体验

(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中存储了缓存值

image20210117005743102.png

(6) 基于注解的Redis缓存更新测试。

http://localhost:8080/updateComment?id=1&author=aaabb 执行后,数据库发生变化

image20210117005818029.png

日志打印中只有更新语句,没有查询语句了。

image20210117010004095.png

可以看出,执行updateComment()方法更新id为1的数据时执行了一条更新SQL语句,后续调用 findById()方法查询id为1的用户评论信息时没有执行查询SQL语句,且浏览器正确返回了更新后的结 果,表明@CachePut缓存更新配置成功

(7)基于注解的Redis缓存删除测试

访问:http://localhost:8080/deleteComment?id=1数据被删除,同时Redis缓存也被删除

image20210117010334781.png

此外,还可以设置缓存有效期

# 对基于注解的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进行删除或者注释

image20210117014525032.png

三、自定义缓存序列化机制

自定义RedisTemplate

Redis API默认序列化机制

打开 RedisTemplate类,查看该类的源码信息可以得到两个结论:

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

(2)使用RedisTemplate进行Redis数据缓存操作时,如果自定义了缓存序列化方式 defaultSerializer,那么将使用自定义的序列化方式。

缓存数据key、value的各种序列化类型都是 RedisSerializer而默认提供的实现类有下面六种:

image20210117015138368.png

自定义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,如下:

image20210117021412394.png

自定义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

image20210117022525711.png