springcache使用详解

前言

在实际的开发中,缓存的使用已经是随处可见了,就目前来看,普遍使用的比较多的大概就是redis了吧,但从编码的角度,纯粹使用redis去操作缓存,似乎并不是一个很好的选择

我们不妨来看下面这段代码(细节请暂时忽略)

	@Autowired
    private RedisTemplate<String,DbUser> redisTemplate;

    public DbUser getUserById(String id) {
        DbUser dbUser = redisTemplate.opsForValue().get("user:" + id);
        if(dbUser != null){
            return dbUser;
        }
        dbUser = dbUserMapper.getByUserId(id);
        if(dbUser != null){
            redisTemplate.opsForValue().set("user:"+id,dbUser);
        }
        return dbUser;
    }

上面这段代码展现的是一个常规的使用redis缓存数据的做法,看完后,是不是觉得这样写挺麻烦的,如果程序中需要缓存的数据比较多,这么写不仅给编码带来了较大的工作量,而且实在是不方便对缓存key的管理,一旦需要缓存的数据多了,最后可能自己都整不清哪些key是需要删的

基于上面这个小小的痛点,在实际开发中,涉及到缓存比较多的项目,我们并不推荐直接使用上面这种方式操作缓存,而是引入springcache

SpringCache 特点

  • 通过少量的配置 annotation 注释即可使得既有代码支持缓存
  • 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件即可使用缓存
  • 支持 Spring Express Language,能使用对象的任何属性或者方法来定义缓存的 key 和 condition
  • 支持 AspectJ,并通过其实现任何方法的缓存支持
  • 支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性
  • 支持各种缓存实现,如对象,对象列表,默认基于ConcurrentMap实现的ConcurrentMapCache,同时支持其他缓存实现

综合来说,springCache并不像正常缓存那样存储数据,而是在我们调用一个缓存方法时,会把该方法参数和返回结果作为一个键值对存放在缓存中,等到下次利用同样的参数来调用该方法时将不再调用该方法,而是直接从缓存中获取结果进行返回,从而实现缓存的效果

SpringCache 管理缓存的几个重要注解

1、@Cacheable注解

  • 该注解用于标记缓存,就是对使用注解的位置进行缓存
  • 该注解可以在方法或者类上进行标记,在类上标记时,该类所有方法都支持缓存

@Cacable使用时通常搭配三个属性使用

  • value,用来指定Cache的名称,就是存储于哪个Cache上 ,简单理解是cache的命名空间或者大的前缀
  • key,用于指定生成缓存对应的key,如果没指定,则会使用默认策略生成key,也可以使用springEL编写,默认是方法参数组合
@Cacheable(value="users", key="#user.id")
public User findUser(User user){
    return user;
}

@Cacheable(value="users", key="#root.args[0]")
public User findUser(String id){
    return user;
}

  • condition,用来指定当前缓存的触发条件,可以使用springEL编写,如下代码,则当user.id为偶数时才会触发缓存
@Cacheable(value="users", key="#user.id",condition="#user.id%2==0")
public User findUser(User user){
    return user;
}

  • cacheManager ,用于指定当前方法或类使用缓存时的缓存管理器,通过cacheManager 的配置,可以为不同的方法使用不同的缓存策略,比如有的对象缓存的时间短,有的缓存的长,可以通过自定义配置cacheManager 来实现

2、@CachePut注解

该注解将标记的方法的返回值放入缓存中,使用方法与@Cacheable一样,通常@CachePut放在数据更新的操作上,举例来说,当 getByUserId这样一个方法上使用了以 userId为key的缓存时,如果更新了这条数据,该key对应的数据是不是要同步变更呢?

答案是肯定的,于是,我们就需要在更新数据的地方添加@CachePut注解,当updateByUserId触发之后,getByUserId上面的key对应的缓存对象数据也能同步变更

3、@CacheEvict注解

  • 该注解用于清理缓存数据
  • 使用在类上时,清除该类所有方法的缓存
  • @CacheEvict同样拥有@Cacheable三个属性,同时还有一个allEntries属性,该属性默认为false,当为true时,删除该value所有缓存

