当前位置 : 首页 » 文章分类 :  开发  »  Spring-Cache

Spring-Cache

Spring 缓存使用笔记

Spring 为我们提供了几个注解来支持 Spring Cache。其核心主要是 @Cacheable@CacheEvict
使用 @Cacheable 标记的方法在执行后 Spring Cache 将缓存其返回结果。
使用 @CacheEvict 标记的方法会在方法执行前或者执行后移除 Spring Cache 中的某些元素。


注意事项

@Cacheable 同一个类内调用不生效

@Transactional 事务注解类似, @Cacheable 注解也是通过 AOP 切面实现的,同一个类内部调用不走代理,所以此时 @Cacheable 也不生效。

@Cacheable 命中时会使同方法上的其他切面失效

如果注解了 @Cacheable 的方法同时有其他切面注解,则当缓存命中的时候,其它注解不能正常切入并执行, @Before 也不行,当缓存没有命中的时候,其它注解可以正常工作。


断点排查

org.springframework.cache.interceptor.CacheAspectSupport
findInCaches 遍历cache,根据key从缓存中查询

org.springframework.cache.interceptor.AbstractCacheInvoker
在这个类上打断点,能看到缓存的具体实现
doGet 根据key从具体一个cache中查询
doPut 放入缓存
doEvict 清理缓存

org.springframework.cache.caffeine.CaffeineCacheManager
cache 创建


CacheManager

要启用缓存支持,我们需要创建一个新的 CacheManager bean。
CacheManager 接口有很多实现,比如 RedisCacheManagerEhCacheCacheManager 等。

假如系统中只有一个 CacheManager 实例的话,Spring 默认使用这个缓存管理器,不需要在 @Cacheable 等缓存操作注解上额外指定 cacheManager 参数。
如果系统中有多个 CacheManager 实例的话,可以通过 cacheManager 参数选择使用的缓存管理器。

CacheManager 配置多个不同有效期的缓存空间

// redis缓存空间名
public static final String REDIS_TIME_CACHE_GROUP = "redisTimeGroup";
public static final String REDIS_CACHENAME_15MIN = "redis15MinCache";
public static final String REDIS_CACHENAME_5MIN = "redis5MinCache";

@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
    // 生成一个默认配置,通过config对象即可对缓存进行自定义配置
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
    config = config
            // 设置缓存的默认过期时间(Duration.ZERO表示永久),也是设置具体时间 Duration.ofMinutes(1)
            .entryTtl(Duration.ZERO)
            // 设置 key为string序列化
            .serializeKeysWith(
                    RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            // 设置value为json序列化
            .serializeValuesWith(
                    RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer()))
            // 不缓存空值
            .disableCachingNullValues();

    // 设置一个初始化的缓存空间set集合
    Set<String> cacheNames = new HashSet<>();
    cacheNames.add(REDIS_TIME_CACHE_GROUP);
    cacheNames.add(REDIS_CACHENAME_15MIN);
    cacheNames.add(REDIS_CACHENAME_5MIN);

    // 对每个缓存空间应用不同的配置
    Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
    configMap.put(REDIS_TIME_CACHE_GROUP, config);
    configMap.put(REDIS_CACHENAME_5MIN, config.entryTtl(Duration.ofMinutes(5)));
    configMap.put(REDIS_CACHENAME_15MIN, config.entryTtl(Duration.ofMinutes(15)));

    // 使用自定义的缓存配置初始化一个cacheManager
    RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory)
            // 一定要先调用该方法设置初始化的缓存名,再初始化相关的配置
            .initialCacheNames(cacheNames)
            .withInitialCacheConfigurations(configMap)
            .build();
    return cacheManager;
}

Spring 缓存集成 redis

以构造 RedisCacheManager 为例, 先构造连接工厂,再构造 RedisTemplate,再构造 CacheManager。
配置 CacheManager 时可以指定默认的过期时间,不设置的话默认为永久有效。

package com.masikkk.cache.redis;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

@Configuration
@EnableCaching
public class RedisCacheConfig extends CachingConfigurerSupport {

