缓存清除策略
FIFO
FIFO(First In First Out)是一种先进先出的调度策略。
先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。
缺点:
判断一个页面置换算法优劣的指标就是缺页率,而 FIFO 算法的一个显著的缺点是,在某些特定的时刻,缺页率反而会随着分配页面的增加而增加,这称为 Belady 现象。产生 Belady 现象现象的原因是,FIFO 置换算法与进程访问内存的动态特征是不相容的,被置换的内存页面往往是被频繁访问的,或者没有给进程分配足够的页面,因此 FIFO 算法会使一些页面频繁地被替换和重新申请内存,从而导致缺页率增加。因此,FIFO 算法的使用场景较少。
LRU
LRU(Least Recently Used)是一种最近最少使用策略。
最近最少使用策略,无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。
LFU
LRU(Least Frequency Used)是一种最少使用调度策略。
最少使用策略,无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。策略算法主要比较元素的hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略。
ARC
ARC 是为了提高缓存效果介于 LRU 和 LFU 设置的算法。借助 LRU 和 LFU 基本思想实现,以获得可用缓存的最佳使用。
OPT
OPT(OPTimal replacement)是一种理论上最佳的页面置换算法。
淘汰以后永远用不到的数据项;如果没有,则淘汰最久以后再用到的数据项。属于理想型算法,不可能实现(因为无法知道全局的访问序列)。但是,可以最为评价其他算法优劣的参考标准。
gcache 源码分析
概述
gcache 是基于 Golang 实现的一个内存级 Cache 基础库,支持带失效时间的 Cache。
在这篇文章里,我们将主要从键值对的读写过程,以及数据变化的角度来阐述 gcache 的基本原理,详细操作可参考源代码。
多种缓存策略
gcache 目前包括 Simple,LFU(Least Frequency Used),LRU(Least Recently Used),ARC(Adaptive Replacement Cache)四种缓存策略。
- Simple:普通缓存策略,随机淘汰。
- LRU:Least Recently Used,优先替换最近最少使用的内容。
- LFU:Least Frequently Used,优先替换访问次数最少的内容。
- ARC:Adaptive Replacement Cache,ARC介于 LRU 和 LFU 之间。
支持回调策略
gcache 的回调函数原型(定制化操作),在 cache.go 中定义:
1 | type ( |
计数统计
- HitCount:命中次数
- MissCount:未命中次数
- LookupCount:查找次数(HitCount + MissCount)
- HitRate:命中率(HitCount / LookupCount)
基本架构
![20191024232913.png](/images
接口
1 | type Cache interface { |
CacheBuilder
CacheBuilder 用来构造缓存对象,以及各种个性化化配置、策略。由 cache.go/CacheBuilder 结构表示:
cache.go 中有详细注解。
1 | // CacheBuilder 构造缓存对象,以及各种个性化化配置、策略。 |
设置回调函数
这里仅列举了 loaderExpireFunc
(key 过期时的回调函数)的具体实现;其他回调(淘汰 key 时的回调、序列化回调、反序列化回调等等)的设置方式相同。
1 | // Set a loader function with expiration. |
设置淘汰策略
这里仅列举一个 LRU 淘汰策略
的设置(LFU,ARC 的设置方式相同)。
1 | // 设置淘汰策略 |
baseCache
baseCache 是 gcache 的基础数据结构,包含缓存大小、回调、失效时间、读写锁、命中率等,由 cache.go/baseCache 结构表示:
1 | type baseCache struct { |
SimpleCache
gcache 中 SimpleCache 由 simple.go/SimpleCache 结构表示:
1 | // SimpleCache has no clear priority for evict cache. It depends on key-value map order. |
SimpleCache 中 simpleItem 由 simple.go/simpleItem 结构表示:
1 | type simpleItem struct { |
源码参考 simple.go
设置超时时间
超时时间的设定方式:在当前时间的基础上加上超时时间 expiration。
1 | // Set a new key-value pair with an expiration time |
键值对失效判断
simpleItem 的超时判断,其他(lruItem,lfuItem,arcItem)超时判断逻辑相同,即检查过期时间 expiration 是否在当前时间 now 之前。
1 | // IsExpired returns boolean value whether this item is expired or not. |
设置键值对
我们通过 SET 方法为 SimpleCache 设置键值对:
1 | gc := gcache.New(3). |
如图,展示了设置上述 3 个键值对之后 SimpleCache 的状态(为了在接下来操作中表述简单,此处未设置失效时间):
读取键值对
由于 SimpleCache 没有排序策略,读取操作不会改变键值对的相对排序。
缓存满时增加键值对
当 SimpleCache 缓存满时再增加键值对,会先执行淘汰策略。随机淘汰 items 中的键值对(因为 golang 中,map 遍历是随机的)。
1 | gc.Set("k4", "v4") |
:
如图,展示了随机淘汰 items 中 1 个键值对,并新增键值对过程中,SimpleCache 的变化(为了在接下来操作中表述简单,此处未设置失效时间):
LRUCache
gcache 中 LRUCache 由 lru.go/LRUCache 结构表示:
1 | // Discards the least recently used items first. |
- baseCache 为缓存基础数据结构,包含缓存大小以及各种回调函数;
- items 是一个 map 类型,可以在 O(1) 时间内找到指定键值对,值为 lruItem;
- evictList 是一个双向链表,用来淘汰最近未使用的键值对,是实现 LRU 策略的关键结构;
LRUCache 中 lruItem 由 lru.go/lruItem 结构表示:
1 | type lruItem struct { |
设置键值对
我们通过 SET 方法为 LRUCache 设置键值对:
1 | gc := gcache.New(3). |
如图,展示了设置上述 3 个键值对之后 LRUCache 的状态(为了在接下来操作中表述简单,此处未设置失效时间):
关键代码:
1 | // set a new key-value pair |
读取键值对
我们通过 LRUCache 方法读取 gcache 中的键值对。当 key 存在时,返回对应的键值对;当 key 不存在时返回错误 KeyNotFoundError(如果设置有回调,则为返回回调执行结果)。
会涉及到键值对顺序,hitCount、missCount 的变化。
读取成功
执行GET("k2")
读取键值对。缓存命中次数(hitCount)+1,k2 在被移动到淘汰链表首部。读取后成功后 LRUCache 状态(流程):读取失败(访问不存在的键)
执行GET("k4")
读取键值对。缺失次数(missCount)+1,淘汰链表顺序不变,LRUCache 状态:
关键代码:
1 | // Get a value from cache pool using key if it exists. |
缓存满时增加键值对
当 LRUCache 缓存满时再增加键值对,会先执行淘汰策略。淘汰链表尾部键值对,并删除 items 中对应 key。
1 | gc.Set("k5", "v5") |
淘汰链表尾部键值对 和 items 对应元素:
关键代码:
1 | // evict removes the oldest item from the cache. |
k5 键值对,插入到链表头部位置:
LFUCache
gcache 中 LFUCache 由 lfu.go/LFUCache 结构表示:
1 | // Discards the least frequently used items first. |
- baseCache 为缓存基础数据结构,包含缓存大小以及各种回调函数;
- items 是一个 map 类型,可以在 O(1) 时间内找到指定键值对,值为 lruItem;
- freqList 是一个双向链表,用来淘汰最近未使用的键值对,是实现 LRU 策略的关键结构;
LFUCache 中 freqEntry 由 lfu.go/freqEntry 结构表示:
1 | type freqEntry struct { |
LFUCache 中 lfuItem 由 lfu.go/lfuItem 结构表示:
1 | type lfuItem struct { |
如图,展示了未设置任何键值对时 LFUCache 的状态:
设置键值对
同样,我们通过 SET 方法为 LFUCache 设置键值对:
1 | gc := gcache.New(3). |
如图,展示了设置上述 3 个键值对之后 LFUCache 的状态(为了在接下来操作中表述简单,此处未设置失效时间):
读取键值对
我们通过 GET 方法读取 LFUCache 中的键值对。当 key 存在时,返回对应的键值对;当 key 不存在时返回错误 KeyNotFoundError(如果设置有回调,则为返回回调执行结果)。
读取操作会改变键值对所在 freqEntry 节点,同时 hitCount、missCount 也会随之变化。
读取失败时,与 LRUCache 的逻辑一致,这里不再说明。
以执行 GET("k1")
读取键值对为例:
删除 freqEntry(freq = 0) 节点 items 中的 key = lfuItem1 的键值对:
创建 freqEntry(freq = 1) 新节点(插入到 freq = 0 节点之后),然后把 key = lfuItem1 的键值对添加到节点 items 中,并修改命中次数(hitCount):
注意:
随着读取操作的发生,freqList 会越来越长(如果某个 key 读取次数最多达到 N 次,那么 freqList 长度就等于 N + 1)。
这样的话,就可能会带来内存问题。
缓存满时增加键值对
当 LFUCache 缓存满时再增加键值对,会先执行淘汰策略。从链表首部(访问频次为 0 的节点)开始删除指定个数(默认 1)的键值对,并删除 items 中对应 key。
1 | gc.Set("k5", "v5") |
从链表首部节点开始选择待淘汰键值对:
淘汰
步骤 1
中选定的键值对:新节点加入到链表首部节点(freq = 0)中,并更新 items:
ARCCache
ARC(Adaptive Replacement Cache) 融合了 LRU 和 LFU 的特点,在二者之间取得一个平衡,同时也使得算法看起来更复杂一些。
gcache 中 ARCCache 由 arc.go/ARCCache 结构表示:
1 | // Constantly balances between LRU and LFU, to improve the combined result. |
如图,展示了未设置任何键值对时 ARCCache 的状态:
相比 LRUCache,LFUCache 来讲,ARCCache 的逻辑复杂了很多。如图,给出 ARCCache 键值对转移状态图:
键值对状态图与失效时间有关,这里给出键值对的流转图示。
设置键值对
同样,我们通过 SET 方法为 ARCCache 设置键值对:
1 | gc := gcache.New(3). |
ARCCache 的内部存储就复杂了很多,大致流程参考前面给的【ARCCache 键值对转移状态图】。键值对如下:
关键代码:
1 | func (c *ARC) Set(key, value interface{}) error { |
读取键值对
执行 GET("k1")
操作(第 1 次读),ARCCache 会将键值对从 t1
移到 t2
。或者从 Ghost list
中捞回。
1 | gc.Get("k1") |
假如在访问的过程中键值对 k1 过期了,则会被移到 b1 中:
可以看到,t1 中的其他键值对,依次被移动到了 t2 中:
![20191025003158](/images再次执行
gc.Get("k2")
操作时,由于键值已经在 t2 中,会改变键值对的排序:再次执行
gc.Set("k1", "v1")
操作时,由于键值对已经在 b1 中,则会把 k1 捞出来,并且放到 t2 中(认为是高频访问):
关键代码:
1 | // Get a value from cache pool using key if it exists. If not exists and it has LoaderFunc, it will generate the value using you have specified LoaderFunc method returns value. |
缓存满时增加键值对
当缓存满时新增键值对,ARCCache 会淘汰 t1,b1,b2 中的数据(参考 SET 方法)。
图略
淘汰策略:
1 | // replace |
引用
操作系统之页面置换算法
谈谈缓存和基本的缓存算法
缓存算法(FIFO 、LRU、LFU三种算法的区别)
Go gcache 源码分析(图解)
常用缓存淘汰算法(LFU、LRU、ARC、FIFO、MRU)
缓存机制Cache ARC算法(一)
gcache 源码分析
阿里P8架构师谈:详解Memcached、Redis等缓存的特征、原理、应用
一文深入了解:分布式系统中的缓存架构
Ristretto is a fast, concurrent cache library built with a focus on performance and correctness.
Caffeine Cache-高性能Java本地缓存组件