使用 Spring Cache + Redis 作为缓存并支持自定义单个key设置过期时长

2022-08-12 13:25:23

Spring Cache

Spring针对不同的缓存技术,需要实现不同的cacheManager,
Spring定义了如下的cacheManger实现,具体使用哪种需要你自己实现。
在这里插入图片描述

pom

springboot 项目加入如下依赖


   <dependency>
      <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-data-redis</artifactId>
   </dependency>
  <!-- 使用spring cache -->
   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-cache</artifactId>
   </dependency>

配置redis的地址和端口号

redis:
        host: 127.0.0.1
        password :
        port: 6379
        timeout: 60s

配置缓存为redis

需要实现CacheManager用来具体实现缓存管理器

@Configuration@EnableCachingpublicclassRedisCacheConfig{privateint defaultExpireTime=36000;//毫秒privateint userCacheExpireTime=10000;private String userCacheName="cache";/**
     * 缓存管理器
     *
     * @param lettuceConnectionFactory
     * @return
     */@Beanpublic CacheManagercacheManager(RedisConnectionFactory lettuceConnectionFactory){
        RedisCacheConfiguration defaultCacheConfig= RedisCacheConfiguration.defaultCacheConfig();// 设置缓存管理器管理的缓存的默认过期时间
        defaultCacheConfig= defaultCacheConfig.entryTtl(Duration.ofSeconds(defaultExpireTime))// 设置 key为string序列化.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(newStringRedisSerializer()))// 设置value为json序列化.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(newGenericJackson2JsonRedisSerializer()))// 不缓存空值.disableCachingNullValues();

        Set<String> cacheNames=newHashSet<>();
        cacheNames.add(userCacheName);// 对每个缓存空间应用不同的配置
        Map<String, RedisCacheConfiguration> configMap=newHashMap<>();
        configMap.put(userCacheName, defaultCacheConfig.entryTtl(Duration.ofSeconds(userCacheExpireTime)));

        RedisCacheManager cacheManager= RedisCacheManager.builder(lettuceConnectionFactory).cacheDefaults(defaultCacheConfig).initialCacheNames(cacheNames).withInitialCacheConfigurations(configMap).build();return cacheManager;}}

业务类启用注解

@Service
@CacheConfig(cacheNames="user")// cacheName 是一定要指定的属性,可以通过 @CacheConfig 声明该类的通用配置
public class UserServiceImpl {

    /**
     * 将结果缓存,当参数相同时,不会执行方法,从缓存中取
     *
     * @param id
     * @return
     */
    @Cacheable(key = "#id")
    public User findUserById(Integer id) {
        System.out.println("===> findUserById(id), id = " + id);
        return new User(id, "taven");
    }

    /**
     * 将结果缓存,并且该方法不管缓存是否存在,每次都会执行
     *
     * @param user
     * @return
     */
    @CachePut(key = "#user.id")
    public User update(User user) {
        System.out.println("===> update(user), user = " + user);
        return user;
    }

    /**
     * 移除缓存,根据指定key
     *
     * @param user
     */
    @CacheEvict(key = "#user.id")
    public void deleteById(User user) {
        System.out.println("===> deleteById(), user = " + user);
    }

    /**
     * 移除当前 cacheName下所有缓存
     *
     */
    @CacheEvict(allEntries = true)
    public void deleteAll() {
        System.out.println("===> deleteAll()");
    }

}

运行即可看到,缓存数据到redis中

Spring Cache 注解

注解作用
@Cacheable将方法的结果缓存起来,下一次方法执行参数相同时,将不执行方法,返回缓存中的结果
@CacheEvict移除指定缓存
@CachePut标记该注解的方法总会执行,根据注解的配置将结果缓存
@Caching可以指定相同类型的多个缓存注解,例如根据不同的条件
@CacheConfig类级别注解,可以设置一些共通的配置,@CacheConfig(cacheNames=“user”), 代表该类下的方法均使用这个cacheNames

Cacheable
@Cacheable使用两个或多个参数作为缓存的key
常见的如分页查询:使用单引号指定分割符,最终会拼接为一个字符串

@Cacheable(key="#page+':'+#pageSize")public List<User>findAllUsers(int page,int pageSize){int pageStart=(page-1)*pageSize;return userMapper.findAllUsers(pageStart,pageSize);}

findAllUsers会组成的key为1:10

当然还可以使用单引号自定义字符串作为缓存的key值

@Cacheable(key="'countUsers'")publicintcountUsers(){return userMapper.countUsers();}

在这里插入图片描述

CacheEvict
可以移除指定key
声明 allEntries=true移除该CacheName下所有缓存
声明beforeInvocation=true 在方法执行之前清除缓存,无论方法执行是否成功

//清除所有books下的实体@CacheEvict(cacheNames="books", allEntries=true)publicvoidloadBooks(InputStream batch)

