spring boot 整合caffeine

概要

Caffeine是使用Java8对Guava缓存的重写版本,在Spring Boot 2.0中将取代Guava。如果出现Caffeine,CaffeineCacheManager将会自动配置。caffeine是目前最高性能的java本地缓存库。
github:

https://github.com/ben-manes/caffeine

caffeine介绍

1. 填充策略

1.1 手动加载

String caffeineSpec = "initialCapacity=50,maximumSize=500,refreshAfterWrite=6s";
CaffeineSpec spec = CaffeineSpec.parse(caffeineSpec);
Caffeine caffeine = Caffeine.from(spec);
Cache manualCache = caffeine.build();
// 通过key查询value,没有返回null
manualCache.getIfPresent("key");
// 通过key查询value,没有根据传入的function初始化这个key
manualCache.get("key", k -> load(k));
// 将key及对应的value放入缓存
manualCache.put("key", "value");
// 删除key对应的mapping
manualCache.invalidate("key");

通过手动加载你可以显式的去查询、更新、删除一个缓存,Caffeine 的创建方式除了上述方式,还可通过以下方式创建

Caffeine caffeine = Caffeine.newBuilder()
		 .expireAfterWrite(1, TimeUnit.MINUTES)
		 .maximumSize(100)

1.2 同步加载

String caffeineSpec = "initialCapacity=50,maximumSize=500,refreshAfterWrite=6s";
CaffeineSpec spec = CaffeineSpec.parse(caffeineSpec);
Caffeine caffeine = Caffeine.from(spec);
// CacheLoader cacheLoader = key -> load(key);
CacheLoader cacheLoader = new CacheLoader() {
    @CheckForNull
    @Override
    public Object load(@Nonnull Object key) throws Exception {
        return load(key);
    }
};
LoadingCache loadingCache = caffeine.build(cacheLoader);
// 通过key查询value,没有则调用load方法初始化这个key
loadingCache.get("key");
List<String> keyList = Arrays.asList("key1", "key2");
loadingCache.getAll(keyList);

LoadingCache 通过指定一个 CacheLoader 来构建一个之前不存在的缓存,通过get方法获取缓存时调用load方法来初始化,我们也可以通过getAll批量获取缓存,默认情况下,getAll将会对缓存中没有值的key分别调用load方法,通过重写CacheLoader 中的loadAll方法提高效率。

1.3 异步加载

String caffeineSpec = "initialCapacity=50,maximumSize=500,refreshAfterWrite=6s";
CaffeineSpec spec = CaffeineSpec.parse(caffeineSpec);
Caffeine caffeine = Caffeine.from(spec);
// CacheLoader cacheLoader = key -> load(key);
CacheLoader cacheLoader = new CacheLoader() {
    @CheckForNull
    @Override
    public Object load(@Nonnull Object key) throws Exception {
        return load(key);
    }
};
AsyncCacheLoader asyncCacheLoader = new AsyncCacheLoader() {
    @Nonnull
    @Override
    public CompletableFuture asyncLoad(@Nonnull Object key, @Nonnull Executor executor) {
        return asyncLoad(key);
    }
};
AsyncLoadingCache asyncLoadingCache = caffeine.buildAsync(cacheLoader);
// 通过key查询value,没有则调用load方法初始化这个key
asyncLoadingCache.get("key").thenAccept(value -> handle(value));
List<String> keyList = Arrays.asList("key1", "key2");
asyncLoadingCache.getAll(keyList).thenAccept(value -> handle(value));

AsyncLoadingCache 与 LoadingCache 是两个完全独立的接口,caffeine通过调用buildAsync来创建异步的cache,buildAsync有两个重载的方法
在这里插入图片描述
CacheLoader 继承自 AsyncCacheLoader,两种cacheLoader的用法暂时还没有比较清晰的认识,异步加载使用Executor去调用方法并返回一个CompletableFuture,我们拿到这个CompletableFuture可以对其做我们需要的操作。
异步加载默认使用ForkJoinPool.commonPool()来执行异步线程,我们可以通过Caffeine.executor(Executor) 方法来替换线程池。

2. 驱逐策略

2.1 基于大小回收