    @Bean
    public JedisConnectionFactory redisConnectionFactory() {
        JedisConnectionFactory redisConnectionFactory = new JedisConnectionFactory();
        redisConnectionFactory.setHostName("192.168.1.166");
        redisConnectionFactory.setPort(6379);
        return redisConnectionFactory;
    }

    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory cf) {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<String, String>();
        redisTemplate.setConnectionFactory(cf);
        return redisTemplate;
    }

    @Bean
    public CacheManager cacheManager(RedisTemplate redisTemplate) {
        RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);

        // Number of seconds before expiration. Defaults to unlimited (0)
        cacheManager.setDefaultExpiration(3000); // Sets the default expire time (in seconds)
        return cacheManager;
    }

}

Redis 缓存 + Spring 的集成示例
https://blog.csdn.net/defonds/article/details/48716161

要缓存的 Java 对象必须实现 Serializable 接口

要缓存的 Java 对象必须实现 Serializable 接口,因为 Spring 会将对象先序列化再存入 Redis。
比如缓存方法返回一个 UserBean ,则此 UserBean 必须实现 Serializable 接口,否则会报如下序列化错误:

org.springframework.data.redis.serializer.SerializationException: Cannot serialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.masikkk.bean.UserBean]

缓存Java对象时报错Could not read JSON: Unrecognized field “xx”

从 reids 中反序列化 json 对象时报错
spring cache class Could not read JSON: Unrecognized field “new”

直接原因:
序列化后的 json 中有多余字段,反序列化时 Jackson 找不到对于的字段,又没配置可忽略未知字段,所以就报错了。

深层原因:
在 json 序列化时,不仅是根据 get 方法来序列化的,而是实体类中所有的有返回值的方法都会将返回的值序列化,但是反序列化时是根据 set 方法来实现的,所以当实体类中有非get,set方法的方法有返回值时,反序列化时就会出错。

解决方法:
1 去掉多余方法,实体类中只放get,set方法或返回值为空的方法。

2 关闭 Jackson 的 FAIL_ON_UNKNOWN_PROPERTIES
RedisTemplate 的 HashValue 序列化器 Jackson2JsonRedisSerializer 的 ObjectMapper 配置关闭 FAIL_ON_UNKNOWN_PROPERTIES 选项

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(connectionFactory);
    template.setValueSerializer(jackson2JsonRedisSerializer());
    template.setKeySerializer(new StringRedisSerializer());
    template.setHashKeySerializer(new StringRedisSerializer());
    template.setHashValueSerializer(jackson2JsonRedisSerializer());
    template.afterPropertiesSet();
    return template;
}

@Bean
public RedisSerializer<Object> jackson2JsonRedisSerializer() {
    //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
    Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class);

    ObjectMapper mapper = new ObjectMapper();
    mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    serializer.setObjectMapper(mapper);
    return serializer;
}

@Cacheable 注解查询方法

@Cacheable 可以标记在一个方法上,也可以标记在一个类上。当标记在一个方法上时表示该方法是支持缓存的,当标记在一个类上时则表示该类所有的方法都是支持缓存的。

对于一个支持缓存的方法,Spring 会在其被调用后将其返回值缓存起来,以保证下次利用同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法。Spring 在缓存方法的返回值时是以键值对进行缓存的,值就是方法的返回结果,至于键的话,Spring 又支持两种策略,默认策略和自定义策略,这个稍后会进行说明。

需要注意的是当一个支持缓存的方法在对象内部被调用时是不会触发缓存功能的

@Cacheable 可以指定三个属性:value/cacheNames, key 和 condition

value/cacheNames 指定cache名称

value 属性是必须指定的,其表示当前方法的返回值是会被缓存在哪个 Cache 上的,对应 Cache 的名称。其可以是一个 Cache 也可以是多 个Cache, 当需要指定多个 Cache 时其是一个数组。

//Cache是发生在cache1上的
@Cacheable("cache1")
public User find(Integer id) {
  returnnull;
}

//Cache是发生在cache1和cache2上的
@Cacheable({"cache1", "cache2"})
public User find(Integer id) {
  returnnull;
}

key指定缓存key

