1、本地缓存

根据缓存是否与应用进程属于同一进程,可以将内存分为本地缓存和分布式缓存。相对于分布式缓存与应用进程部署在不同的机器,需要通过网络来完成缓存数据读写不同,本地缓存在同一个进程的内存空间中缓存数据,并完成数据读写过程。

1.1、本地缓存的使用场景

场景一:二级缓存

当服务访问量出现逾期外的陡增,可能会导致分布式缓存性能变差,甚至被击穿,大量请求将缓存服务打挂,从而影响正常业务功能。此时,可以将本地缓存用作二级缓存,降低服务的压力,提高访问速度,保障业务的稳定性。

场景二:常规流量

是指请求流量稳定,在业务系统中相对重要且访问频繁的热点数据。例如,内部员工手机号数据量不大且修改频率低、但线索量较大时,查询qps较高,是线索转化过程中必须进行的校验,通过使用本地缓存,可以有效提升访问速度,降低对数据库的查询频率,减轻数据库压力。

1.2、本地缓存的优、缺点

序号

优点

缺点

1

数据不需要跨网络传输,访问速度较快

占用应用进程的内存空间,无法进行大数据存储

2

有效减小后端数据库或服务器压力

一般无法被其他应用进程访问,数据一致性差

3

支持离线访问,提高了系统稳定性

数据存在过期时间和淘汰策略,缓存易失效

1.3、本地缓存对比

对比项

ConcurrentHashMap

GuavaCache

CaffeineCache

读写性能(并发)

很好

较好

很好

淘汰算法

LRU算法

W-TinyLFU算法

命中率

较好

较好

极好

功能丰富度

简单

丰富

丰富

本地缓存Caffeine高性能简析_本地缓存本地缓存Caffeine高性能简析_后端_02本地缓存Caffeine高性能简析_数据_03

        读 (100%)                 读 (75%) / 写 (25%)             写 (100%)

ConcurrentHashMap 可以作为进程内缓存,不受外部系统影响,读写性能好,速度快。但是无法进行缓存淘汰,需要限定 ConcurrentHashMap的容量范围,此时命中率将会下降,否则内存会无限制的增长。

Guava Cache提供了异步刷新和 LRU 淘汰策略解决上述问题,但偶发性、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。

Caffeine Cache则是为解决Guava Cache存在的问题而提出的,采用了 W-TinyLFU算法淘汰策略(LFU+LRU变种),有效提高了缓存的命中率,避免了缓存污染的问题。

2、Caffeine高性能读写

2.1、不同缓存读写策略差异

缓存数据的读写过程往往伴随着一些额外的操作,可以讲这些操作定义为缓存事务,数据过期的计算、判断,访问频率的统计,执行数据的淘汰,这些事务的处理策略是不同缓存读写性能差异点

Guava Cache 的事务处理在读取缓存时同步进行,这样的好处是不需要后台线程定期扫描处理事务,保证数据的实时性,但会增加一定的耗时。

Caffeine 借鉴了数据库系统的 WAL(Write-Ahead Logging)思想来减轻并发带来的锁竞争问题,在执行读写操作时,先把操作记录在缓冲区,后台线程在合适的时机异步、批量地执行缓冲区中的内容,读写的性能更高。本地缓存Caffeine高性能简析_java_04本地缓存Caffeine高性能简析_java_05

                 Guava读写策略                           Caffeine读写策略

2.2、Caffeine缓冲队列读写操作

在Caffeine的内部实现中,通过RingBuffer支持不同的操作(如 Removal,Refresh,Cleanup 等等)来降低锁竞争。

RingBuffer 的实现是一个Buffer[]数组,每个元素就是一个RingBuffer,每个线程都有自己对应的RingBuffer。

写入缓存成功后,缓存事务会写入到RingBuffer中,如果RingBuffer已满&&调度状态满足条件,会触发一个异步任务,异步的执行缓存事务。RingBuffer满之后,后续写入该队列的读操作事务会直接被丢弃。

本地缓存Caffeine高性能简析_本地缓存_06

本地缓存Caffeine高性能简析_缓存_07

Caffeine认为写操作的量远小于读,且不允许写操作有损,因此所有的写操作共享同一个传统的有界队列,统一由一个消费者按序处理。

3、Caffeine的高命中率

缓存性能关键指标之一就是缓存命中率(命中请求数 / 请求总数),命中率取决于缓存组件的数据淘汰算法

在缓存清理中,常见的淘汰算法包括LRU、LFU等,而Caffeine则基于LRU算法提出了更高效率的W-TinyLFU算法,该算法同时具备LRU和LFU两者的优点。

