阅读 Redis 源码,学习缓存淘汰算法 W
发布日期:2022-06-18 17:09 点击次数:124
文末本文转载自微信公众号「董泽润的技艺条记」,作家董泽润 。转载本文请臆度董泽润的技艺条记公众号。
统统 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 短时分莫得看望, 小12萝8禁在线喷水观看就会被只使用一次的冷数据冲掉,无法反映竟然的使用情况
LFU 能幸免上述情况,可是朴素 LFU 已矣无法应答突发流量,无法驱散历史热 keys,是以 redis LFU 已矣类似于 W-TinyLFU[3], 其中 W 是 windows 的旨趣,即一定时分窗口后对频率进行减半,如若不减的话,cache 就成了对历史数据的统计,而不是缓存
上头还提到突发流量如若应答呢?谜底是给新看望的 key 一个运行频率值,不至于由于运行值为 0 无法更新频率
LRU 已矣int processCommand(redisClient *c) { ...... /* Handle the maxmemory directive. * * First we try to free some memory if possible (if there are volatile * keys in the dataset). If there are not the only thing we can do * is returning an error. */ if (server.maxmemory) { int retval = freeMemoryIfNeeded(); if ((c->cmd->flags & REDIS_CMD_DENYOOM) && retval == REDIS_ERR) { flagTransaction(c); addReply(c, shared.oomerr); return REDIS_OK; } } ...... }
在每次处理 client 号召时都会调用 freeMemoryIfNeeded 检查是否有必有驱散某些 key, 当 redis 施行使用内存达到上限时出手淘汰。可是 redis 做的比拟取巧,并莫得对统统的 key 做 lru 队伍,而是按照 maxmemory_samples 参数进行采样,系统默许是 5 个 key
上头是很经典的一个图,jk制服白丝自慰无码自慰网站当到达 10 个 key 时恶果更接近表面上的 LRU 算法,可是 cpu 蹧跶会变高,是以系统默许值就够了。
LFU 已矣robj *lookupKey(redisDb *db, robj *key, int flags) { dictEntry *de = dictFind(db->dict,key->ptr); if (de) { robj *val = dictGetVal(de); /* Update the access time for the ageing algorithm. * Don't do it if we have a saving child, as this will trigger * a copy on write madness. */ if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){ if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) { updateLFU(val); } else { val->lru = LRU_CLOCK(); } } return val; } else { returnNULL; } }
当 lookupKey 看望某 key 时,会更新 LRU. 从 redis 4.0 出手迟缓引入了 LFU 算法,由于复用了 LRU 字段,是以只可使用 24 bits
* We split the 24 bits into two fields: * * 16 bits 8 bits * +----------------+--------+ * + Last decr time | LOG_C | * +----------------+--------+
其中低 8 位 counter 用于计数频率,取值为从 0~255, 可是经由取对数的,是以不错暗意很大的看望频率
高 16 位 ldt (Last Decrement Time)暗意终末一次看望的 miniutes 时分戳, 用于衰减 counter 值,如若 counter 不衰减的话就酿成了对历史 key 看望次数的统计了,而不是 LFU
unsigned long LFUTimeElapsed(unsigned long ldt) { unsigned long now = LFUGetTimeInMinutes(); if (now >= ldt) return now-ldt; return 65535-ldt+now; }
雅致由于 ldt 只用了 16位计数,最大值 65535,是以会出现回卷 rewind
LFU 取得己有计数* counter of the scanned objects if needed. */ unsigned long LFUDecrAndReturn(robj *o) { unsigned long ldt = o->lru >> 8; unsigned long counter = o->lru & 255; unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0; if (num_periods) counter = (num_periods > counter) ? 0 : counter - num_periods; return counter; }
num_periods 代表规画出来的待衰减计数,lfu_decay_time 代表衰减悉数,默许值是 1,如若 lfu_decay_time 大于 1 衰延缓率会变得很慢
终末复返的计数值为衰减之后的,也有可能是 0
LFU 计数更新并取对数/* Logarithmically increment a counter. The greater is the current counter value * the less likely is that it gets really implemented. Saturate it at 255. */ uint8_t LFULogIncr(uint8_t counter) { if (counter == 255) return 255; double r = (double)rand()/RAND_MAX; double baseval = counter - LFU_INIT_VAL; if (baseval < 0) baseval = 0; double p = 1.0/(baseval*server.lfu_log_factor+1); if (r < p) counter++; return counter; }
计数跨越 255, 就无谓算了,班师复返即可。LFU_INIT_VAL 是运行值,默许是 5
如若减去运行值后 baseval 小于 0 了,讲解快落伍了,就更倾向于递加 counter 值
double p = 1.0/(baseval*server.lfu_log_factor+1);
这个概率算法中 lfu_log_factor 是对数底,默许是 10, 当 counter 值较小时自增的概率较大,如若 counter 较大,倾向于不做任何操作
counter 值从 0~255 不错暗意很大的看望频率,饱和用了
# +--------+------------+------------+------------+------------+------------+ # | factor | 100 hits | 1000 hits | 100K hits | 1M hits | 10M hits | # +--------+------------+------------+------------+------------+------------+ # | 0 | 104 | 255 | 255 | 255 | 255 | # +--------+------------+------------+------------+------------+------------+ # | 1 | 18 | 49 | 255 | 255 | 255 | # +--------+------------+------------+------------+------------+------------+ # | 10 | 10 | 18 | 142 | 255 | 255 | # +--------+------------+------------+------------+------------+------------+ # | 100 | 8 | 11 | 49 | 143 | 255 | # +--------+------------+------------+------------+------------+------------+
基于这个特点,咱们就不错用 redis-cli --hotkeys 号召,来稽察系统中的最近一段时分的热 key, 终点实用。老版块中是没这个功能的,需要人工统计
$ redis-cli --hotkeys # Scanning the entire keyspace to find hot keys as well as # average sizes per key type. You can use -i 0.1 to sleep 0.1 sec # per 100 SCAN commands (not usually needed). ...... [47.62%] Hot key 'key17' found so far with counter 6 [57.14%] Hot key 'key43' found so far with counter 7 [57.14%] Hot key 'key14' found so far with counter 6 [85.71%] Hot key 'key42' found so far with counter 7 [85.71%] Hot key 'key45' found so far with counter 8 [95.24%] Hot key 'key50' found so far with counter 7 -------- summary ------- Sampled 105 keys in the keyspace! hot key found with counter: 7 keyname: key40 hot key found with counter: 7 keyname: key42 hot key found with counter: 7 keyname: key50谈谈缓存的规画
前边提到的是 redis LFU 已矣,这是聚合式的缓存,咱们还有好多程度的土产货缓存。怎样评价一个缓存已矣的利害,有好多规画,细节更紧迫
浑沌量:常说的 QPS, 对标 bucket 已矣的 hashmap 复杂度是 O(1), 缓存复杂度要高一些,还有锁竞争要处理,总之缓存库已矣的效能要高
缓存掷中率:光有浑沌量还不够,缓存掷中率也终点要道,掷中率越高讲解引入缓存作用越大
高档特点:缓存规画统计,怎样应答缓存击穿等等