key 属性是用来指定 Spring 缓存方法的返回结果时对应的 key 的。该属性支持 SpringEL 表达式。没有指定该属性时,Spring 将使用默认的 SimpleKeyGenerator 策略生成 key
自定义策略是指我们可以通过 Spring 的 EL 表达式来指定key。这里的EL表达式可以使用方法参数及它们对应的属性。使用方法参数时我们可以直接使用 #参数名 或者 #p参数index。下面是几个使用参数作为key的示例。

@Cacheable(value="users", key="#id")
public User find(Integer id) {
  return null;
}

@Cacheable(value="users", key="#p0")
public User find(Integer id) {
  return null;
}

@Cacheable(value="users", key="#user.id")
public User find(User user) {
  return null;
}

@Cacheable(value="users", key="#p0.id")
public User find(User user) {
  return null;
}

使用对象toString()方法做key

@Cacheable(value="users", key="#user.toString()")
public User find(User user) {
  return null;
}

@Cacheable 固定key报错Property or field ‘xx’ cannot be found on object of type

报错
org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field ‘xx’ cannot be found on object of type ‘org.springframework.cache.interceptor.CacheExpressionRootObject’ - maybe not public or not valid?
at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:217)

原因:
@Cacheable 中指定固定 key 参数时,必须在key串上加英文单引号,如下,否则会报上述错误

@Cacheable(value = "REDIS_CACHENAME_15MIN", key = "'user-ids'")
public List<String> getRepositoryIdList() {
}

@Cacheble annotation on no parameter method
https://stackoverflow.com/questions/33383366/cacheble-annotation-on-no-parameter-method

SpEL

用 缓存名 + 字符串入参的小写 作为key,入参的小写可用spel写作:args[0]?.toLowerCase(),使用?符号代表若左边的值为null,将不执行右边方法,避免空指针产生

@Cacheable(value = "user-name-", key = "caches[0].name + args[0]?.toLowerCase()")
public UserSourceTypeBean getUserByName(String name) {
    checkState(StringUtils.isNotBlank(name), "Required string param 'name' can not be blank.");
    UserExample example = new UserExample();
    example.createCriteria().andNameEqualTo(name.toLowerCase());
    List<User> userList = userMapper.selectByExample(example);
    return CollectionUtils.isNotEmpty(userList) ? convertToUserBean(userList.get(0)) : null;
}

root对象

除了上述使用方法参数作为key之外,Spring还为我们提供了一个root对象可以用来生成key。通过该root对象我们可以获取到以下信息。

属性名称 描述 示例
methodName 当前方法名 #root.methodName
method 当前方法 #root.method.name 等于 #root.methodName
target 当前被调用的对象 #root.target
targetClass 当前被调用的对象的class #root.targetClass
args 当前方法参数组成的数组 #root.args[0]
caches 当前被调用的方法使用的Cache #root.caches[0].name

当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。如:

@Cacheable(value={"users", "xxx"}, key="caches[1].name")
public User find(User user) {
  return null;
}

比如用cache名称和第一个参数做key:

@Cacheable(value = "cache-user", key = "caches[0].name + args[0]")
public User findUser(Long userId) {
 ...
}

keyGenerator 指定key生成器

自定义 key 生成器的 bean 名称,实现 org.springframework.cache.interceptor.KeyGenerator 接口

如何查看自动生成的key

可以在 CacheAspectSupport 抽象类的 generateKey 方法上打个断点,看到生成的具体 key 是什么
或者打开 tracer 日志后可以看到 Computed cache key {} for operation {}


SimpleKeyGenerator 默认key生成策略

Spring Cache 默认 key 生成策略为 SimpleKeyGenerator,返回一个 SimpleKey 对象,具体如何使用这个 SimpleKey,不同的底层 cache 实现不同:

  • 使用 Caffeine 缓存时,直接用 Object 对象做缓存的 key
  • 使用 redis 缓存时,用 Object 的 toString() 结果做缓存 key

所以,使用 Caffeine 缓存时,方法入参是 object 时不能直接使用默认 key 生成策略,比如 User find(Query query),因为如果每次入参 query object 都是新生成的,则每次 key 都不一样

public class SimpleKeyGenerator implements KeyGenerator {
    @Override
    public Object generate(Object target, Method method, Object... params) {
        return generateKey(params);
    }