3.1、LRU算法

  • 思想:根据数据的最近访问记录进行淘汰,算法认为如果缓存最近被命中,那么在以后被访问的概率也较大。
  • 优点:可以有效的对访问频繁的热点数据进行保护
  • 缺点:对于周期性和偶发性高频率访问,易对缓存造成数据污染,使热点数据被淘汰,反而保留这些偶发性的非热点数据,导致此后的缓存命中率下降。

本地缓存Caffeine高性能简析_本地缓存_08

3.2、LFU算法

  • 思想:LFU会统计一段时间内的访问频率,并保留访问频率较高的数据,进行数据淘汰。
  • 优点:LFU也可以有效的保护缓存,相对周期性和偶发性高频率访问场景来讲,比LRU有更好的缓存命中率。
  • 缺点:对突发性的稀疏流量无力,需要针对每个记录项维护Long类型的频率信息,每次访问都要更新频率,占用较大的空间记录所有出现过的 key 和其对应的频次;

本地缓存Caffeine高性能简析_缓存_09

3.3、W-TinyLFU算法

3.3.1、算法数据结构

W-TinyLFU的数据结构如图所示,它由两个缓存单元组成,主缓存使用SLRU驱逐策略和TinyLFU准入策略,窗口缓存仅使用LRU驱逐策略,无准入策略。

本地缓存Caffeine高性能简析_本地缓存_10

  • 准入窗口(Admission Window)是一个较小的LRU队列,其容量只有缓存大小的1%,这个窗口的作用主要是为了保护一些新进入缓存的数据,给予一定的成长时间来积累使用频率,避免被快速淘汰。
  • 频次过滤器(TinyLFU)是Caffeine数据淘汰策略的核心所在,他依赖CountMin Sketch非精确的记录数据的历史访问次数,从而决定主缓存区数据的淘汰策略。
  • 主缓存区(Main region)用于存放大部分的缓存数据,数据结构为一个分段LRU队列(SLRU),包括保护区和观察区两部分,该部分使用TinyLFU策略进行数据的淘汰,一些访问频次很低的数据可以被快速淘汰掉,避免了主缓存区被新缓存污染。

3.3.2、频率统计方法

由于LFU算法基于hashMap来记录缓存项的频率,需要占用较大内存空间。为解决这个问题,在W-TinyLFU中使用Count-Min Sketch方法,来实现在有限的空间高效的记录缓存项访问频率。Count-Min Sketch算法详细实现方案如下:

本地缓存Caffeine高性能简析_数据_11

  • Count-Min Sketch维护了一个long[] table数组,通过四次hash过程可以有效降低hash冲突的概率。
  • Count-Min Sketch的每个计数器占用4bit,而table数组的每个元素大小占用64bit,相当于单个table元素包含16个计数器,相对于LFU算法而言,统计频率占用的内存就减小了16倍。
  • 16个计数器进一步分为4个group,那么每个group包含4个计数器,正好等于hash函数的个数,同一个key的四个计数器分别使用group内相应位置的计数器。

3.3.3、整体流转过程

本地缓存Caffeine高性能简析_缓存_12

1、所有进入缓存区的数据会首先被add进窗口区,当该队列长度达到容量限制后,会触发窗口区的淘汰操作,超出的元素会被下放至主缓存区的观察区队列,这些数据被称为竞争者

2、如果观察区未满,且观察区队列中的数据在被主缓存区淘汰之前获得了一次访问,该节点会被add进保护区队列,节点完成晋升

3、如果观察区也满了,对窗口区竞争者和观察区受害者的访问频次进行比较,而频次通过Count-Min Sketch方法获取。

4、保护区队列如果达到其容量限制会触发节点降级过程,同样通过比较竞争者与受害者的访问频次,如果受害者频次较低,队列首部的元素将会被下放到观察区队列。

4、Caffeine主要功能概括

本地缓存Caffeine高性能简析_缓存_13


5、回顾

  • 有效的数据结构(缓存分区)

caffeine综合了LFU和LRU的优势,将不同特性的缓存项存入不同的缓存区域,通过这种机制,很好的保障了访问时间新鲜程度因素,尽量将新鲜的缓存项保留在缓存中,能较好的处理稀疏流量、短时超热点流量等传统LRU和LFU无法很好处理的场景。

  • 高效的频率统计方法(概率计数)

在维护缓存项访问频率时,使用CountMin-Sketch算法进行概率计数,同时引入计数器饱和和衰减机制,较大程度上节省了存储资源,将访问频率高的缓存项保留在缓存中,实现了LFU更加高效的模拟实现。

  • 异步的缓存读写策略(WAL思想)

借鉴了数据库系统的 WAL(Write-Ahead Logging)思想,即:先写日志,再执行操作,执行读写操作时,先把操作记录在缓冲区,然后在合适的时机异步、批量地执行缓冲区中的内容,有效提升了caffeine缓存读写性能。

附:

本地缓存Caffeine高性能简析_数据_14