Caching
可以让你在一个方法上嵌套多个相同的Cache 注解(@Cacheable, @CachePut, @CacheEvict),分别指定不同的条件

//清除primary,secondary:deposit的缓存@Caching(evict={@CacheEvict("primary"),@CacheEvict(cacheNames="secondary", key="#p0")})public BookimportBooks(String deposit, Date date)

默认 cache key

缓存的本质还是以 key-value 的形式存储的,默认情况下我们不指定key的时候 ,使用 SimpleKeyGenerator 作为key的生成策略

  • 如果没有给出参数,则返回SimpleKey.EMPTY。
  • 如果只给出一个Param,则返回该实例。
  • 如果给出了更多的Param,则返回包含所有参数的SimpleKey。
    注意:当使用默认策略时,我们的参数需要有 有效的hashCode()和equals()方法

实现原理

使用注解切入方法创建代理拦截器,实现调用方法之前之后执行关于redis相关的操作
@EnableCaching 注释触发后置处理器, 检查每一个Spring bean 的 public 方法是否存在缓存注解。如果找到这样的一个注释, 自动创建一个代理拦截方法调用和处理相应的缓存行为。

同步缓存-同步锁缓存

在多线程环境中,可能会出现相同的参数的请求并发调用方法的操作,默认情况下,spring cache 不会锁定任何东西,相同的值可能会被计算几次,这就违背了缓存的目的

对于这些特殊情况,可以使用sync属性。此时只有一个线程在处于计算,而其他线程则被阻塞,直到在缓存中更新条目为止。

@Cacheable(cacheNames="foos", sync=true)public FooexecuteExpensiveOperation(String id){...}

条件缓存

  • condition: 什么情况缓存,condition = true 时缓存,反之不缓存
  • unless: 什么情况不缓存,unless = true 时不缓存,反之缓存
//name长度<32时缓存@Cacheable(cacheNames="book", condition="#name.length() < 32")public BookfindBook(String name)//name长度<32时缓存 否则result不为空则缓存result为空则hardback@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")public Optional<Book>findBook(String name)

高级点的东西

以上只是基本应用,我们可以自己定义序列化方式,和缓存key的前缀。多级缓存的实现,如果redis挂掉如何不影响业务正常运行?

CachingConfigurerSupport提供了如下几个方法,它可以让我们通过实现该方法实现更多选择

