guava cache缓存组件在并发场景下的问题

前言

去年面蚂蚁的时候,二面提到自己项目里使用了guava cache来缓存一些高频读的配置文件,被问到了它的实现以及并发场景下可能存在什么问题,因为没有深入了解过,所以没回答上来。现在想来除了技术上的问题之外,也反映了当时我个人的思考不够的问题:
1 在对基础组件原理完全不了解的情况下就盲目使用,是否会因此带来一些隐藏的坑;
2 为什么没有抽时间去对用到的技术栈做一下简单的了解和调研,做出这样的技术选型的背景和理由根本讲不出来。

常常说要积累技术的广度和深度,就是应该要在这些地方多思考才行。想起之前一个组长说过的话,即使这些思考方式都是套路,都是所谓的方法论,但确实也是有所帮助的。

于是,终于在前段时间有空了解了一下,发现里面确实有很多东西值得学习,当初自己挂得也不算冤,于是打算写出来记录一下。

初步了解

篇幅问题这篇文章可能不会对guava cache整体实现的细节描写太多。所以这里只是简单介绍一下。

功能

guava cache提供的功能还是挺多的,参考美团技术博客里提到的:

  • 自动将entry节点加载进缓存结构中;
  • 当缓存的数据超过设置的最大值时,使用LRU算法移除;
  • 具备根据entry节点上次被访问或者写入时间计算它的过期机制;
  • 缓存的key被封装在WeakReference引用内;
  • 缓存的Value被封装在WeakReference或SoftReference引用内;
  • 统计缓存使用过程中命中率、异常率、未命中率等统计数据。

这几点功能在这篇文章里也都大概的会提到一些。

数据结构

看guava cache的代码可以发现,他的实现是和java老版本的concurrentHashMap有点相似的。
在localcache这个类中,实现了核心的数据结构和算法。继承abstractMap,实现concurrentMap接口,持有一个segment数组,segment继承自reentrantLock,有keyRederenceQueue队列,valueReferenceQueue队列,用于快速回收。recencyQueue用于记录访问了哪些节点来更新访问列表的顺序,在大于清除阈值或segment写时被清理;还有writeQueue按照写入时间排序,在写时添加到队尾;accessQueue按照访问时间排序,在访问时添加到队尾

ReferenceEntry数据结构用来记录节点数据,这种数据结构可以支持按照强引用或弱引用来生成key;recency队列用于实现lru算法;write和access这两个队列都是双向连表,用于实现最近最少使用算法。

使用方式

guava cache在创建时提供了两种创建方式

Loading cache

可以理解为在build cache的时候就定义好了缓存CacheLoader里的load方法,缓存中不存在时会去调用这个load方法加载数据

Callable cache

和loading cache不同的是,没有在build时传入CacheLoader,而是在使用get的时候传入一个callable对象来获取数据

这两种使用方式的原则都是在get操作时优先从缓存里读取数据,当数据不存在时通过cacheloader尝试加载数据写入缓存,callable cache的方式更灵活,loading cache的方式写起来更简单。

就不写示例代码来撑篇幅了。

使用场景
回收触发时机,缓存过期

按照以往对一些其他数据结构的了解,缓存过期策略的实现不外乎应该也是在读时判断是否超时,或者定时清理,鉴于这是个基础工具,应该不会去做定时清理这种重量级操作,所以应该还是在get的时候去判断是否超时,至于是根据写入时间还是根据读取时间,在这个框架里也是可以配置的,通过expireAfterWriteexpireAfterAccess

通过看代码发现,这其实是比较简单的,entry节点记录了accessTimewriteTime,在根据key get value的时候,实现了一个getLiveValue方法,在这个方法里判断过期策略,以及对比当前时间和节点过期时间,而这个时间分别是在写和读节点时通过recordWriterecordRead方法写入的。

并发问题

总算到了重点,那么并发情况下会有什么问题呢。cache本身的实现是线程安全的,那么并发下存在的问题可能就是加锁导致的阻塞了。

当尝试通过get获取缓存值的时候,如果不存在key或者key已经过期,这时候需要去执行load方法加载数据,为了保证线程安全,这个方法会对segment加锁,在这种锁粒度下,其他尝试获取这个key的线程就会被waitForValue方法阻塞,如果加载速度过慢就会有大量的线程被阻塞。

解决方式

其实关于并发问题的解决是有通用方法的,当看到guava cache解决这些问题的方法的时候,不自主的就想到了之前看过的一些其他代码和解决方案。读多写少,热key问题,这几个关键词就自己浮现了出来。随便提一个,并发安全的读多写少的数据结构还有一种是copyonwrite,通过读写分离来实现,写操作完成前读使用旧值。

在guava cache下,可以通过创建时配置refreshAfterWrite这个参数,定义在写入多久后刷新数据,在满足条件时只阻塞加载数据的线程,其余线程直接返回旧数据。

缓存雪崩

即使使用refreshAfterWrite这种配置方式,当多个缓存到达刷新条件时,如果同时对这些key进行请求,依然会有多个线程会被阻塞,造成大量线程阻塞,并且他们会同时去调用load方法请求资源,造成服务端的压力。

这时可以通过重写实现CacheLoader的reload方法,把加载数据的任务提交到线程池,这时用户请求不会被阻塞,会直接返回旧值,执行完成后而且线程池能够控制对资源的访问,不会对数据库等造成过大的压力。

结语

去年的这段时间是一段跌宕起伏的时间,去年一整年又是拥抱了巨大变化的一年,换了公司,换了城市,所以去年下半年的计划更多的是尝试稳定。不过已经到了新一年,希望自己在新的一年,能够对于周围的东西有更多的探索和思考,能够有所成长。

Guava Cache是一个全内存的本地缓存实现,是Guava工具包中的一个模块。它提供了简单易用且性能好的线程安全的本地缓存机制。Guava Cache适用于对性能有高要求、数据不经常变化、占用内存不大、需要访问整个集合、数据允许不时一致的场景Guava Cache的优势包括缓存过期和淘汰机制、并发处理能力、更新锁定、集成数据源以及监控缓存加载/命中情况等。在创建Guava Cache对象时,可以使用CacheLoader来自动加载数据到缓存中,也可以使用Callable Callback来在缓存中获取数据并回填缓存。要删除缓存数据,可以使用Cache的invalidate方法来实现。123 #### 引用[.reference_title] - *1* [Guava Cache本地缓存介绍及使用](https://blog.csdn.net/unbelievevc/article/details/128365002)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item] - *2* *3* [Guava Cache简介、应用场景分析、代码实现以及核心的原理](https://blog.csdn.net/weixin_44795847/article/details/123702038)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值