CaffeineCache基本使用 & SpringBoot集成缓存

平常开发中我们经常会使用到缓存,比如对于一些不常更新的数据却需要经常的访问或者计算,为这些热点数据加缓存可以有效减少服务器的性能损失和资源浪费。
本地缓存相比Redis缓存以及其他存储避免了网络IO的开销,它不需要发送redis命令,直接在本地jvm进程中操作缓存数据,而且基于内存的读写效率很高,所以在需要的时候合理使用本地缓存可以有效提高系统的吞吐量。CaffeineCache是非常优秀的开源本地缓存框架。本篇不分析其原理,只关注其简单用法。

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.6.2</version>
</dependency>

一、常用API

CaffeineCache 有非常简易的API,可以通过如下方式快速构建一个本地缓存。

LoadingCache<String, Object> cache = Caffeine.newBuilder()
                .expireAfterWrite(1, TimeUnit.SECONDS)
                .expireAfterAccess(1, TimeUnit.SECONDS)
                .maximumSize(10)
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String s)  {
                        return s + "loadValue";
                    }
                });

缓存的常用API如下:

public interface Cache<K, V> {
    //取值,如果不存在则返回null
    @Nullable
    V getIfPresent(@Nonnull Object key);

    //取值,如果不存在则执行函数并将执行结果缓存
    @Nullable
    V get(@Nonnull K key, @Nonnull Function<? super K, ? extends V> var2);

    //存值
    void put(@Nonnull K key, @Nonnull V value);

    //批量存值,接受一个map类型的参数,将map中的元素缓存起来
    void putAll(@Nonnull Map<? extends K, ? extends V> map);

    //移除缓存项
    void invalidate(@Nonnull Object key);

    //接受一个可迭代的参数进行批量移除
    void invalidateAll(@Nonnull Iterable<?> var1);

    //清空缓存
    void invalidateAll();
    
    //获取当前缓存项的数量(map中的条目数)
    @Nonnegative
    long estimatedSize();
    
    //缓存的map视图
    @Nonnull
    ConcurrentMap<K, V> asMap();
    //……
}
public interface LoadingCache<K, V> extends Cache<K, V> {
	@Nullable
    V get(@Nonnull K key);
    @Nonnull
    Map<K, V> getAll(@Nonnull Iterable<? extends K> var1);
    void refresh(@Nonnull K var1);
}

可以看到LoadingCache继承了Cache接口,扩展了三个API,这三个API描述如下:

1.get

  1. 获取key对应的value,在必要时通过CacheLoader加载数据。
  2. 加载的 非null数据会缓存 到Cache中。
  3. 如果同时已有其他线程在加载key对应的数据,当前线程阻塞waits,等待那个线程加载并返回那个加载到的数据。

2.getAll

  1. 获取所有key对应的value,在必要时通过CacheLoader加载数据。
  2. 加载的非null数据会缓存到Cache中。
  3. 如果同时已有其他线程在加载某个key对应的数据,当前线程阻塞waits,等待那个线程加载并返回那个加载到的数据。

3.refresh

  1. 异步刷新key对应的缓存数据
  2. 在新数据加载成功前,旧数据仍可用(除非过期或被驱逐)
  3. 新数据加载成功后,replace旧数据
  4. 如果数据加载时异常,会被swallowed。
  5. 如果数据加载的value为null,key对应的Entry会从缓存中移除。

此处参考自:http://events.jianshu.io/p/881c6f716850

二、缓存回收(清除):

1. 显式回收

  1. invalidate(@Nonnull Object key);
  2. invalidateAll(@Nonnull Iterable<?> var1);
  3. invalidateAll()

2. 隐式回收

2.1 基于容量

maximumSize(long):通过指定缓存的最大条目数,当缓存的条目超过最大大小,则按照一定的淘汰算法将旧数据移除。

LoadingCache<String, Object> cache = Caffeine.newBuilder().maximumSize(10)//……

Caffeine采用W-TinyLFU算法作为缓存淘汰算法,这种算法结合了LRU和LFU。算法描述可参考https://my.oschina.net/manmao/blog/603253https://blog.csdn.net/l_dongyang/article/details/108583476

2.2 基于时间

  1. TTL(Time To Live): 通过expireAfterWrite(long, TimeUnit)指定当缓存项被写入(创建/覆盖)多久后被回收。
  2. TTI(Time To Idle): 通过expireAfterAccess(long, TimeUnit)指定缓存项多久没有被访问(读/写)而将其回收。
LoadingCache<String, Object> cache = Caffeine.newBuilder().weakValues()
                .expireAfterWrite(5, TimeUnit.SECONDS)
                .expireAfterAccess(5, TimeUnit.SECONDS)//……

特别需要注意的是:设置过期时间之后,到达指定之间后缓存项并不会被马上移除,它没有启动定时器,而是依赖下次对cache的操作(读写操作),当下次对这个cache对象进行读写操作时,它会利用一个线程将cache中的已失效缓存项从内存中清除。expireAfterWrite和expireAfterAccess如果同时设置,取二者指定的最小时间。

  1. put操作:put当前的缓存项,并启动一个线程去后台执行过期缓存项的移除任务。
  2. get操作:如果发现cache中有过期缓存项,则启动一个线程去后台执行这些缓存项的移除任务,如果get的正好是过期的缓存项,则当前线程会马上执行load方法,直到load方法加载完毕,get方法返回。
  3. getIfPresent操作:如果发现cache中有过期缓存项,则启动一个线程去后台执行这些缓存项的移除任务,如果getIfPresent的正好是过期的缓存项,则当前线程马上返回null。

2.3 基于引用

  1. 软引用:如果一个对象是软引用,那么当JVM堆内存不足时,垃圾回收器可以回收这些对象。软引用适合用来做缓存,从而当JVM堆内存不足时,可以回收这些对象腾出一些空间供强引用对象使用,从而避免OOM。框架提供 softValues() 供我们在创建缓存时调用,可以将值包装成软引用。

  2. 弱引用:当垃圾回收器回收内存时,如果发现弱引用,则将立即回收它。相对于软引用有更短的生命周期。框架提供weakKeys()和weakValues()两个方法供我们在创建缓存时调用,可以将键/值包装成弱引用,但是不要使用weakKeys(),这可能导致缓存无法命中。

LoadingCache<String, Object> cache = Caffeine.newBuilder().weakValues()
//……

2.4 基于权重

通过权重来计算,每个缓存项都有不同的权重,总权重到达最高时按照淘汰算法进行回收。
框架提供maximumWeight()方法指定最大的权重阈值,通过weigher()方法指定缓存项所占权重。

LoadingCache<String, Object> cache = Caffeine.newBuilder()
    .maximumWeight(10)
    .weigher(new Weigher<Object, Object>() {
                @Override
                public int weigh(Object key, Object value) {
                    //TODO
                    return 0;
                }
    }) //……

三、刷新缓存(reload)

LoadingCache<String, Object> cache = Caffeine.newBuilder()
                //方式二
                .refreshAfterWrite(3,TimeUnit.SECONDS)
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) throws Exception {
                        return key + "loadValue";
                    }

                    @Override
                    public Object reload(String key, Object oldValue) throws Exception {
                        //该方法默认返回 return CacheLoader.super.reload(key, oldValue)---里面直接调用了this.load(key)方法
                        return key + "reloadValue";
                    }
                });
    //方式一
   cache.refresh("key")      
  1. 方式一:通过cache.refresh(“key”)显示刷新,如果key已经被缓存,则执行reload方法重新加载;如果key没有被缓存,则执行load方法加载。

  2. 方式二:通过refreshAfterWrite方法定时刷新,该方法并不是通过额外的线程启动定时任务去刷新,而是依赖查询请求,当我们查询(get/getIfPresent)某个缓存时它会去比对当前时间与该缓存项被写入(创建/覆盖)的时间,如果超过了所设置的时间值,则执行reload方法刷新。

注意:reload方法是通过启动后台线程异步执行的,当前线程依然会立刻返回之前的旧值。当后台线程刷新完成并成功取代之前的旧值后再次获取才是刷新后的新值。

四、监听器

框架提供了缓存项被移除的事件监听机制,通过这种机制我们可以得知哪些缓存项被移除了,因为什么原因被移除的。当然也可以在key被移除时做一些其他操作。

