如何理解SpringBoot的缓存

写在前面

本文参考自 Spring Boot 官方文档


简介

对于缓存,我们常说的应该是 redis来做缓存了,但是 Spring 难道仅仅只支持特定的 redis 来做缓存嘛?Spring 经常强调的思想是:”俺们不重复造轮子,俺们只是轮子的适配者>“。

Spring 框架支持向应用程序透明地添加缓存。你可以自由地选择缓存的具体实现。抽象的核心是将缓存应用于方法,从而减少了基于缓存中可用信息的执行次数。缓存逻辑对于应用程序来说是透明的,对调用程序没有干扰。只要通过 @EnableCaching 注释启用了缓存支持,Spring Boot 就会自动配置缓存的基础设施。

缓存的抽象并没有提供实际的存储,抽象的好处就在于我们不用依赖某种具体实现,我们可以在生产期间更换不同的实现,对代码却不会产生什么影响(除非你觉得更换是有必要的,否则仍然不建议这么做)。缓存的抽象主要依赖于 org.springframework.cache.Cacheorg.springframework.cache.CacheManager 接口。


如果没有添加任何特定的缓存库,Spring Boot 将自动配置一个简单的 ConcurrentMap 实现的缓存。


支持的缓存实现库

如果没有定义 CacheManager 类型的 bean 或 名为 cachedResolverCacheResolver , Spring Boot 将按以下的顺序检测提供程序:

  1. Generic
  2. JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
  3. EhCache 2.x
  4. Hazelcast
  5. Infinispan
  6. Couchbase
  7. Redis
  8. Caffeine
  9. Simple

我们仍然可以选择通过 spring.cache.type 来强制使用特定的缓存提供程序。或者 spring.cache.type = none 来禁用缓存。有关以上特定的缓存库介绍,可以查看文档(指导了你需要确保怎样的jar包需要在你的类路径下)。


简单使用

抽象的好处开始体现,想要我们针对不同的缓存库写代码??

instrest
所以,我们只需要 “简单使用”。


这里只介绍基于注解的声明式缓存使用,它还支持基于xml的,并且从 4.1 开始,它还支持 JCache 标准注解。

  • @Cacheable:标志着方法调用的结果可以被缓存,如果注解在类上,则代表该类的所有方法的执行结果都可以被缓存。
  • @CacheEvict:标志着方法将会触发 org.springframework.cache.Cacheevict 方法(任然可以注解在类上),通俗地来说,就是移除缓存。
  • @CachePut:标志着方法将会触发 cacheput 方法。与 @Cacheable 注解所不同的是,该注解标识的方法并不会跳过执行,它总是保存方法执行的结果到关联的缓存中。
  • @Caching:缓存注解的组注解。
  • @CacheConfig:提供了一种在类级别上共用的缓存配置。它将给被注解类的任何缓存操作提供默认设置。

用例:操作 ”student“ 实体,以下部分代码,均建立在启动类上已经添加 @EnableCaching 注解,并且类路径下不存在其它特定的缓存库实现。

@Cacheable
	@Override
    @Cacheable("students")
    public Student getStudentById(String id){
        for (Student student : container) {
            if(student.getId().equals(id)){
                return student;
            }
        }
        return null;
    }

在可调试模式下,为上面的代码打上断点,基于相同的访问时,只会执行一次。

如果你想了解的更多,你可以通过注入 ApplicationContext bean 来查看 cacheManager 这个 bean,看一下缓存的具体实现到底是怎样的。下面计算来自于第一次执行 getStudentById 方法体的时候:
CacheManager

如果不会该注解添加 value 值或者 cacheNames 值,你将得到一个错误;从上面的图片也能发现在第一次进入方法体执行之前,需要为抽象的缓存构建具体的存储,需要 “students” 这个 key。

以下是在依次请求了 id 为 1,2以后,此次请求id为 3时,在方法体内断点时,重新计算的 cacheManager
CacheManager

我们的暂时抛弃以上 key 这个概念,它所代表的是一个 Cache 的具体实现。我们依赖它在众多 @Cacheable 注解标记的方法中,找到具体的 Cache 实现。那么如何在单个方法的多次调用定位到缓存的值呢?这才是 Spring 所说的 “key”。

  • 如果方法没有任何参数,将使用 SimpleKey.EMPTY
  • 如果仅仅只有一个参数,将使用它的实例
  • 如果有多个参数,将使用SimpleKey 包含所有的参数。

