背景
接口优化,在微服务中调用了另一个服务的接口,这个接口提供一个类似词典的基础数据服务,信息更新又不频繁,对实时性要求不高,如果每次直接访问都去调用一次性能很差,而接口的底层还是每次去 DB 捞一次数据(理论上应该对这个接口进行优化,考虑到要怀疑第三方的态度,还是需要这种方法保护自己的服务不要因为依赖外部资源而导致的宕机),所以考虑对这个接口做一个 cache,理论上就可以大幅度提升接口性能了。
要解决的问题
- 当缓存结果到期后,如果同时多个并发请求过来,如何避免这些请求都会重新去调用远程服务(读取 DB )来刷新缓存?如何确保只有一个请求线程会去真实的去调用远程服务(读取 DB),其他请求直接返回老值或等待返回结果?
- 另一个问题就是更新线程还是会被阻塞,可能还会使响应时间变得不满足要求。
- 当缓存 key 集体过期时,可能还会使响应时间变得不满足要求。
解决方案
缓存框架 guava cache
针对以上问题有相应的解决方案,分三步:
- 设置
refreshAfterWrite
缓存过期策略,在guava cache
中设置缓存过期的策略有refreshAfterWrite
和expireAfterWrite
,两者的区别是expireAfterWrite
到期会直接删除缓存,如果同时多个并发请求过来,这些请求都会重新去刷新缓存,会造成线程的阻塞。而refreshAfterWrite
,则不会删除缓存,只有一个请求线程会去执行缓存更新,其他请求直接返回老值。这样可以避免同时过期时大量请求被阻塞,从而提升性能。 - 当刷新过期时,开启一个新线程异步刷新,请求直接返回旧值,防止耗时过长。具体做法就是在
reload
方法里提交一个新的线程,就可以用这个线程来刷新cache
了,如果刷新cache
没有完成的时候有其他线程来请求该key
,则会直接返回旧值。 - 上面两步完成了不阻塞刷新缓存的功能,如果项目刚启动的时候,所有的缓存都是不存在,这个时候如果处理大批量请求过来,同样会被阻塞,因为没有旧的值供返回,都得等待缓存的第一次执行
load
完毕。所以,解决这个问题的方法就是在项目启动的过程中,将所有的缓存预先初始化完成,这样用户请求直接读缓存,不用等待缓存的第一次执行load
。
总结:设置 refreshAfterWrite
缓存过期策略 ——> 后台线程异步刷新 ——> 初始化缓存
代码示例
package com.gemantic.finance.dw.repository; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.annotation.Resource; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * @author Yezhiwei * 2020/11/10 */ @Component @Slf4j public class DictColumnWithCache { // 外部服务 @Resource private MetadataThemeRepository metadataThemeRepository; /** * 对外暴露的方法 */ public List<DictColumn> getDictColumn() throws ExecutionException { return cache.get("DictColumn"); } protected List<DictColumn> getDictColumnFromDb() { ResponseEntity<Response<List<DictColumn>>> responseResponseEntity = metadataThemeRepository.selectAllColumn(); List<DictColumn> data = responseResponseEntity.getBody().getData(); return data; } ListeningExecutorService refreshPools = MoreExecutors.listeningDecorator(new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>())); LoadingCache<String, List<DictColumn>> cache = CacheBuilder.newBuilder() .refreshAfterWrite(30, TimeUnit.MINUTES) .build(new CacheLoader<String, List<DictColumn>>() { @Override // 当本地缓存命没有中时,调用load方法获取结果并将结果缓存 public List<DictColumn> load(String appKey) { log.info("------------------- load newValue"); return getDictColumnFromDb(); } @Override // 后台线程刷新,开启一个新线程异步刷新,老请求直接返回旧值,防止耗时过长 public ListenableFuture<List<DictColumn>> reload(String key, List<DictColumn> oldValue) throws Exception { log.info("=================== return oldValue"); refreshPools.submit(() -> getDictColumnFromDb()); return Futures.immediateFuture(oldValue); } }); @PreDestroy public void destroy() { try { refreshPools.shutdown(); } catch (Exception e) { log.error("thread pool showdown error ", e); } } /** * 初始化缓存 */ @PostConstruct public void initDictColumnWithCache() { try { getDictColumn(); } catch (Exception e) { log.error("init dictColumn with cache error ", e); } } }
合理的使用缓存提升接口性能。
如果觉得还有帮助的话,你的关注和转发是对我最大的支持,O(∩_∩)O: