• guava缓存


    guava缓存介绍:

    guava缓存是谷歌开源的一种本地缓存,缓存是使用本机的内存来存储的,实现原理类似于ConcurrentHashMap

    实现原理

    看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的方式写起来更简单。

    为什么要用缓存

    为了系统的高并发,高性能,提高访问速度

    应用

    //实例化缓存构建器 CacheBuildercacheBuilder=CacheBuilder.newBuilder(); //构建缓存容器 Cache<String,String>cache=cacheBuilder.build(); //缓存数据 cache.put(“cache”,“cache-value”); //获取缓存数据 Stringvalue=cache.getIfPresent(“cache”); //删除缓存 cache.invalidate(“cache5”);

    参数

    maximumSize
    设置构建缓存容器的最大容量,当缓存数量达到该容量时,会删除其中的缓存(根据实际测试结果,会先删除先放入的数据)
    concurrencyLevel
    设置并发等级,也就是同时操作缓存的线程数。默认是4。在guava cache的实现中,会根据这个值创建一个希表段,每个哈希表段由其自己的写锁控制。每次显式写入都会使用一次段锁,每次缓存加载计算都会使用两次段锁(一次在加载新值之前,一次在加载完成之后),许多内部缓存管理是在段粒度上执行的。
    expireAfterWrite
    缓存写入多久后过期,这个方法和我们前面分享的Caffeine是一样的。
    expireAfterAccess
    访问后多久过期,这个方法也和Caffeine是一样的。

    refreshAfterWrite
    缓存写入多久后刷新,这个方法也和Caffeine的一样,这几个方法应该就是Caffeine文档中所说的设计借鉴吧。

    使用场景

    消耗内存空间来提升速度(时间换空间)
    某些键会呗查询多次
    缓存中存放的数据总量不会超出内存的容量

    回收触发时机,缓存过期

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

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

    并发问题

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

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

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

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

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

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

  • 相关阅读:
    779. 第K个语法符号
    11.10 知识总结(数据的增删改查、如何创建表关系、Django框架的请求生命周期流程图)
    SpringBoot 2.18升级到2.7.5, 踩到的坑
    Java8 CompletableFuture runAsync等使用学习总结 submit() execute()等
    postgres源码解析56 Brin Index--3(update/delete/insert流程)
    Python潮流周刊#8:Python 3.13 计划将解释器提速 50%!
    Redux Toolkit中action派发但state值不更新的原因
    直播视频录制技巧,分享2个实用的方法
    txt文件恢复如何操作?详细图文教程
    通过shell批量更新多台linux上jar包
  • 原文地址:https://blog.csdn.net/zw147258369/article/details/125484533