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

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
@EnableCaching
public class RedisCacheConfig {


    private int defaultExpireTime=36000;//毫秒

    private int userCacheExpireTime=10000;

    private String userCacheName="cache";

    /**
     * 缓存管理器
     *
     * @param lettuceConnectionFactory
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory lettuceConnectionFactory) {
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig();
        // 设置缓存管理器管理的缓存的默认过期时间
        defaultCacheConfig = defaultCacheConfig.entryTtl(Duration.ofSeconds(defaultExpireTime))
                // 设置 key为string序列化
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                // 设置value为json序列化
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                // 不缓存空值
                .disableCachingNullValues();

        Set<String> cacheNames = new HashSet<>();
        cacheNames.add(userCacheName);

        // 对每个缓存空间应用不同的配置
        Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
        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'")
public int countUsers() {
    return userMapper.countUsers();
}

在这里插入图片描述

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

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

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

//清除primary,secondary:deposit的缓存
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(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 Foo executeExpensiveOperation(String id) {...}

条件缓存

  • condition: 什么情况缓存,condition = true 时缓存,反之不缓存
  • unless: 什么情况不缓存,unless = true 时不缓存,反之缓存
//name长度<32时缓存
@Cacheable(cacheNames="book", condition="#name.length() < 32") 
public Book findBook(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提供了如下几个方法,它可以让我们通过实现该方法实现更多选择

public class CachingConfigurerSupport implements CachingConfigurer {
    public CachingConfigurerSupport() {
    }

    @Nullable
    public CacheManager cacheManager() {//使用那种形式的缓存-redis还是Ecache
        return null;
    }

    @Nullable
    public CacheResolver cacheResolver() {//如何选择缓存器-多级缓存
        return null;
    }

    @Nullable
    public KeyGenerator keyGenerator() {//key的生成方案
        return null;
    }

    @Nullable
    public CacheErrorHandler errorHandler() {//异常处理
        return null;
    }
}

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

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

@Component
public class MyStringSerializer implements RedisSerializer<String> {
 
    private final Logger logger = LoggerFactory.getLogger ( this.getClass () );
 
    @Autowired
    private RedisProperties redisProperties;
 
    private final Charset charset;
 
    public MyStringSerializer() {
        this ( Charset.forName ( "UTF8" ) );
    }
 
    public MyStringSerializer(Charset charset) {
        Assert.notNull ( charset, "Charset must not be null!" );
        this.charset = charset;
    }
 
    @Override
    public String deserialize(byte[] bytes) {
        String keyPrefix = redisProperties.getKeyPrefix ();
        String saveKey = new String ( 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);
    }
 
    @Override
    public byte[] 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和堆内存中。

	@Override
    public CacheResolver cacheResolver() {
        // 通过Guava实现的自定义堆内存缓存管理器
        CacheManager guavaCacheManager = new GuavaCacheManager();
        CacheManager redisCacheManager = redisCacheManager();
        List<CacheManager> list = new ArrayList<>();
        // 优先读取堆内存缓存
        list.add(concurrentMapCacheManager);
        // 堆内存缓存读取不到该key时再读取redis缓存
        list.add(redisCacheManager);
        return new CustomCacheResolver(list);
    }

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

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

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

@Override
    public CacheErrorHandler errorHandler() {
        CacheErrorHandler cacheErrorHandler = new CacheErrorHandler() {

            @Override
            public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
                RedisErrorException(exception, key);
            }

            @Override
            public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
                RedisErrorException(exception, key);
            }

            @Override
            public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
                RedisErrorException(exception, key);
            }

            @Override
            public void handleCacheClearError(RuntimeException exception, Cache cache) {
                RedisErrorException(exception, null);
            }
        };
        return cacheErrorHandler;
    }

    protected void RedisErrorException(Exception exception,Object key){
        logger.error("redis异常:key=[{}], exception={}", key, exception.getMessage());
    }

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

2020-01-10 16: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-10 16:29:31,895 - redis异常:key=[2], exception=Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to localhost:6379
2020-01-10 16:29:31,895 - ==>  Preparing: select id, retail_role, unratio, ratio from j_retail_ratio where id = ? 
2020-01-10 16:29:31,895 - ==> Parameters: 2(Integer)
2020-01-10 16: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();
    }

参考资料

redis缓存实现
https://www.jianshu.com/p/931484bb3fdc

自定义序列化和key前缀实现方式
https://www.jianshu.com/p/713069fbd889
https://blog.csdn.net/u012129558/article/details/80520693
自定义fastJson作为redis序列化方式
https://blog.csdn.net/b376924098/article/details/79820642

<think>好的,我现在要回答用户关于Spring Cachekey生成规则及自定义配置方法的问题。首先,我需要回顾一下Spring Cache的基本概念和相关注解,尤其是@Cacheable、@CachePut和@CacheEvict这些常用注解。根据用户提供的引用内容,我需要整合这些信息,确保回答准确。 用户提到他们想了解key的用法和配置问题,所以我需要先解释默认的key生成规则,然后再说如何自定义。根据引用[1],默认的Key生成策略是使用SimpleKeyGenerator,它基于方法参数生成key。如果有多个参数,会生成包含所有参数的键;如果无参数,则返回SimpleKey.EMPTY。这部分需要详细说明,让用户明白默认行为是怎样的。 接下来是自定义key的方法,这需要使用SpEL表达式。引用[2]、[3]、[4]都提到了在@Cacheable注解中使用key属性,给出了一些例子,比如key = "#root.method.name"或者组合多个参数。这里需要举几个例子,说明不同的SpEL表达式如何影响key的生成,比如使用方法名、参数属性等。 然后是关于自定义KeyGenerator的部分。引用[1]和[5]提到可以自定义实现KeyGenerator接口,在配置类中声明Bean。需要说明实现步骤,例如创建一个类实现KeyGenerator接口,重写generate方法,然后在注解中指定generator属性。同时,用户需要注意如果配置了全局的key-prefix,可能会导致value失效,如引用[4]所提到的,这点需要特别提醒用户,避免配置错误。 另外,引用[5]提到缓存Rediskey是value和key值的拼接,这里可能需要举例说明生成的key格式,比如user::getUserById_123,这样用户能更直观地理解拼接方式。 在组织回答结构时,应该先分点说明默认规则,再讲自定义方法,再提配置注意事项。同时,确保引用标识正确对应,比如引用[1]对应默认生成器,引用[4]对应key-prefix的问题等。最后生成相关问题,需要围绕Spring Cache的其他配置、SpEL的用法、性能优化等方面提问,帮助用户扩展知识。 需要检查是否有遗漏的信息,例如是否提到缓存分区的value属性以及key-prefix的影响,确保覆盖用户可能遇到的问题。另外,注意使用中文,避免技术术语错误,保持回答清晰易懂。</think>### Spring CacheKey生成规则及自定义配置方法 #### 一、默认Key生成规则 Spring Cache默认使用`SimpleKeyGenerator`生成缓存Key,规则如下: 1. **无参数方法**:Key为`SimpleKey.EMPTY`,例如:`value::SimpleKey.EMPTY`[^1]。 2. **单个参数方法**:直接使用参数值作为Key,例如参数为`123`时生成`value::123`。 3. **多个参数方法**:将所有参数组合成`SimpleKey`对象,例如参数为`(1, "test")`时生成`value::1-test`[^4]。 #### 二、自定义Key的3种方式 1. **通过SpEL表达式定义Key** 在`@Cacheable`注解中直接指定`key`属性,支持Spring表达式语言(SpEL): ```java @Cacheable(value="user", key="#root.methodName + '_' + #id") // 示例:getUserById_123 public User getUserById(Long id) { ... } @Cacheable(value="order", key="#user.id + ':' + #orderType") // 示例:10001:VIP public Order getOrder(User user, String orderType) { ... } ``` 常用SpEL变量: - `#root.methodName`:当前方法名 - `#root.targetClass`:目标类 - `#参数名`:方法参数值[^4] 2. **自定义KeyGenerator实现类** 步骤: ```java // 1. 实现KeyGenerator接口 public class CustomKeyGenerator implements KeyGenerator { @Override public Object generate(Object target, Method method, Object... params) { return method.getName() + "_" + Arrays.hashCode(params); } } // 2. 声明Bean @Configuration public class CacheConfig { @Bean public KeyGenerator customKeyGenerator() { return new CustomKeyGenerator(); } } // 3. 使用注解指定 @Cacheable(value="product", keyGenerator="customKeyGenerator") ``` 此方式适合需要全局统一Key生成逻辑的场景[^1][^5]。 3. **配置全局Key前缀** 在`application.properties`中设置: ```properties spring.cache.redis.key-prefix=CACHE_ spring.cache.redis.use-key-prefix=true ``` **注意**:启用全局前缀后,`@Cacheable`的`value`将失效,所有Key以全局前缀开头[^4]。 #### 三、配置注意事项 | 场景 | 解决方案 | |-----------------------|--------------------------------------------------------------------------| | 参数包含复杂对象 | 使用`#参数名.属性`,如`#user.id` | | 需要拼接多级Key | 组合SpEL表达式,如`key="#type + ':' + #id"` | | 全局前缀与局部Key冲突 | 禁用`use-key-prefix`,在SpEL中手动拼接前缀 | | 热Key问题 | 在Key生成规则中加入随机因子(如时间戳) | #### 四、示例:Redis中的最终Key格式 若配置全局前缀`CACHE_`且使用`@Cacheable(value="user", key="#id")`,实际存储的Key为: ``` CACHE_user::123 ``` 其中`user`为缓存分区,`::`是Spring默认的分隔符。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值