博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
java 缓存架构剖析–本地缓存(LoadingCache)
阅读量:3899 次
发布时间:2019-05-23

本文共 7545 字,大约阅读时间需要 25 分钟。

java 缓存架构剖析–本地缓存(LoadingCache)


目录

缓存的使用可以大大提高程序的执行效率,但是如果缓存无法及时更新会导致脏读的情况。

1 适用场景

缓存在很多场景下都是相当有用的。例如,计算或检索一个值的代价很高,并且对同样的输入需要不止一次获取值的时候,就应当考虑使用缓存。

``Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。在某些场景下,尽管LoadingCache 不回收元素,它也是很有用的,因为它会自动加载缓存。`

``通常来说,Guava Cache适用于:

  • 你愿意消耗一些内存空间来提升速度。
  • 你预料到某些键会被查询一次以上。
  • 缓存中存放的数据总量不会超出内存容量。(Guava Cache是单个应用运行时的本地缓存。它不把数据存放到文件或外部服务器。如果这不符合你的需求,请尝试这类工具)

如果你的场景符合上述的每一条,Guava Cache就适合你。

如同范例代码展示的一样,Cache实例通过CacheBuilder生成器模式获取,但是自定义你的缓存才是最有趣的部分。*注*:如果你不需要Cache中的特性,使用ConcurrentHashMap有更好的内存效率——但Cache的大多数特性都很难基于旧有的ConcurrentMap复制,甚至根本不可能做到。

1.1 实例理解

记得早期我呆过的一家公司有个核心服务是在启动的时候一下把常用的交易配置信息是从DB查出来放在Map里面来做缓存,先不考虑其他的,如果我想更新一下交易配置信息是不是需要每次都重启服务器呢,又或者说我开几个后门接口用来更新Map信息,这样不还得考虑线程安全的问题么。

好吧,我先上个在中小型项目中,乃至大型项目中也常用的缓存架构,如下:

img

内存架构图

我大概解释一下流程吧:

1、系统A中使用LoadingCache来维护本地缓存信息

2、当缓存刷新时(**同步、异步)**调用B系统来更新缓存信息

3、系统B接收A获取配置数据的请求,如果redis缓存中有数据就直接从redis中拿

4、当缓存中不存在请求则穿透到DB里面查询再将结果塞到redis,并返回结果

5、其实还有一步没画出来应该是有个定时job轮询DB配置信息变化时刷新redis信息(或者消息机制来实现缓存更新)

言归正传,下面来详解一下LoadingCache的使用:

2 LoadingCache的使用

详细使用示例见文档:

依赖:

com.google.guava guava 27.1-jre
public static LoadingCache
cahceBuilder = CacheBuilder.newBuilder() //缓存池大小,在缓存项接近该大小时, Guava开始回收旧的缓存项 .maximumSize(1) //对象没有被读/写访问设定时间后则对象从内存中删除(在另外的线程里面不定期维护) // .expireAfterAccess(GUAVA_CACHE_DAY, TimeUnit.DAYS) // 缓存在写入之后 设定时间 后失效 //.expireAfterWrite(10, TimeUnit.SECONDS) // 设置2ms自动定时刷新,当有访问时会重新执行load方法更新缓存 .refreshAfterWrite(2, TimeUnit.MILLISECONDS) //expireAfterWrite与refreshAfterWrite不能同时发生 //移除监听器,缓存项被移除时会触发 .removalListener(new RemovalListener() {
@Override public void onRemoval(RemovalNotification rn) {
// 处理缓存键不存在缓存值时的**移除**处理逻辑 System.out.println(rn.getKey() + "被移除"); } }) .build(new CacheLoader() {
@Override public String load(String key) throws Exception {
// 处理缓存键对应的缓存值不存在时的处理逻辑 //即当调用get取值: null 调用 load //默认的load实现return "null" String strProValue = "hello " + key + "!"; System.out.println("%%%%%" + strProValue); return strProValue; } }); public static void main (String[]args) throws ExecutionException, InterruptedException {
cahceBuilder.get("jerry"); cahceBuilder.get("peida"); Thread.sleep(1000); cahceBuilder.get("jerry1"); }

输出结果为:

%%%%%hello jerry! – 在第一次get的时候没有值会执行load方法,去取值然后塞到本地缓存

%%%%%hello peida! – 在第一次get的时候没有值会执行load方法,去取值然后塞到本地缓存

jerry被移除 – maximumSize(1) 最大值为1,当预存储第二个值的时候第一个值会被移除

%%%%%hello jerry1! – refreshAfterWrite设置2ms自动定时刷新,当有访问时会重新执行load方法更新缓存

peida被移除 – maximumSize(1) 最大值为1,当预存储第二个值的时候第一个值会被移除

3 基本特性

3.1 值操作:

get(K):这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地loading新值(就是上面说的当缓存没有值的时候执行Load方法)。

put(key, value):这个方法可以直接显示地向缓存中插入值,这会直接覆盖掉已有键之前映射的值。

Cache.asMap(): 使用Cache.asMap()视图提供的任何方法也能修改缓存。但请注意,asMap视图的任何方法都不能保证缓存项被原子地加载到缓存中。进一步说,asMap视图的原子运算在Guava Cache的原子加载范畴之外,所以相比于Cache.asMap().putIfAbsent(K, V)Cache.get(K, Callable<V>)应该总是优先使用。

3.2 缓存回收:

  1. 基于容量的回收

    • CacheBuilder.maximumSize(long):这个方法规定缓存项的数目不超过固定值(其实你可以理解为一个Map的最大容量),当容量超出指定值时缓存尝试回收最近没有使用或总体上很少使用的缓存项
  2. 定时回收(2种):

    • expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。

    • expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。

  3. 基于引用的回收

    通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收:

    • CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(),使用弱引用键的缓存用而不是equals比较键。
    • CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(),使用弱引用值的缓存用而不是equals比较值。
    • CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用==而不是equals比较值。

    需要注意:

    • 警告:在缓存项的数目达到限定值之前,缓存就可能进行回收操作。通常来说,这种情况发生在缓存项的数目逼近限定值时。另外,不同的缓存项有不同的“权重”(weights)——例如,如果你的缓存值,占据完全不同的内存空间,你可以使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大权重。
    • 测试定时回收:对定时回收进行测试时,不一定非得花费两秒钟去测试两秒的过期。你可以使用Ticker接口和CacheBuilder.ticker(Ticker)方法在缓存中自定义一个时间源,而不是非得用系统时钟。

3.3 显式清除:

任何时候,你都可以显式地清除缓存项,而不是等到它被回收:

  • 个别清除:

  • 批量清除:

  • 清除所有缓存项:

3.4 移除监听器

通过,你可以声明一个监听器,以便缓存项被移除时做一些额外操作。缓存项被移除时,会获取移除通知[],其中包含移除原因[]、键和值。

就如我上面的例子一样,当内存回收或者定时回收都会执行

不过亲测当有数据 refresh 刷新额度时候也会触发这个监听功能

!!! 警告:默认情况下,监听器方法是在移除缓存时同步调用的。因为缓存的维护和请求响应通常是同时进行的,代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求。在这种情况下,你可以使用把监听器装饰为异步操作

3.5 移除机制

guava做cache时候数据的移除分为被动移除和主动移除两种。

  1. 被动移除分为三种:

    1).基于大小的移除:数量达到指定大小(即maximumSize(MAX_SIZE)设置值),会把不常用的键值移除

    2).基于时间的移除:

    expireAfterAccess(long, TimeUnit)根据某个键值对最后一次访问之后多少时间后移除

expireAfterWrite(long, TimeUnit)根据某个键值对被创建或值被替换后多少时间移除

​ 3).基于引用的移除:主要是基于java的垃圾回收机制,根据键或者值的引用关系决定移除

  1. 主动移除分为三种:

    1).单独移除:Cache.invalidate(key)

    2).批量移除:Cache.invalidateAll(keys)

    3).移除所有:Cache.invalidateAll()

如果配置了移除监听器RemovalListener,则在所有移除的动作时会同步执行该listener下的逻辑。

如需改成异步,使用:RemovalListeners.asynchronous(RemovalListener, Executor)

3.6 刷新:

  1. :刷新和回收不太一样。刷新表示为键加载新值,这个过程可以是异步的。在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。如果刷新过程抛出异常,缓存将保留旧值,而异常会在记录到日志后被丢弃[swallowed]。

    重载可以扩展刷新时的行为,这个方法允许开发者在计算新值时使用旧的值。

  2. :可以为缓存增加自动定时刷新功能。和expireAfterWrite相反,refreshAfterWrite通过定时刷新可以让缓存项保持可用,但请注意:

    • 缓存项只有在被检索时才会真正刷新,即只有刷新间隔时间到了你再去get(key)才会重新去执行Loading否则就算刷新间隔时间到了也不会执行loading操作。因此,如果你在缓存上同时声明expireAfterWriterefreshAfterWrite,缓存并不会因为刷新盲目地定时重置,如果缓存项没有被检索,那刷新就不会真的发生,缓存项在过期时间后也变得可以回收。
    • 还有一点比较重要的是和expireAfterWrite两个方法设置以后,重新get会引起loading操作都是同步串行的。这其实可能会有一个隐患,当某一个时间点刚好有大量检索过来而且都有刷新或者回收的话,是会产生大量的请求同步调用loading方法,这些请求占用线程资源的时间明显变长。如正常请求也就20ms,当刷新以后加上同步请求loading这个功能接口可能响应时间远远大于20ms。为了预防这种井喷现象,可以不设置,改用因为它是异步执行的,不会影响正在读的请求,同时使用可以帮助你很好地实现这样的定时调度,配上cache.asMap().keySet()返回当前所有已加载键,这样所有的key定时刷新就有了。*

4 其他特性

4.1 统计

CacheBuilder.recordStats()用来开启Guava Cache的统计功能。统计打开后,Cache.stats()方法会返回CacheStats对象以提供如下统计信息:

  1. hitRate():缓存命中率;

  2. averageLoadPenalty():加载新值的平均时间,单位为纳秒;

  3. evictionCount():缓存项被回收的总数,不包括显式清除

此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议密切关注这些数据。

4.2 asMap视图

asMap视图提供了缓存的ConcurrentMap形式,但asMap视图缓存的交互需要注意:

  1. cache.asMap()包含当前所有加载到缓存的项。因此相应地,cache.asMap().keySet()包含当前所有已加载键;

  2. asMap().get(key)实质上等同于cache.getIfPresent(key),而且不会引起缓存项的加载。这和Map的语义约定一致。

所有读写操作都会重置相关缓存项的访问时间,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合视图上的操作。比如,遍历Cache.asMap().entrySet()不会重置缓存项的读取时间。

4.3 可能遇到的问题

1). 在put操作之前,如果已经有该键值,会先触发removalListener移除监听器,再添加

2). 配置了expireAfterAccessexpireAfterWrite,但在指定时间后没有被移除。

解决方案:CacheBuilder在文档上有说明

If expireAfterWrite or expireAfterAccess is requested entries may be evicted on each cache modification, on occasional cache accesses, or on calls to Cache.cleanUp(). Expired entries may be counted in Cache.size(), but will never be visible to read or write operations.

翻译过来大概的意思是:CacheBuilder构建的缓存不会在特定时间自动执行清理和回收工作,也不会在某个缓存项过期后马上清理,它不会启动一个线程来进行缓存维护,因为

a)线程相对较重

b)某些环境限制线程的创建。它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做。

当然,也可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp()

4.5 清理什么时候发生?

​ 使用CacheBuilder构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。相反,它会在写操作时顺带做少量的维护工作,或者偶尔在读操作时做——如果写操作实在太少的话。

​ 这样做的原因在于:如果要自动地持续清理缓存,就必须有一个线程,这个线程会和用户操作竞争共享锁。此外,某些环境下线程创建可能受限制,这样CacheBuilder就不可用了。

​ 相反,我们把选择权交到你手里。如果你的缓存是高吞吐的,那就无需担心缓存的维护和清理等工作。如果你的 缓存只会偶尔有写操作,而你又不想清理工作阻碍了读操作,那么可以创建自己的维护线程,以固定的时间间隔调用。可以帮助你很好地实现这样的定时调度。


参考:

转载地址:http://zzden.baihongyu.com/

你可能感兴趣的文章
js中时钟的写法
查看>>
js事件冒泡
查看>>
京东金融曹鹏:通过JDD大赛,实现“比你更懂你”的极致价值,让金融更简单,更平等
查看>>
HTML我的家乡杭州网页设计作业源码(div+css)~ HTML+CSS网页设计期末课程大作业 ~ web前端开发技术 ~ web课程设计网页规划与设计 ~HTML期末大作业
查看>>
HTML网页设计期末课程大作业~动漫樱桃小丸子5页表格div+css学生网页设计作业源码
查看>>
HTML学生网页设计作业成品~化妆品官方网站设计与实现(HTML+CSS+JS)共8个页面
查看>>
web课程设计网页规划与设计~在线阅读小说网页共6个页面(HTML+CSS+JavaScript+Bootstrap)
查看>>
HTML期末大作业~棋牌游戏静态网站(6个页面) HTML+CSS+JavaScript
查看>>
XmlValidationModeDetector源码分析
查看>>
解析 xml 为Document
查看>>
中国银行2013年校园招聘机试回忆录(综合部分专业题 考点)
查看>>
广发银行2013校园招聘笔试回忆录
查看>>
Android canvas rotate():平移旋转坐标系至任意原点任意角度-------附:android反三角函数小结...
查看>>
Matlab读取avi视频并播放 你必须要知道的
查看>>
word字体大小与公式编辑器字体对照表
查看>>
visio画图-----如何克服两箭头交叉变形 及 箭头自动重绘?
查看>>
Android开发:安装NDK,移植OpenCV2.3.1,JNI调用OpenCV全过程
查看>>
“金9银10”2020年JVM高频率面试题整理,技术提升就差一个点!
查看>>
简简单单的分享2020常见的MySQL面试题MySQL与答案整理
查看>>
听说只有大厂的Android工程师才能全答对这20道题?我看你在吹牛哦!
查看>>