所有 IT 从业者都接触过缓存,一定了解基本工作原理,业界流行一句话: 缓存就是万金油,哪里有问题哪里抹一下 。那他的本质是什么呢?
上图代表从 cpu 到底层硬盘不同层次,不同模块的运行速度,上层多加一层 cache, 就能解决下层的速度慢的问题,这里的慢是指两点:IO 慢和 cpu 重复计算缓存中间结果
但是 cache 受限于成本,cache size 一般都是固定的,所以数据需要淘汰,由此引出一系列其它问题:缓存一致性、击穿、雪崩、污染等等,本文通过阅读 redis 源码,学习主流淘汰算法
如果不是 leetcode 146 LRU [1] 刷题需要,我想大家也不会手写 cache, 简单的实现和工程实践相距十万八千里,真正 production ready 的缓存库非常考验细节
Redis 缓存淘汰配置
一般 redis 不建义当成存储使用,只允许当作 cache, 并设置 max-memory, 当内存使用达到最大值时,redis-server 会根据不同配置开始删除 keys. Redis 从 4.0 版本引进了 LFU [2] , 即 Least Frequently Used ,4.0 以前默认使用 LRU 即 Least Recently Used
- volatile-lru 只针对设置 expire 过期的 key 进行 lru 淘汰
- allkeys-lru 对所有的 key 进行 lru 淘汰
- volatile-lfu 只针对设置 expire 过期的 key 进行 lfu 淘汰
- allkeys-lfu 对所有的 key 进行 lfu 淘汰
- volatile-random 只针对设置 expire 过期的进行随机淘汰
- allkeys-random 所有的 key 随机淘汰
- volatile-ttl 淘汰 ttl 过期时间最小的 key
- noeviction 什么都不做,如果此时内存已满,系统无法写入
默认策略是 noeviction , 也就是不驱逐,此时如果写满,系统无法写入,建义设置为 LFU 相关的。 LRU 优先淘汰最近未被使用,无法应对冷数据,比如热 keys 短时间没有访问,就会被只使用一次的冷数据冲掉,无法反应真实的使用情况
LFU 能避免上述情况,但是 朴素 LFU 实现无法应对突发流量,无法驱逐历史热 keys ,所以 redis LFU 实现类似于 W-TinyLFU [3] , 其中 W 是 windows 的意思,即一定时间窗口后对频率进行减半,如果不减的话,cache 就成了对历史数据的统计,而不是缓存
上面还提到突发流量如果应对呢?答案是给新访问的 key 一个初始频率值,不至于由于初始值为 0 无法更新频率
LRU 实现
int processCommand(r