CacheLoader cacheLoader = new CacheLoader() {
     @CheckForNull
     @Override
     public Object load(@Nonnull Object key) throws Exception {
         return load(key);
     }
 };
// 根据缓存的数量进行驱逐
Caffeine.newBuilder().maximumSize(10).build(cacheLoader);
// 根据权重进行驱逐
Caffeine.newBuilder().maximumWeight(10)
        .weigher((key, value) -> Integer.valueOf(key.toString()))
        .build(cacheLoader);

基于大小的回收策略分为两种:

  • 当缓存的数量超过配置的缓存大小限制时会发生回收
  • 当缓存的总权重超过配置的权重大小限制时会发生回收,我们可以通过weigher方法来指定每个缓存权重的计算方式。

2.2 基于时间回收

// 访问后过期
Caffeine.newBuilder()
        .expireAfterAccess(5, TimeUnit.SECONDS)
        .build(cacheLoader);
// 写入后过期
Caffeine.newBuilder()
        .expireAfterWrite(5, TimeUnit.SECONDS)
        .build(cacheLoader);
//自定义过期策略
Caffeine.newBuilder().expireAfter(new Expiry<Object, Object>() {
    @Override
    public long expireAfterCreate(
            Object key, Object value, long currentTime) {
        return 10;
    }
    @Override
    public long expireAfterUpdate(
            Object key, Object value, long currentTime, long currentDuration) {
        return 10;
    }
    @Override
    public long expireAfterRead(
            Object key, Object value, long currentTime, long currentDuration) {
        return 10;
    }
}).build(cacheLoader);

基于时间的回收策略分为三种:

  • 访问后过期:自上次读或者写算起
  • 写入后过期:自上次写算起
  • 自定义:自定义过期策略
    expireAfterWrite和expireAfterAccess同时存在时,以expireAfterWrite为准。

2.3基于引用回收

Caffeine.newBuilder()
                .weakKeys()
                .weakValues()
                .build(cacheLoader);

关于强引用、软引用、弱引用、虚引用的详细描述可参考《深入理解jvm虚拟机》这本书,在caffeine中的应用就是你可以指定key、value为软引用,在jvm内存不足时进行回收。

3.刷新策略

CacheLoader cacheLoader = new CacheLoader() {
    @CheckForNull
    @Override
    public Object load(@Nonnull Object key) throws Exception {
        return load(key);
    }

    @CheckForNull
    @Override
    public Object reload(@Nonnull Object key, @Nonnull Object oldValue) throws Exception {
        return load(key);
    }
};
Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.SECONDS)
        .expireAfterAccess(10, TimeUnit.SECONDS)
        .build(cacheLoader);
Caffeine.newBuilder()
        .executor(new ThreadPoolExecutor(1, 1, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<>()));

我们可以通过expireAfterWrite跟expireAfterAccess来指定刷新时机,当两个参数同时指定时,数据将在具备刷新条件时才去刷新。
caffeine的刷新通过异步调用CacheLoader接口中的reload方法来实现,reload方法的默认实现是通过调用load方法来刷新,当然我们也可以通过重写reload方法来自定义刷新逻辑。
由于刷新是通过ForkJoinPool.commonPool()来异步调用,所以触发刷新的线程会直接拿到旧数据返回,我们可以使用executor指定的线程池替换ForkJoinPool.commonPool()。

spring boot + caffeine

1.引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.6.2</version>
</dependency>

2.配置caffeine

@Configuration
@EnableCaching
public class CaffeineConfig {

    private static final Logger logger = LoggerFactory.getLogger(CaffeineConfig.class);

    @Autowired
    private ConfigureParameter configureParameter;

    /**
     * caffeine CacheManager
     * @return
     */
    @Bean("caffeine")
    public CacheManager cacheManager() {
        String caffeineSpec = configureParameter.getCaffeineSpec();
        CaffeineSpec spec = CaffeineSpec.parse(caffeineSpec);
        Caffeine caffeine = Caffeine.from(spec);
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        cacheManager.setCaffeine(caffeine);
        cacheManager.setAllowNullValues(false);
        CacheLoader<Object, Object> cacheLoader = key -> null;
        cacheManager.setCacheLoader(cacheLoader);
        return cacheManager;
    }

}

