本地缓存 - LoadingCache

本地缓存

面试经常会被问到如何解决缓存击穿问题,今天就来带你弄懂他!平时业务中也会经常使用到本地缓存,公司里使用比较多的本地缓存 loadingcache,其背后的架构就是Guava cache,Guava Cache 是一个全内存的本地缓存实现,它提供了线程安全的实现机制。 整体上来说Guava Cache 是本地缓存的不二之选。

适用场景

  1. 适合少量热点数据缓存(受限于内存大小),解决缓存击穿问题, 可以使用LRU作为淘汰缓存策略。

  2. 愿意以空间换时间,缓存数据到本地内存(没有网络IO,速度快)

  3. 允许在重新load前读到的是脏数据(对同一数据一直访问, 且间隔小于失效时间, 则不会去load数据)

  4. 可以监听Entry清除状态

  5. 支持缓存命中情况统计

2. 使用方法

2.1 使用方式

<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>24.1-jre</version>
</dependency>
	private final LoadingCache<Long, Entity> entityCache = CacheBuilder.newBuilder()// 缓存池大小,在缓存数量到达该大小时, 开始回收旧的数据.maximumSize(10000)// 设置时间10s对象没有被读/写访问则对象从内存中删除.expireAfterAccess(10, TimeUnit.SECONDS)// 设置缓存在写入之后 设定时间10s后失效.expireAfterWrite(10, TimeUnit.SECONDS)// 定时刷新,设置时间5s后,当有访问时会重新执行load方法重新加载.refreshAfterWrite(5, TimeUnit.SECONDS)// 移除监听器,缓存项被移除时会触发.removalListener(new RemovalListener() {@Overridepublic void onRemoval(RemovalNotification rn) {// 处理缓存键不存在缓存值时的**移除**处理逻辑log.error(rn.getKey() + "remove");}})// 处理缓存键对应的缓存值不存在时的处理逻辑.build(new CacheLoader<Long, Entity>() {@Overridepublic Entity load(Long id) {return EntityService.getById(id);}});public Entity getEntity(Long id) {Entity entity = entityCache.get(id);}public ImmutableMap<Long, Entity> getAll(List<Long> ids) throws ExecutionException {return cache.getAll(ids);}

2.2 常用参数

参数

说明

注意事项

maximumSize

缓存的k-v最大数据,当总缓存的数据量达到这个值时,就会淘汰它认为不太用的一份数据,会使用LRU策略进行回收

expireAfterAccess

缓存项在给定时间内没有被读/写访问,则回收,这个策略主要是为了淘汰长时间不被访问的数据

数据过期不是立即淘汰,而是有数据访问时才会触发

expireAfterWrite

缓存项在给定时间内没有被写访问(创建或覆盖),则回收, 防止旧数据被缓存过久

同上

refreshAfterWrite

缓存项在给定时间内没有被写访问(创建或覆盖),则刷新

同上

recordStats

开启Cache的状态统计(默认是开启的)

可能会影响到性能

2.3 显示清除

  • 单个清除:Cache.invalidate(key)

  • 批量清除:Cache.invalidate(keys)

  • 清除所有:Cache.invalidateAll()

3. LoadingCache解析

3.1 数据结构

底层数据结构是一个K.V的存储结构,这个图我想应很明显了,这分明就就是ConcurrentHashMap的结构,底层是一个segment数组,链表的节点和ConcurrentHashMap不太一样,是一个每一个segment是一个节点为ReferenceEntry<K, V>数组,segment继承了ReentrantLock,缩小了锁的力度,体现了分段式锁的思想。