@CacheEvict在实际使用中需要重点关注,比如一开始我们给用户组,角色,部门等与用户查询相关的业务上面添加了key的时候,当一个userId对应的这条数据被清理的时候,那么关联的key,即所说的用户组,部门,角色等关联的用户数据都需要一同清理

4、caching

  • 组合多个cache注解一起使用
  • 允许在同一个方法上使用 以上3个注解的组合

补充说明

以上简单介绍了springcache中的几个重要注解,以及各自的作用,通常来讲,在开发过程中,使用springcache也就是在和这些注解打交道,里面有一个点值得注意就是,关于方法级别上 key的使用规范和命名规范问题,这里可以关注和参考下springEL的写法规范

与springboot的整合

下面我们来通过实例演示如何在springboot中整合springcache

1、添加pom依赖

需要说明的是,springcache提供了多种缓存的实现,其中与redis的整合比较符合大家对redis的使用习惯,同时也更进一步了解springcache在redis中存储的结构,因此这里需引入springboot-redis的依赖

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

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

2、配置文件做简单的配置

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://IP:3306/bank1?autoReconnect=true&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
    username: root
    password: 123456

  redis:
    host: 127.0.0.1
    port:  6379
    database: 1
  cache:
    type: redis

重点是 spring.redis.cache.type 这个配置,可以看到,springcache是提供了多种实现方式的,redis只是其中一种,大家结合自己的情况合理选择一种
在这里插入图片描述

3、自定义cacheManager

在上文,提到了cacheManager这个组件,它是作为@Cacheable这个注解中的一个属性搭配使用的,为了方便开发中,根据不同的业务需求对不同类型的key做个性化配置管理,比如有的数据需要缓存分钟级别,有的key需要缓存小时级别,这里我们就可以通过自定义配置cacheManager的方式来达到这个目的

提供一个配置类RedisConfig ,这种类实际开发中只需要一次配置保存即可,具体的细节配置参数可参考 spring官网,重点关注对cacheManager的配置,我们分别设定了分钟级,小时级和天级的3个cacheManager ,以bean的方式注入到spring容器中

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.time.Duration;

@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(mapper);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }

    /**
     * 分钟级别
     * @param connectionFactory
     * @return
     */
    @Bean("cacheManagerMinutes")
    public RedisCacheManager cacheManagerMinutes(RedisConnectionFactory connectionFactory){
        RedisCacheConfiguration configuration = instanceConfig(60L);
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(configuration)
                .transactionAware()
                .build();
    }

    /**
     * 小时级别
     * @param connectionFactory
     * @return
     */
    @Bean("cacheManagerHour")
    @Primary
    public RedisCacheManager cacheManagerHour(RedisConnectionFactory connectionFactory){
        RedisCacheConfiguration configuration = instanceConfig(3600L);
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(configuration)
                .transactionAware()
                .build();
    }

    /**
     * 天级别
     * @param connectionFactory
     * @return
     */
    @Bean("cacheManagerDay")
    public RedisCacheManager cacheManagerDay(RedisConnectionFactory connectionFactory){
        RedisCacheConfiguration configuration = instanceConfig(3600 * 24L);;
        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(configuration)
                .transactionAware()
                .build();
    }

    private RedisCacheConfiguration instanceConfig(long ttl){
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.configure(MapperFeature.USE_ANNOTATIONS,false);
        //只针对非空的值进行序列化
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        //将类型序列化到属性的json字符串
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        return RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(ttl))
                .disableCachingNullValues()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
    }

    /**
     * 自定义key生成策略
     * @return
     */
    @Bean("defaultSpringKeyGenerator")
    public KeyGenerator defaultSpringKeyGenerator(){
        return new KeyGenerator() {
            @Override
            public Object generate(Object o, Method method, Object... objects) {
                String key = o.getClass().getSimpleName() + "_"
                        + method.getName() +"_"
                        + StringUtils.arrayToDelimitedString(objects,"_");

                System.out.println("key :" + key);
                return key;
            }
        };
    }
}

在该配置类中,有一个KeyGenerator的bean需要稍加说明,即我们在给方法上声明key的时候,key的生成规则配置可以使用springEL的方式,也可以使用自定义key的生成规则,这个需要结合实际情况,搭配使用

经验来说,那么缓存的数据和key的结构关系非常紧密的,建议采用springEL的方式,做到精准控制,而那些比较通用的缓存数据,则可以考虑自定义key的规则,减少编码的工作量

以上所有的前置准备工作就完成了,下面通过几个增删改查接口来实际体验下springcache的使用

1、查询单条数据

接口:

	@GetMapping("/getById")
    public DbUser getById(String id){
        return dbUserService.getById(id);
    }

业务实现:

	@Override
    @Cacheable(value = {"dbUser"},key = "#root.args[0]",cacheManager = "cacheManagerMinutes")
    public DbUser getById(String id) {
        System.out.println("查询数据库");
        DbUser dbUser = dbUserMapper.getByUserId(id);
        return dbUser;
    }

关于key的规则使用,springcache提供了丰富的选择,通常情况下,使用的比较多的是以参数作为key,或者以方法名称为key这里我们以方法参数为例做演示
在这里插入图片描述
前置准备:数据库中初始化了一批数据

在这里插入图片描述

下面启动项目,并启动本地redis,测试一下这个方法是否好使,接口访问:http://localhost:8083/getById?id=000ef60318254a768ed14b31514848a5

接口数据返回:
在这里插入图片描述
接口连续刷几次,发现除了第一次之后没有再走数据库查询,说明缓存生效了
在这里插入图片描述
redis中key的存储结构如下:

在这里插入图片描述

如果换成自定义的keyGenerator,对方法上的注解做一下改造即可,然后再次请求接口,这时候发现缓存的key就是我们自定义的格式了
在这里插入图片描述

2、修改数据

修改数据涉及到@CachePut注解的使用,当修改之后,添加该注解,可以使得原来查询数据的接口中的缓存数据同步发生变更

接口:

	@GetMapping("/updateById")
    public DbUser updateById(String id,String name){
        return dbUserService.updateById(id,name);
    }

业务实现:

    @Override
    @CachePut(value = {"dbUser"},key = "#root.args[0]",cacheManager = "cacheManagerMinutes")
    public DbUser updateById(String id,String name) {
        DbUser dbUser = dbUserMapper.getByUserId(id);
        dbUser.setRealname(name);
        dbUserMapper.updateUserName(id,name);
        return dbUser;
    }

启动项目,仍然使用上面这条数据的user_id,我们修改下名称,依次调用修改的接口,和查询的接口,看看数据如何变化,

1、首先调用:http://localhost:8083/getById?id=000ef60318254a768ed14b31514848a5
在这里插入图片描述
在这里插入图片描述
2、调用修改数据接口:http://localhost:8083/updateById?id=000ef60318254a768ed14b31514848a5&name=邓聪国2
在这里插入图片描述
3、再次调用查询接口:http://localhost:8083/getById?id=000ef60318254a768ed14b31514848a5

在这里插入图片描述
redis中的数据已经近实时发生了修改,但是key仍然未发生变化
在这里插入图片描述

3、删除数据

接口:

 	@GetMapping("/deleteById")
    public String deleteById(String id){
        return dbUserService.deleteById(id);
    }

业务实现:

	 @Override
    @CacheEvict(value = {"dbUser"},key = "#root.args[0]",cacheManager = "cacheManagerMinutes")
    public String deleteById(String id) {
        dbUserMapper.deleteByUserId(id);
        return "delete success";
    }

启动项目,按照以下步骤做测试,

1、首先调用:http://localhost:8083/getById?id=000ef60318254a768ed14b31514848a5
在这里插入图片描述
在这里插入图片描述

2、调用删除数据接口:http://localhost:8083/deleteById?id=000ef60318254a768ed14b31514848a5

在这里插入图片描述
redis中的key被同步清理

在这里插入图片描述

通过以上的几个接口演示,我们基本上弄清了springcache的几个注解配合缓存的实际使用,更复杂的场景有兴趣的同学可以在此基础上做进一步的深究,比如最后那个 caching组合注解的使用,这里限于篇幅就不再继续展开了

本篇到此结束,最后感谢观看!

猜你喜欢

转载自blog.csdn.net/zhangcongyi420/article/details/121318021