可以参考 SimpleKey 这个类,理解它如何处理参数,来生成 key 的。以上的方法适用于大多数用例,只要参数具有自然键并实现有效的hashCode()和equals()方法。如果不是这样,你需要改变策略,自己实现 org.springframework.cache.interceptor.KeyGenerator 接口。

如何实现自定义key生成

拓展以上方法

	@Override
    @Cacheable(cacheNames = "studentsOfTeacher", keyGenerator = "keyGeneratorForTeacher")
    public List<Student> getStudentByTeacher(Teacher teacher){
        List<Student> findStudents = new ArrayList<>();
        for (Student student : container) {
            if(student.getName().equals(teacher.getName())){
                findStudents.add(student);
            }
        }
        return findStudents;
    }

在使用自定义 key 生成之前,你需要确保通过 “key” 属性,无法完成你的需求,否则,没有必要做负责的工作。如果使用 key 属性(支持 SpEL), 我们仅仅需要修改 @Cacheable :

@Cacheable(cacheNames = "studentsOfTeacher", key="#teacher.name")

KeyGenerator 接口的实现如下:

@Component("keyGeneratorForTeacher")
public class KeyGeneratorForTeacher implements KeyGenerator {
    @Override
    public Object generate(Object target, Method method, Object... params) {
        if(params.length != 1){
            throw new IllegalArgumentException("i just need one param!");
        }
        if(params[0] instanceof Teacher){
            Teacher teacher = (Teacher) params[0];
            return teacher.getName();
        }
        return method.getName() + Arrays.stream(params)
                .map(Object::toString).collect(Collectors.joining(","));
    }
}

需要注意的是在第一次请求该方法时,以上 key 的生成策略会被调用两次;第二次请求该方法(使用相同的参数),生成策略仅仅只会被调用一次。

该注解还支持使用其它 CacheManager 的实现(cacheManager 属性),甚至支持自定义如何从上下文中解析出 CachecacheResolver属性)。

如何应对多线程环境

在一个多线程环境中,某些操作可能并发调用相同的点(通常在启动时)。默认情况下,缓存抽象不锁,相同的值可能会计算几次。

对于这些特定的情况下,可以使用 sync 属性指示底层的缓存提供者锁定缓存条目。结果只会有一个线程正在忙着计算值, 而其他人则将被阻塞,直到该条目更新缓存。

这是一个可选的特性,某些缓存库可能不支持它。

如何实现只缓存某些特定的调用(基于参数或者调用结果)

conditionunless 可以很好的帮助你完成这项需求。unless参数否决向缓存添加值的操作。与 condition 不同,unless 表达式是在方法被调用后求值的。


@CachePut

当需要在不影响方法执行的情况下更新缓存时,可以使用 @CachePut 注释。也就是说,方法总是被执行,它的结果被放到缓存中(根据 @CachePut 选项)。它支持与 @Cacheable 相同的选项,应该用于缓存填充而不是方法流优化。

并不应该讲 @CachePut@Cacheable 声明在同一个方法上,它们具有不同的行为。


@CacheEvict

此注解用于从缓存中删除过时或未使用的数据。由于方法仅仅充当触发器,返回值将被忽略;但如果方法中抛出了异常,此时,你并仍然想触发缓存清除操作怎么办?我们可以通过 beforeInvocation 这个属性来解决。某种时刻,你想清楚缓存的所有条目,可以使用 allEntries 属性来解决。


@Caching

有时,需要指定同一类型的多个注释(如 @CacheEvict@CachePut )—例如,因为条件或键表达式在不同的缓存之间是不同的。@Caching 允许在同一方法上使用多个嵌套的 @Cacheable@CachePut@CacheEvict 注释。


@CachConfig

存操作提供了许多定制选项,您可以为每个操作设置这些选项。但是,如果某些自定义选项应用于类的所有操作,那么配置它们可能会很繁琐。例如,为类的每个缓存操作指定要使用的缓存名称可以由单个类级别定义替换。这就是 @CacheConfig 发挥作用的地方。

操作级定制总是覆盖 @CacheConfig 上的定制集。因此,这为每个缓存操作提供了三层定制:

  • 全局配置,可用的 CacheManager , KeyGenerator

  • 在类级别,使用 @CacheConfig

  • 在操作层面。

总结

缓存的使用是非常简单的,多个注解拥有大多重复的属性,并且由于它的 透明性(应用程序),我们可以不依赖某种特定的缓存库来写代码。对于需要应用缓存的方法,应保证相同的参数调用,结果是一样的,否则,使用缓存就没有什么意义了。在多进程环境下,缓存的支持就和特定的缓存库实现有关,这个需要注意。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值