Redis原理篇——内存回收
Redis是基于内存存储的,性能强。Redis的性能瓶颈也在于内存,但是单节点内存不宜过大,会影响持久化或主从同步性能。
通过配置文件
来设置Redis的最大内存:
一、过期策略
Redis是键值类型的数据库,所有的key和value保存在 Dict
结构中,在Redis数据库结构体中,有两个Dict
:一个用来记录key-value
,另一个记录key-TTL
typedef struct redisDb {
dict *dict; /* 存放所有key和value */
dict *expires; /* 存放key和对应的TTL存活时间 只包含设置了TTL的key */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS (事务) */
int id; /* Database ID 使用的是几号库 0~15 */
long long avg_ttl; /* Average TTL, 平均TTL时间 */
unsigned long expires_cursor; /* expire检查时在dict中抽取的索引位置 */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
因此只需要查Redis中的key对应的TTL,就可以判断这个key是否过期
删除策略
TTL到期就立刻删除吗?
可以设置定时器,当一个key到期了就立刻删除,但是对于大量key来说,设置大量定时器对CPU的占用率高,影响Redis主线程的性能,因此Redis不会立刻删除到期的key
1.惰性删除:
在访问一个key的时候检查该key的存活时间,如果已经过期了才执行删除。
robj *lookupKeyWriteWithFlags(redisDb *db, robj *key, int flags) {
// 检查key是否过期
expireIfNeeded(db,key);
return lookupKey(db,key,flags);
}
robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
robj *val;
// 检查key是否过期
if (expireIfNeeded(db,key) == 1) {
/*
......... 略
*/
}
/*
......... 略
*/
}
int expireIfNeeded(redisDb *db, robj *key) {
// 判断是否过期,如果没过期就返回0
if (!keyIsExpired(db,key)) return 0;
/*
......... 略
*/
/* 删除过期的key */
deleteExpiredKeyAndPropagate(db,key);
return 1;
}
惰性删除存在的问题:
惰性删除是在访问一个key的时候检查是否过期,如果一个key过期了但是客户端长时间没有操作,那么这个key就一直不能被删除,如果这样的key存在大量的,那么就会一直占据着内存,影响性能。
2.周期删除
周期性的抽样一部分过期的key,然后执行删除。
执行周期有两种:
- SLOW模式: Redis会设置一个定时任务
serverCron()
,按照server.hz
的频率来执行过期key清理
- 执行频率受
server.hz
影响,默认为10,即每秒执行10次,每个执行周期100ms - 执行清理耗时不超过一次执行周期的25%
- 逐个遍历db,抽取20个key判断是否过期
- 如果没达到时间上限(25ms)并且过期key比例大于10%,再进行一次抽样,否则结束
- FAST模式: Redis的每个事件循环前会调用
beforeSleep()
函数,执行过期key清理
- 执行频率受beforeSleep()调用频率影响,但两次FAST模式间隔不低于2ms
- 执行清理耗时不超过1ms
- 依次遍历db,抽取20个key判断是否过期
- 如果没达到时间上限(1ms)并且过期key比例大于10%,再进行一次抽样,否则结束
二、内存淘汰策略
内存淘汰: Redis内存使用达到设置的阈值时,Redis主动挑选部分key删除以释放内存。
Redis支持8种不同的策略:
noeviction:
不淘汰任何key,内存满时不允许写入新数据(默认策略)volatile-ttl:
对设置TTL的key,比较剩余的TTL值,先淘汰数值小的allkeys-random:
对全体key,随机进行淘汰。volatile-random:
对设置了TTL的key,随机淘汰。allkeys-lru:
对全体key,基于LRU算法进行淘汰volatile-lru:
对设置了TTL的key,基于LRU算法进行淘汰allkeys-lfu:
对全体key,基于LFU算法进行淘汰volatile-lfu:
对设置了TTL的key,基于LFU算法进行淘汰
typedef struct redisObject {
unsigned type:4; // 对象类型,分别为五种基本数据类型 占4个bit位
unsigned encoding:4; // 编码方式 11 中 占4个bit位
unsigned lru:LRU_BITS; /*LRU以秒为单位记录最近一次访问时间,长度为24bit LFU:高16位以分钟为单位记录最近一次访问时间,低8位记录逻辑访问次数*/
int refcount; // 引用计数器 0表示对象无人引用 可以被回收
void *ptr; // 指针,指向存放实际数据的空间
} robj;
LFU的访问次数之所以叫逻辑访问次数,是因为并不是每次key被访问都计数。而是通过运算:
- 生成0~1之间的随机数R
- 计算
1/(旧次数*lfu_log_factor+1)
,记录为P,lfu_log_factor
默认为10 - 如果
R<P
,计数器+1,最大不超过255 - 访问次数会随时间衰减,距离上一次访问时间每隔
lfu_decay_time
分钟(默认1),计数器-1