3.2 Get方法

  V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {try {if (count != 0) { // read-volatileReferenceEntry<K, V> e = getEntry(key, hash);if (e != null) {long now = map.ticker.read();//检查entry是否符合expireAfterAccess淘汰策略V value = getLiveValue(e, now);// value是有效的 则返回if (value != null) {// 记录该值的最近访问时间recordRead(e, now);statsCounter.recordHits(1);// 内部实现了定时刷新,若未开启refreshAfterWrite则直接返回valuereturn scheduleRefresh(e, key, hash, value, now, loader);}ValueReference<K, V> valueReference = e.getValueReference();// 如果有别的线程已经在load value,则等到其他线程完成后再取结果if (valueReference.isLoading()) {return waitForLoadingValue(e, key, valueReference);}}}// 如果没拿到有效的value,则执行加载逻辑;return lockedGetOrLoad(key, hash, loader);} catch (ExecutionException ee) {...} finally {postReadCleanup();}}

先获取未过期的值(指内存中已经存在的,未符合expireAfterAccess淘汰策略),recordRead方法则是记录该值的最近访问时间,然后判断执行scheduleRefresh方法。

这个方法里先是判断是否设置了refreshAfterWrite属性,并判断当前时间是否符合刷新策略。符合则调用refresh进行刷新操作

3.3 load方法

@GwtCompatible(emulated = true)
public abstract class CacheLoader<K, V> {public abstract V load(K key) throws Exception;}

key对应的value不存在(或已过期)会触发load方法。

load方法是同步的,对于同一个key,多次请求只会触发一次加载。

在Thread1进行load加载完成之前,这些请求线程都会被hang等待。

3.4 reload方法

  • //  Guava的默认实现是同步的
    public ListenableFuture<V> reload(K key, V oldValue) throws Exception {checkNotNull(key);checkNotNull(oldValue);return Futures.immediateFuture(load(key));
    }

  • 当cache中有值,但需要刷新该值的时候会触发reload方法。

  • LoadingCache的所有更新操作都是依靠读写方法触发的,因为其内部没有时钟或者定时任务。比如上一次写之后超过了refresh设置的更新时间,但之后没有cache的访问了,那么下次get的时候才会触发refresh。

  • 对于同一个key,多次请求只会有一个线程触发reload,其他请求线程直接返回旧值。

3.5 CacheLoader

同步模式,会阻塞用户请求线程。

new CacheLoader<Long, Entity>() {@Overridepublic Entity load(Long entityId) {return EntityService.getById(entityId);}} 

3.6 AyncReloadCacheLoader

根据Guava的API实现的异步CacheLoader,refresh操作不堵塞任何一个用户请求线程。

相对于Guava中默认实现的reload,只减少了“一个”线程的阻塞。

 
/*** 这个类只是改写了reload方法,配合refreshAfterWrite异步刷新* 避免因为使用expireAfterWrite造成缓存miss时请求线程回流影响用户请求* <p>* 代价就是一个额外的线程调度更新** @author w.vela*/
public abstract class AsyncReloadCacheLoader<K, V> extends CacheLoader<K, V> {/*** <WARNING> 请务必要覆盖这个名字,不然有人会不开心的……*/protected String statsName() {return "unknown_async_cache_reloader";}@Overridepublic ListenableFuture<V> reload(K key, V oldValue) throws Exception {ListenableFutureTask<V> task = create(() -> load(key));ExecutorHolder.execute(statsName(), task::run);return task;}
}

此类实现不推荐使用,存在一些问题:

共用一个全局线程池,线程池不为使用者所感知,不同使用方可能相互影响;

集中大量发生reload是出现频繁线程创建和销毁。

推荐替代方式:

直接使用CacheLoader,override reload方法,提供自己的异步实现。异步实现可以使用支持异步调用的API(如直接使用grpc异步)

如果没有异步调用API可以自己提供一个线程池用来做异步化。

3.7 BatchReloadCacheLoader

如果有大量集中refresh的情况,可以使用BatchReloadCacheLoader 批量处理

相比于AyncReloadCacheReloader,优点在于:使用 BufferTrigger(本地归并消费)将单个 cache refresh 操作聚合成为批量 refresh,减少线程上下文切换,提升效率。BufferTrigger中会有一个额外线程去真正执行load操作,所以不会堵塞用户请求线程。

private final LoadingCache<Long, Entity> entityCache = KsCacheBuilder.newBuilder().maximumSize(10000).expireAfterAccess(5, TimeUnit.SECONDS).enablePerf(perfName) .buildBatchReload(new CacheLoader<Long, Entity>() {@Overridepublic Entity load(Long id) {return EntityService.getById(id);}});

com.kuaishou.framework.concurrent.BatchReloadCacheLoader

    private void doBatchReload(Queue<ReloadTask<K, V>> tasks) {Stopwatch stopwatch = Stopwatch.createStarted();Multimap<K, SettableFuture<V>> futureMap = tasks.stream().collect(toMultimap(ReloadTask::getKey, ReloadTask::getFuture, ArrayListMultimap::create));try {Map<K, V> result = loadAll(futureMap.keySet());futureMap.forEach((key, future) -> future.set(result.get(key)));} catch (UnsupportedLoadingOperationException e) {futureMap.forEach((key, future) -> {try {future.set(load(key));} catch (Throwable e2) {future.setException(e2);rateLogger.warn("cache reload fail, biz:{}", bizName, e);}});} catch (Throwable e) {tasks.forEach(task -> task.getFuture().setException(e));rateLogger.warn("cache reload fail, biz:{}", bizName, e);} finally {perf("batchReload", stopwatch.elapsed());}}

4.思考与总结

4.1 本地缓存是一个被动更新的过程

缓存在未失效的情况下,确实是保证了其可用性,却很难保证数据的正确性,传统意义上,需要等 缓存数据过期,命中缓存失败,才去DB中更新数据,导致缓存内的数据不是最新的数据,如果缓存的过期时间过长,数据的不一致的风险就越高。

如果想要及时的保证缓存与DB数据一致的话,另一种就是监听binlog,当DB中的数据发生变化的时候,主动触发ReloadableCache去更新缓存。

4.2 小心外部接口调用超时

load操作,如果是调用外部接口, 接口RT变慢的情况, 会导致链路load调用 hang住

可以设置超时时间, 配置降级策略

4.3 refreshTime一定要小于expiredTime

Guava Cache 并没使用额外的线程去做定时清理和加载的功能,而是依赖于查询请求。在查询的时候去比对上次更新的时间,如超过指定时间则进行回源。

是先判断过期,再判断refresh,如果refreshTime 大于 expiredTime, 会直接返回旧值, 在另外一个线程再去reload

所以我们可以通过设置refreshAfterWrite为1s,将expireAfterWrite设为2s,当访问频繁的时候,会在每秒都进行refresh,而当超过2s没有访问,下一次访问必须load新值。


 

参考资料:

《本地缓存-loadingCache》

https://blog.csdn.net/String_guai/article/details/121109056

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/318894.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

李沐机器学习系列2--- mlp

1 Introduction LP中有一个很强的假设&#xff0c;输入和输出是线性关系&#xff0c;这一般是不符合事实的。 通过几何的方式去对信息进行理解和压缩是比较高效的&#xff0c;MLP可以表示成下面的形式。 1.1 从线性到非线性 X ∈ R n d X \in R^{n \times d} X∈Rnd表示输入…

分布微服软件体系快速云端架构

1 概述 分布微服软件体系云端架构平台&#xff0c;以主流的NACOS服务器作为注册配置中心&#xff0c;采用主流的Gradle框架&#xff0c;内嵌Tomcat10以上版本&#xff0c;用于快速构造各类基于JDK17以上的信息应用系统的分布式微服务软件体系架构&#xff0c;可以适用关系型SQ…

Unity ShaderGraph 技能冷却转圈效果

Unity ShaderGraph 技能冷却转圈效果 前言项目场景布置代码编写ShaderGraph 连线总结 参考 前言 遇到一个需求&#xff0c;要展示技能冷却的圆形遮罩效果。 项目 场景布置 代码编写 Shader核心的就两句 // 将uv坐标系的原点移到纹理中心 float2 uv i.uv - float2(0.5, 0…

芯课堂 | MCU之TIMER精准延时

引言 华芯微特公司SWM系列单片机提供的TIMER个数和功能有些微差别&#xff0c;为了让您更加简单的使用这一功能&#xff0c;下面小编将以SWM190为例&#xff0c;给大家展示如何使用SWM系列产品的TIMER功能。 TIMER精准延时 一、TIMER简介 TIMER是一种定时器工具&#xff0c;…

【零基础入门TypeScript】TypeScript - 环境设置

目录 本地环境设置 文本编辑器 TypeScript 编译器 安装 Node.js 在 Windows 上安装 在 Mac OS X 上安装 IDE支持 视觉工作室代码 在 Windows 上安装 在 Mac OS X 上安装 在 Linux 上安装 括号 括号的 TypeScript 扩展 var message:string "Hello World"…

力扣hot100 二叉树的层序遍历 BFS 队列

&#x1f468;‍&#x1f3eb; 题目地址 时间复杂度&#xff1a; O ( n ) O(n) O(n)空间复杂度&#xff1a; O ( n ) O(n) O(n) &#x1f60b; 队列写法 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode…

工业物联网上篇——什么是IIOT?

工业物联网背后的理念是使用工业设施中“哑巴设备”多年来产生的数据。装配线上的智能机器不仅可以更快地捕获和分析数据&#xff0c;且在交流重要信息方面也更快&#xff0c;这有助于更快、更准确地做出业务决策。 信息技术&#xff08;IT&#xff09;和运营技术&#xff08;O…

网络安全—IPSec安全策略

文章目录 网络拓扑添加策略ESP添加筛选器添加筛选器的操作另一台主机设置 AH 使用Windows Server 2003系统 网络拓扑 client1 IP 192.168.17.105client2 IP 192.168.17.106 只要保证两个主机在同一网段接口&#xff0c;即互相ping通即可完成策略的实现 下面的所有通讯都只是…

【Qt之Quick模块】6. QML语法详解_4 集成QML和JavaScript

QML语法详解_4 集成QML和JavaScript 1. JavaScript表达式和属性绑定1.1 属性绑定 1.2 JavaScript函数2. 从JavaScript动态创建QML对象2.1 动态创建对象2.1.1 Qt.createComponent()动态创建组件2.1.2 Qt.createQmlObject()从QML字符串创建对象 2.2 维护动态创建的对象2.3 动态删…

go执行静态二进制文件和执行动态库文件

目的和需求&#xff1a;部分go的核心文件不开源&#xff0c;例如验证&#xff0c;主程序核心逻辑等等 第一个想法&#xff0c;把子程序代码打包成静态文件&#xff0c;然后主程序执行 子程序 package mainimport ("fmt""github.com/gogf/gf/v2/os/gfile"…

git在本地创建dev分支并和远程的dev分支关联起来

文章目录 git在本地创建dev分支并和远程的dev分支关联起来1. 使用git命令2. 使用idea2.1 先删除上面建的本地分支dev2.2 通过idea建dev分支并和远程dev分支关联 3. 查看本地分支和远程分支的关系 git在本地创建dev分支并和远程的dev分支关联起来 1. 使用git命令 git checkout…

DrGraph原理示教 - OpenCV 4 功能 - 颜色变幻

二值化是逐像素处理&#xff0c;而逐像素处理会有很多效果&#xff0c;这主要是给人眼看的&#xff0c;因为像素值的变化&#xff0c;直观的就是图像变化&#xff0c;比如颜色。 颜色变幻处理 OpenCV提供了一些图片&#xff0c;如下&#xff1a; 粗看是一些风格&#xff0c;…