redis笔记-2- 内存回收
认清事实
redis 作为 数据库 or 缓存的区别
作为缓存,存储 热点数据。需要考虑 redis里的数据 怎么能随着业务变化,只保留热点数据,因为 内存大小是有限的,是 redis 的瓶颈。
过期策略
redis 提供了 过期策略:
> 主动淘汰: 默认每隔100ms定时轮训找出一定比例 设置过期时间的key,做判断,如果过期则删除。
> 被动淘汰:被再次访问是,判断是否过期,过期则删除
基于 定期策略 和 被动删除,还是会存在 很多过期的key 在内存堆积,导致内存耗尽。 redis提供了淘汰策略。
淘汰策略
allkeys-lru: 当内存不足以容纳新写的入数据时,在 键空间中,移除最近最少使用的key(最常用)
volatile-lru:当内存不足以容纳新写的入数据时,在 设置过期时间的 建空间中,移除最近最少使用的的key
allkey-lfu: 在带有过期时间的键空间中 选择 最不常用的key 进行删除
volatile-lfu: 在所有的建中选择最不常用的key 进行删除
allkeys-random:当内存不足以容纳新写的入数据时,在 键空间中,随机移除 随机某个key
volatile-random:内存不足以容纳新写的入数据时,在设置过期的键空间中,随机移除某个key
volatile-ttl:内存不足以容纳新写的入数据时,在设置了过期时间的键空间中,有更咋骨气时间的key优先
noeviction: (默认策略)。当内存不足以容纳 新写入的数据时,新写入操作会报错。
注:如果没有符合前提条件的 key 被淘汰,那么 volatile-lru、volatile-random 、volatile-ttl 相当于 noeviction(不做内存回收).
LRU淘汰原理
如果基于传统 LRU 算法实现 Redis LRU 会有什么问题?
需要额外的数据结构存储,消耗内存。
Redis LRU 对传统的 LRU 算法进行了改良,通过**随机采样**来调整算法的精度。
如果淘汰策略是 LRU,则根据配置的采样值 maxmemory_samples(默认是 5 个),随机从数据库中选择 m 个 key, 淘汰其中热度最低的 key 对应的缓存数据。所以采样参数m配置的数值越大, 就越能精确的查找到待淘汰的缓存数据,但是也消耗更多的CPU计算,执行效率降低。
如何找出热度最低的数据
Redis 中所有对象结构都有一个 lru 字段, 且使用了 unsigned 的低 24 位,这个字段用来记录对象的热度。对象被创建时会记录 lru 值。在被访问的时候也会更新 lru 的值,但是不是获取系统当前的时间戳,而是设置为全局变量 server.lruclock 的值。
源码:server.h
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} robj;
server.lruclock 的值怎么来的?
Redis 中 有 个 定 时 处 理 的 函 数 serverCron , 默 认 每 100 毫 秒 调 用 函 数,updateCachedTime 更新一次全局变量的 server.lruclock 的值,它记录的是当前 unix时间戳。
源码:server.c
void updateCachedTime(void) {
time_t unixtime = time(NULL);
atomicSet(server.unixtime,unixtime);
server.mstime = mstime();
struct tm tm;
localtime_r(&server.unixtime,&tm);
server.daylight_active = tm.tm_isdst;
}
为什么不获取精确的时间而是放在全局变量中?不会有延迟的问题吗?
函数 lookupKey 中更新数据的 lru 热度值时,就不用每次调用系统函数 time,可以提高执行效率。
当对象里面已经有了 LRU 字段的值,就可以评估对象的热度了。
函数 estimateObjectIdleTime 评估指定对象的 lru 热度,思想就是对象的 lru 值和全局的 server.lruclock 的差值越大(越久没有得到更新), 该对象热度越低。
源码 evict.c
/* Given an object returns the min number of milliseconds the object was never
* requested, using an approximated LRU algorithm. */
unsigned long long estimateObjectIdleTime(robj *o) {
unsigned long long lruclock = LRU_CLOCK();
if (lruclock >= o->lru) {
return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
} else {
return (lruclock + (LRU_CLOCK_MAX - o->lru)) *
LRU_CLOCK_RESOLUTION;
}
}
server.lruclock 只有 24 位,按秒为单位来表示才能存储 194 天。当超过 24bit 能表示的最大时间的时候,它会从头开始计算。
server.h
define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */
在这种情况下,可能会出现对象的 lru 大于 server.lruclock 的情况,如果这种情况出现那么就两个相加而不是相减来求最久的 key.
为什么不用常规的哈希表+双向链表的方式实现?需要额外的数据结构,消耗资源。而 Redis LRU 算法在 sample 为 10 的情况下,已经能接近传统 LRU 算法了。
除了消耗资源之外,传统 LRU 还有什么问题?
假设 A 在 10 秒内被访问了 5 次,而 B 在 10 秒内被访问了 3 次。因为 B 最后一次被访问的时间比 A 要晚,在同等的情况下,A 反而先被回收。

需要基于频率的淘汰机制
LFU淘汰原理
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} robj;
当这 24 bits 用作 LFU 时,其被分为两部分:
高 16 位用来记录访问时间(单位为分钟,ldt,last decrement time)
低 8 位用来记录访问频率,简称 counter(logc,logistic counter)
counter 是用基于概率的对数计数器实现的,8 位可以表示百万次的访问频率。对象被读写的时候,lfu 的值会被更新。
db.c——lookupKey
void updateLFU(robj *val) {
unsigned long counter = LFUDecrAndReturn(val);
counter = LFULogIncr(counter);
val->lru = (LFUGetTimeInMinutes()<<8) | counter;
}
增长的速率由,lfu-log-factor 越大,counter 增长的越慢.
redis.conf 配置文件中的设置:
```
lfu-log-factor 10 #增长的速率
lfu-decay-time 1 #衰减因子
```
如果计数器只会递增不会递减,也不能体现对象的热度。没有被访问的时候,计数器怎么递减呢?
减少的值由衰减因子 lfu-decay-time(分钟)来控制,如果值是 1 的话,N 分钟没有访问就要减少 N。
最大内存设置
redis.conf的参数配置:
`# maxmemory <byte>`
如果不设置 maxmemory 或者 设置为0, 64位系统不限制内存, 32位系统最多使用 3GB
动态修改配置:
`config set maxmemory 2GB`
参考: 咕泡学院 redis课程

浙公网安备 33010602011771号