publicclassCachingConfigurerSupportimplementsCachingConfigurer{publicCachingConfigurerSupport(){}@Nullablepublic CacheManagercacheManager(){//使用那种形式的缓存-redis还是Ecachereturn null;}@Nullablepublic CacheResolvercacheResolver(){//如何选择缓存器-多级缓存return null;}@Nullablepublic KeyGeneratorkeyGenerator(){//key的生成方案return null;}@Nullablepublic CacheErrorHandlererrorHandler(){//异常处理return null;}}

自定义StringSerializer和自定义缓存key前缀

实现自定义的序列化方式只需要实现redis序列化RedisSerializer接口即可,这里有两个关键方法,一个是序列化,一个是反序列化。同时在这个方法中也可以加入我们自定义的规则,比如统一把缓存放到一个全局的key前缀下面,这样就比较集中的方式实现缓存,避免散乱。

@ComponentpublicclassMyStringSerializerimplementsRedisSerializer<String>{privatefinal Logger logger= LoggerFactory.getLogger(this.getClass());@Autowiredprivate RedisProperties redisProperties;privatefinal Charset charset;publicMyStringSerializer(){this( Charset.forName("UTF8"));}publicMyStringSerializer(Charset charset){
        Assert.notNull( charset,"Charset must not be null!");this.charset= charset;}@Overridepublic Stringdeserialize(byte[] bytes){
        String keyPrefix= redisProperties.getKeyPrefix();
        String saveKey=newString( bytes, charset);int indexOf= saveKey.indexOf( keyPrefix);if(indexOf>0){
            logger.info("key缺少前缀");}else{
            saveKey= saveKey.substring( indexOf);}
        logger.info("saveKey:{}",saveKey);return(saveKey.getBytes()== null? null: saveKey);}@Overridepublicbyte[]serialize(String string){
        String keyPrefix= redisProperties.getKeyPrefix();
        String key= keyPrefix+ string;
        logger.info("key:{},getBytes:{}",key, key.getBytes( charset));return(key== null? null: key.getBytes( charset));}}

上面的序列化中加入了自定义key前缀

在上面的RedisCacheConfig里我们可以修改为自定义的序列化方式

@Autowired
MyStringSerializer  myStringSerializer;// 配置序列化(解决乱码的问题)
        RedisCacheConfiguration config= RedisCacheConfiguration.defaultCacheConfig()// 7 天缓存过期.entryTtl(Duration.ofSeconds(cacheConfigProperties.getTtlTime())).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(myStringSerializer))//自定义的key序列化方式.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))//这里我们也可以修改成FastJson等第三方序列化.disableCachingNullValues();

效果如下,这里我的前缀配置的是bamboo,其他和没有配置前缀没有差别,这样所有的注解缓存都会放入该子路径下面,可以做到一键清除所有缓存。
在这里插入图片描述
自定义fastJson请查看下文的参考内容

CacheResolver实现多级缓存

开发者可以通过自定义CacheResolver实现动态选择CacheManager,使用多种缓存机制:优先从堆内存读取缓存,堆内存缓存不存在时再从redis读取缓存,redis缓存不存在时最后从mysql读取数据,并将读取到的数据依次写到redis和堆内存中。

@Overridepublic CacheResolvercacheResolver(){// 通过Guava实现的自定义堆内存缓存管理器
        CacheManager guavaCacheManager=newGuavaCacheManager();
        CacheManager redisCacheManager=redisCacheManager();
        List<CacheManager> list=newArrayList<>();// 优先读取堆内存缓存
        list.add(concurrentMapCacheManager);// 堆内存缓存读取不到该key时再读取redis缓存
        list.add(redisCacheManager);returnnewCustomCacheResolver(list);}

Redis故障或不可用时仍然执行方法服务可用

SimpleCacheErrorHandler直接抛出异常,我们可以重写org.springframework.cache.annotation.CachingConfigurerSupport.errorHandler方法自定义CacheErrorHandler操作缓存异常时异常处理。

重写errorHandler异常处理什么都不做即可,这里只是打印异常信息

@Overridepublic CacheErrorHandlererrorHandler(){
        CacheErrorHandler cacheErrorHandler=newCacheErrorHandler(){@OverridepublicvoidhandleCacheGetError(RuntimeException exception, Cache cache, Object key){RedisErrorException(exception, key);}@OverridepublicvoidhandleCachePutError(RuntimeException exception, Cache cache, Object key, Object value){RedisErrorException(exception, key);}@OverridepublicvoidhandleCacheEvictError(RuntimeException exception, Cache cache, Object key){RedisErrorException(exception, key);}@OverridepublicvoidhandleCacheClearError(RuntimeException exception, Cache cache){RedisErrorException(exception, null);}};return cacheErrorHandler;}protectedvoidRedisErrorException(Exception exception,Object key){
        logger.error("redis异常:key=[{}], exception={}", key, exception.getMessage());}

直接关闭redis服务,然后访问接口,看控制台打印的记录如下,抛出异常后接着调用mysql查询出结果

2020-01-1016:29:29,891- redis key=bamboo:retailRatio::2,getBytes=[98,97,109,98,111,111,58,114,101,116,97,105,108,82,97,116,105,111,58,58,50]2020-01-1016:29:31,895- redis异常:key=[2], exception=Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to localhost:63792020-01-1016:29:31,895-==>  Preparing: select id, retail_role, unratio, ratio from j_retail_ratio where id=?2020-01-1016:29:31,895-==> Parameters:2(Integer)2020-01-1016:29:31,897-<==      Total:1

推荐第三方maven

以上封装觉得麻烦可以直接使用中央仓库中我已经封装好的的版本

1.pom依赖


<dependency>
  <groupId>com.github.bamboo-cn</groupId>
  <artifactId>jt-common-core</artifactId>
  <version>1.0.3</version>
</dependency>

2.Java启动类配置和业务层数据注解配置

//Java启动类配置
@SpringBootApplication(scanBasePackages = {"com.bamboo.common"})

//serviceImp需要使用注解
serviceImp类中的启用spring cache注解,用法同UserServiceImpl

3.yml配置自定义缓存配置-可以不用配置使用默认值

spring:
    cache: #缓存配置
        prefix: bamboo #key前缀
        ttlTime: 2000  #缓存时长单位秒

cache配置默认值

  • prefix: spring-cache
  • ttlTime: 3600 一个小时
  • 使用方式和spring cache注解相同
  • 使用fastjson作为序列化

4.在1.0.3版本中已经支持单个key设置过期时长,从而避免使用默认过期时间

自定义单个key设置超时时间,key加上字符串TTL=1000实现扩展单个KEY过期时间

    //@Cacheable(key = "T(String).valueOf(#code).concat('TTL=10')")
    @Cacheable(key = "#code+'TTL=10'")
    public Double getRetailByCode(Integer code) {
        RetailRatio retailRatio =  retailRatioMapper.selectByPrimaryKey(code);
        return retailRatio.getUnratio();
    }

https://blog.csdn.net/b376924098/article/details/79820642

  • 作者:牧竹子
  • 原文链接:https://blog.csdn.net/zjcjava/article/details/103920388
    更新时间:2022-08-12 13:25:23