LoadingCache<String, Object> cache = Caffeine.newBuilder()
                .removalListener(new RemovalListener<Object, Object>() {
                    @SneakyThrows
                    @Override
                    public void onRemoval(Object key, Object value, RemovalCause removalCause) {
                        System.out.println(key + ":" + value + "被移除了,原因是:" + removalCause);
                    }
                })//……
  1. SIZE:由于超过缓存最大数量被移除
  2. EXPLICIT: 被显式清除的
  3. EXPIRED:超过过期时间
  4. REPLACED:被取代(比如同一个key连续put两次,第一次的缓存项就是被取代的,还有被刷新等)
  5. COLLECTED:被垃圾收集器收集导致的。

五、外部存储

框架提供CacheWriter接口供我们在写入缓存项和删除缓存项时进行扩展,可以用于操作外部存储。

LoadingCache<String, Object> cache = Caffeine.newBuilder()
                .writer(new CacheWriter<Object, Object>() {
                    //写入缓存后会调用此方法
                    @Override
                    public void write(@NonNull Object key, @NonNull Object value) {
                        //这里可以将缓存的数据写入外部存储
                    }

                    //删除缓存项后会调用此方法
                    @Override
                    public void delete(@NonNull Object key, @Nullable Object value, @NonNull RemovalCause removalCause) {
                        //这里可以删除外部存储
                    }
                })//……

六、SpringBoot集成缓存

上述是Caffeine中的一些常用API使用,在SpringBoot项目中可以直接定义一个个LoadingCache的Bean,然后使用原生API操作缓存。除此之外,也可以使用SpringBoot提供的缓存操作的相关注解,可以简化开发(前提是需要配置**@EnableCaching**开启缓存)。

1. 几个常用的缓存注解

1.1 @Cacheable(最重要的注解)

该注解一般标注在方法上,在方法执行前会先查询缓存,如果命中,则不再目标执行方法,如果没有命中缓存,则目标执行方法,并将方法的返回结果缓存(这里就像上面CaffeineCache的CacheLoad一样,没有命中则执行load方法去加载)。该注解的属性如下:

public @interface Cacheable {
    @AliasFor("cacheNames")
    String[] value() default {};

    //  注定缓存的名字,比如员工缓存可以指定为empCache,学生缓存可以指定为stuCache。这些Cache都被CacheManager管理
    @AliasFor("value")
    String[] cacheNames() default {};

    //指定缓存的key,默认使用的是方法的参数值。支持SpEL写法
    String key() default "";

    //指定缓存key的生成器来生成key。若同时指定了key属性,则使用key属性指定的key
    String keyGenerator() default "";

    //指定缓存管理器
    String cacheManager() default "";

    //指定缓存解析器(与cacheManager功能一样)
    String cacheResolver() default "";

    //指定符合条件的情况下才缓存,支持SpEL写法
    String condition() default "";

    //否定缓存。当unless的条件为true,则不会被缓存。可以对结果进行判断,如unless="#result == null"(表示结果为null则不缓存),支持SpEl写法。
    String unless() default "";

    //是否需要异步去缓存
    boolean sync() default false;
}

CacheManager定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。
上述部分属性可以通过SpEL指定,Spring Cache提供了一些供我们使用的SpEL上下文数据,如下:

名称位置描述示例
methodNameroot对象当前被调用的方法名#root.methodname
methodroot对象当前被调用的方法#root.method.name
targetroot对象当前被调用的目标对象实例#root.target
targetClassroot对象当前被调用的目标对象的类#root.targetClass
argsroot对象当前被调用的方法的参数列表#root.args[0]
cachesroot对象当前方法调用使用的缓存列表#root.caches[0].name
Argument Name执行上下文当前被调用的方法的参数,如findArtisan(Artisan artisan),可以通过#artsian.id获得参数#artsian.id
result执行上下文方法执行后的返回值(仅当 方法执行后 的判断有效,如 unless cacheEvict的beforeInvocation=false)#result

使用如下:

@Cacheable(cacheNames = {"person"}, key = "#tid", cacheManager = "cacheManager")
public Person getPerson(String tid){
	return DBData.selectPerson(tid);
}

上面通过key属性指定了key,除此之外还可以实现KeyGenerator接口来指定key的生成规则。

@FunctionalInterface
public interface KeyGenerator {
	/**
	 * target:目标方法的对象实例
	 * method:目标方法
	 * args:目标方法的执行参数
	 * @return:key
     */
    Object generate(Object target, Method method, Object... args);
}

1.2 @CacheEvict

该注解一般标注在方法上,作用是清除缓存,当方法内对数据进行更新、删除操作的时候,那就需要用到它来清除缓存。否则将拿到旧数据。

public @interface CacheEvict {
    @AliasFor("cacheNames")
    String[] value() default {};
	//指定缓存
    @AliasFor("value")
    String[] cacheNames() default {};
    // 指定要清除的key
    String key() default "";
    //设置为true则表示清空缓存
    boolean allEntries() default false;
    //缓存的清除是否在方法执行之前,默认在方法执行成功之后执行
    boolean beforeInvocation() default false;
   //……
}

1.3 @CacheConfig

该注解只能标注在类上,用于指定缓存属性的全局配置。像上面@Cacheable、@CacheEvict注解如果每次使用都需要指定cacheNames属性的配置,而且这些cacheNames都一样,那就可以使用@CacheConfig将公共的配置提取到类上,改类的方法就默认以全局配置为主,不需要二次配置。

public @interface CacheConfig {
    String[] cacheNames() default {};
    String keyGenerator() default "";
    String cacheManager() default "";
    String cacheResolver() default "";
}

1.4 @CachePut

该注解一般标注在方法上,作用是会将被注解方法的返回值进行缓存。利用该注解也可以做缓存的更新。

1.5 @Caching

该注解是上面三种注解的组合注解,如果缓存规则比较复杂,可以使用它。

public @interface Caching {
    Cacheable[] cacheable() default {};
    CachePut[] put() default {};
    CacheEvict[] evict() default {};
}

2. 集成Caffeine作为缓存框架

SpringBoot默认使用ConcurrentMapCache,内部使用ConcurrentHashMap作缓存。当我们引入Caffeine的依赖之后,SpringBoot中的CaffeineCacheConfiguration就会生效,就会自动装配CaffeineCacheManager,则内部使用的缓存框架就变成Caffeine。默认的CaffeineCacheManager如下:

@Bean
CaffeineCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers customizers, ObjectProvider<Caffeine<Object, Object>> caffeine, ObjectProvider<CaffeineSpec> caffeineSpec, ObjectProvider<CacheLoader<Object, Object>> cacheLoader) {
	CaffeineCacheManager cacheManager = this.createCacheManager(cacheProperties, caffeine, caffeineSpec, cacheLoader);
	List<String> cacheNames = cacheProperties.getCacheNames();
	if (!CollectionUtils.isEmpty(cacheNames)) {
		cacheManager.setCacheNames(cacheNames);
	}
	return (CaffeineCacheManager)customizers.customize(cacheManager);
}

private CaffeineCacheManager createCacheManager(CacheProperties cacheProperties, ObjectProvider<Caffeine<Object, Object>> caffeine, ObjectProvider<CaffeineSpec> caffeineSpec, ObjectProvider<CacheLoader<Object, Object>> cacheLoader) {
	CaffeineCacheManager cacheManager = new CaffeineCacheManager();
	this.setCacheBuilder(cacheProperties, (CaffeineSpec)caffeineSpec.getIfAvailable(), (Caffeine)caffeine.getIfAvailable(), cacheManager);
	cacheLoader.ifAvailable(cacheManager::setCacheLoader);
	return cacheManager;
}

当然我们可以自己配置一个缓存管理器的Bean,那么自动配置类CaffeineCacheConfiguration就不会生效,让其内部维护CaffeineCache即可。此处配置CaffeineCacheManager的Bean,如下:

@EnableCaching
@Configuration
public class CacheConfiguration {
    /**
     * 配置缓存管理器
     */
    @Bean("caffeineCacheManager")
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .expireAfterAccess(20, TimeUnit.SECONDS)
                .initialCapacity(10)
                .weakValues()
                .maximumSize(100));
		cacheManager.setCacheLoader(new CacheLoader<Object, Object>() {
            @Override
            public Object load(Object o) throws Exception {
                //从redis中读取缓存项,如果redis中没有则返回null,null不会被缓存。
                // 那么就会执行@Cacheable注解的方法,从DB中读
                return null;
            }
        });
        return cacheManager;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值