    public static Object generateKey(Object... params) {
        if (params.length == 0) {
            return SimpleKey.EMPTY;
        }
        if (params.length == 1) {
            Object param = params[0];
            if (param != null && !param.getClass().isArray()) {
                return param;
            }
        }
        return new SimpleKey(params);
    }
}

public class SimpleKey implements Serializable {
    @Override
    public String toString() {
        return getClass().getSimpleName() + " [" + StringUtils.arrayToCommaDelimitedString(this.params) + "]";
    }
}

KeyGenerator 实例

baeldung 给出的 KeyGenerator 最佳实践,还挺好用的

/**
 * 缓存的key是 类名+方法名+参数列表
 */
@Bean
public KeyGenerator keyGenerator() {
    return (target, method, params) -> target.getClass().getSimpleName() + "_"
            + method.getName() + "_"
            + StringUtils.arrayToDelimitedString(params, "_");
}

Spring Cache – Creating a Custom KeyGenerator
https://www.baeldung.com/spring-cache-custom-keygenerator


condition指定缓存的条件

有的时候我们可能并不希望缓存一个方法所有的返回结果。通过condition属性可以实现这一功能。
condition属性默认为空,表示将缓存所有的调用情形。其值是通过SpringEL表达式来指定的,当为true时表示进行缓存处理;当为false时表示不进行缓存处理,即每次调用该方法时该方法都会执行一次。

如下示例表示只有当user的id为偶数时才会进行缓存。

@Cacheable(value={"users"}, key="#user.id", condition="#user.id%2==0")
public User find(User user) {
  System.out.println("find user by user " + user);
  return user;
}

unless 指定不缓存的条件

使用 Spring Expression Language (SpEL) 指定不缓存哪些情况

使@Cacheable不缓存null值

@Cacheable(key = "#id", unless="#result == null")
public XXXPO get(int id) {
   //get from db
}

@CachePut 更新缓存

在支持 Spring Cache 的环境下,对于使用 @Cacheable 标注的方法,Spring 在每次执行前都会检查 Cache 中是否存在相同 key 的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。
@CachePut 也可以声明一个方法支持缓存功能。与 @Cacheable 不同的是使用 @CachePut 标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。

@CachePut 也可以标注在类上和方法上。使用@CachePut时我们可以指定的属性跟@Cacheable是一样的。

//每次都会执行方法,并将结果存入指定的缓存中
@CachePut("users")
public User find(Integer id) {
  returnnull;
}

@CacheEvict 清除缓存

@CacheEvict 是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。
@CacheEvict 可以指定的属性有value、key、condition、allEntries和beforeInvocation。其中value、key和condition的语义与@Cacheable对应的属性类似。即value表示清除操作是发生在哪些Cache上的(对应Cache的名称);key表示需要清除的是哪个key,如未指定则会使用默认策略生成的key;condition表示清除操作发生的条件。

下面我们来介绍一下新出现的两个属性allEntries和beforeInvocation。

allEntries 属性

allEntries 是 boolean 类型,表示是否需要清除缓存中的所有元素。默认为false,表示不需要。当指定了allEntries为true时,Spring Cache将忽略指定的key。有的时候我们需要Cache一下清除所有的元素,这比一个一个清除元素更有效率。

@CacheEvict(value="users", allEntries=true)
public void delete(Integer id) {
  System.out.println("delete user by id: " + id);
}

beforeInvocation 属性

清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。

@CacheEvict(value="users", beforeInvocation=true)
public void delete(Integer id) {
  System.out.println("delete user by id: " + id);
}

其实除了使用@CacheEvict清除缓存元素外,当我们使用Ehcache作为实现时,我们也可以配置Ehcache自身的驱除策略,其是通过Ehcache的配置文件来指定的。

Spring缓存注解@Cacheable、@CacheEvict、@CachePut使用
https://blog.csdn.net/wjacketcn/article/details/50945887

注释驱动的 Spring cache 缓存介绍
https://www.ibm.com/developerworks/cn/opensource/os-cn-spring-cache/


上一篇 Node.js

下一篇 VIM

阅读
评论
3.8k
阅读预计16分钟
创建日期 2019-01-30
修改日期 2023-07-12
类别

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论