在上例中我们通过caffeineSpec来构建caffeine对象,并且CacheLoade中的load方法我们直接返回了一个null,这是为了让spring去调用原方法,来初始化caffeine缓存,这个在之后的源码分析中会提到,我们并没有去重写reload方法,这时候默认去调用load方法,而load返回null,所以注意在异步刷新的时候caffeine会把这个null放置到caffeine缓存中,在caffeine中并没有找到控制value是否可为null的配置,但是spring为我们提供了一个方法setAllowNullValues,如果为false不允许value为null,则在value为null时会抛出一个异常。

我们也可以通过SimpleCacheManage的方式来让spring管理caffeine

/**
  * caffeine CacheManager
  * @return
  */
 @Bean("caffeine")
 public CacheManager cacheManager() {
     String groupInfo = Constant.GROUP_INFO;
     CacheLoader cacheLoader = cacheLoaderContext.getCacheLoader(groupInfo);
     SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
     List<CaffeineCache> cacheList = new ArrayList<>();
     String caffeineSpec = configureParameter.getCaffeineSpec();
     CaffeineSpec spec = CaffeineSpec.parse(caffeineSpec);
     Caffeine caffeine = Caffeine.from(spec);
     CaffeineCache caffeineCache = new CaffeineCache(groupInfo, caffeine.build(cacheLoader) , false);
     cacheList.add(caffeineCache);
     simpleCacheManager.setCaches(cacheList);
     return simpleCacheManager;
 }

CaffeineCache构造器的第三个参数是allowNullValues,不过这个参数我测试并没有限制到value是否允许为null

3.使用注解

@Cacheable(cacheNames = "cahceName", key = "#key", cacheManager = "caffeine", unless = "#result == null")

我们可以使用spring提供的@Cacheable、@CachePut、@CacheEvict等注解来方便的使用caffeine缓存。
如果使用了多个cahce,比如redis、caffeine等,必须指定某一个CacheManage为@primary,在@Cacheable注解中没指定cacheManager 则使用标记为primary的那个。

4.CacheAspectSupport

	private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
		// 是否异步 sync = true/false
		if (contexts.isSynchronized()) {
			CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
			if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
				Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
				Cache cache = context.getCaches().iterator().next();
				try {
					return wrapCacheValue(method, cache.get(key, new Callable<Object>() {
						@Override
						public Object call() throws Exception {
							return unwrapReturnValue(invokeOperation(invoker));
						}
					}));
				}
				catch (Cache.ValueRetrievalException ex) {
					// The invoker wraps any Throwable in a ThrowableWrapper instance so we
					// can just make sure that one bubbles up the stack.
					throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();
				}
			}
			else {
				// No caching required, only call the underlying method
				return invokeOperation(invoker);
			}
		}


		// Process any early evictions
		processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
				CacheOperationExpressionEvaluator.NO_RESULT);

		// 真正的去调用底层cache,获取value
		Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));

		// Collect puts from any @Cacheable miss, if no cached item is found
		List<CachePutRequest> cachePutRequests = new LinkedList<CachePutRequest>();
		if (cacheHit == null) {
			collectPutRequests(contexts.get(CacheableOperation.class),
					CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
		}

		Object cacheValue;
		Object returnValue;

		if (cacheHit != null && cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
			// If there are no put requests, just use the cache hit
			cacheValue = cacheHit.get();
			returnValue = wrapCacheValue(method, cacheValue);
		}
		// 这里如果底层cache的返回值为null,spring会去调用原方法
		else {
			// Invoke the method if we don't have a cache hit
			returnValue = invokeOperation(invoker);
			cacheValue = unwrapReturnValue(returnValue);
		}

		// Collect any explicit @CachePuts
		collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);

		// 并将获取到的value放置到底层cache中
		for (CachePutRequest cachePutRequest : cachePutRequests) {
			cachePutRequest.apply(cacheValue);
		}

		// Process any late evictions
		processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);

		return returnValue;
	}

CacheAspectSupport是spring提供的与三方cache整合的一个重要类,若想了解更详细的底层实现可以去阅读下源码。

猜你喜欢

转载自blog.csdn.net/qq_35532948/article/details/86524954
今日推荐