SpringCache

124 阅读5分钟

一、前置知识:JCache

在Spring Boot中使用缓存之前,我们有必要了解一下JCache。

JCache定义了五个核心接口,分别是CachingProvider,CacheManager,Cache,Entry和Expiry。

-   一个CachingProvider可以创建和管理多个**CacheManager**,并且一个CacheManager只能被一个CachingProvider所拥有,而一个应用可以在运行期间访问多个CachingProvider。
-   一个CacheManager可以创建和管理多个唯一命名的**Cache**,且一个Cache只能被一个CacheManager所拥有,这些Cache存在于CacheManager的上下文中。
-   Cache是一个类似Map的数据结构
-   Entry是一个存储在Cache中的key-value对
-   Expiry是指存储在Cache中的Entry的有效期,一旦超过这个时间,Entry将处于过期状态,即不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。

1664469141521.png

二、 Spring Cache原理

Spring 3.1开始,引入了Spring Cache,即Spring 缓存。

  • 通过定义org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术,并支持使用JCache注解简化开发过程。

  • Cache接口包含缓存的各种操作集合。

  • Spring中为Cache接口提供了各种xxxCache的实现:RedisCache,EhCacheCache,ConcurrentMapCache等。

    (ConcurrentMapCache无法实现持久化缓存,是Spring默认的Cache实现)

CacheManager

CacheManager 基于 name 管理一组 Cache。

public interface CacheManager {
    @Nullable
    //获取指定name的Cache,可能延迟创建
    Cache getCache(String name);

    //获取当前CacheManager下的Cache name集合
    Collection<StringgetCacheNames();
}

三、Spring Cache实践

在Spring Cache时,还可以通过 JCache注解来实现缓存的功能。

@Cacheable、@CacheEvict和@CachePut三个注解都是对方法进行配置

image.png

image.png

Cache中可用的SpEL表达式

image.png

@EnableCaching

这个注解表示开启基于注解的缓存,一般放在主程序类前面

@Cacheable

这个注解放在方法前,可以将方法的运行结果进行缓存,之后就不用调用方法了,直接从缓存中取值即可。

  • @Cacheable注解中常用的参数有cacheNames/value(指定缓存组件的名字,可以指定多个)
  • key(缓存数据时使用的key,默认使用方法参数的值,也可以自定义)
  • keyGenerator(key的生成器,可以自定义,key与keyGenerator二选一)
  • cacheManager(指定缓存管理器,或者使用cacheResolver指定获取解析器)
  • condition(符合条件才缓存)、
  • unless(符合条件则不缓存,可以获取方法运行结果进行判断)
  • sync(是否使用异步模式,不可与unless一起使用)。
@Cacheable(cacheNames = {"emp"}, key = "#id", conditon = "mid>0", unless = "#result == null")
public Employee getEmp(Integer id) {
    Employee emp = employeeMapper.getEmpById(id);
    return emp;
}

@Cacheable的运行原理

  1. 方法运行前,程序会使用cacheManager根据cacheNames获取Cache,如果没有对应名称的Cache,则自动创建一个Cache。
  2. 使用key去Cache中查找对应的缓存内容。 key默认使用SimpleKeyGenerator生成,其生成策略如下:
    • 如果没有参数,key=new SimpleKey()
    • 如果只有一个参数,key=参数值
    • 如果有多个参数,key=new SimpleKey(params)
  3. 没有查到缓存值,则调用目标方法
  4. 以步骤二中返回的key,目标方法返回的结果为value,存入缓存

@CachePut

@CachePut注解先调用目标方法,然后再缓存目标方法的结果。

@CachePut(value = "emp", key = "#result.id")
public Employee updateEmp(Employee employee) {
    employeeMapper.updateEmp(employee);
    return employee;
}

@CacheEvict

@CacheEvict用于清除缓存,可以通过key指定需要清除的缓存,allEntries置为true表示删除所有缓存。

@CacheEvict(value = "emp", key = "#id", allEntries = true)
public void deleteEmp(Integer id) {
    employeeMapper.deleteEmpById(id);
}

默认删除缓存行为在方法执行之后(beforeInvocation=false),如果方法运行异常,则该缓存不会被删除。但可以通过设置beforeInvocation = true,将删除缓存行为在方法执行之前。

@Caching&@CacheConfig

@Caching注解中包含了@Cacheable、@CachePut和@CacheEvict注解,可以同时指定多个缓存规则。示例如下所示:

@Caching(
    cacheable = {
    @Cacheable(value = "emp", key = "#lastName")
},
    put = {
        @CachePut(value = "emp", key = "#result.id")
        @CachePut(value = "emp", key = "#result.email")
    }
)

@CacheConfig注解放在类上,抽取缓存的公共配置,如cacheNames、cacheManager等,这样就不用在每个缓存注解中重复配置了。

@Cacheable和@CachePut的区别

相同:都是Spring的缓存注解

不同点:

  • @Cacheable:只会执行一次,标记在一个方法上时表示该方法是支持缓存,方法被调用后将其返回值缓存起来,下次再执行该方法时可以直接从缓存中获取结果。

  • @CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。

四、自定义配置

使用Redis实现Spring cache时。

Redis自定义key规则

通过自定义KeyGenerator就不需要在每一个缓存方法上自定义key的规则了。

以下配置的规则是使用类名+方法名+参数值来作为key的规则。

@Bean
public KeyGenerator keyGenerator() {
    return new KeyGenerator() {
        @Override
        public Object generate(Object target, Method method, Object... params) {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getName());
            sb.append(method.getName());
            for (Object obj : params) {
                sb.append(obj.toString());
            }
            return sb.toString();
        }
    };
}

自定义RedisTemplate

被序列化的对象必须实现Serializable接口

  • RedisTemplate用的key和value的序列化方式是默认的 JdkSerializationRedisSerializer,采用这种序列化方式,redis缓存中的key和value是乱码的。
  • 通过使用StringRedisSerializer(字符串形式)以及Jackson2JsonRedisSerializer来设置序列化方式,就可以避免乱码。
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常```
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        
        //设置key序列化方式string
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置value的序列化方式json
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        //设置hash key序列化方式string
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        //设置hash value的序列化方式json
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        //初始化RedisTemplate的一些参数设置,如果不执行此方法,可能会报一些错误
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}