缓存淘汰策略
参考:Redis 内存淘汰机制
认识
- 最大缓存
在 redis 中,允许用户设置最大使用内存大小 server.maxmemory,默认为0,没有指定最大缓存,如果有新的数据添加,超过最大内存,则会使redis崩溃,所以一定要设置。redis 内存数据集大小上升到一定大小的时候,就会实行数据淘汰策略。
- 主键失效
作为一种定期清理无效数据的重要机制,在 Redis 提供的诸多命令中,EXPIRE、EXPIREAT、PEXPIRE、PEXPIREAT 以及 SETEX 和 PSETEX 均可以用来设置一条 Key-Value 对的失效时间,而一条 Key-Value 对一旦被关联了失效时间就会在到期后自动删除(或者说变得无法访问更为准确)
- 淘汰机制
随着不断的向redis中保存数据,当内存剩余空间无法满足添加的数据时,redis 内就会施行数据淘汰策略,清除一部分内容然后保证新的数据可以保存到内存中。
内存淘汰机制是为了更好的使用内存,用一定得miss来换取内存的利用率,保证redis缓存中保存的都是热点数据。
设置过期时间
可以给redis中的key设置过期时间,当key过期的时候(生存期为0),它会被删除。
对key进行覆盖会修改数据的生存时间,如set和getSet命令
修改key对应的value和使用另外相同的key和value来覆盖以后,当前数据的生存时间不同。 如对一个 key 执行INCR命令,对一个列表进行LPUSH命令,或者对一个哈希表执行HSET命令,这类操作都不会修改 key 本身的生存时间。
使用rename对一个 key 进行改名,那么改名后的 key 的生存时间和改名前一样。
- 使用persist命令可以在不删除 key 的情况下,移除 key 的生存时间,让 key 重新成为一个persistent key
- 使用expire命令可以更新key的生存时间
淘汰策略
redis淘汰策略配置:maxmemory-policy voltile-lru,支持热配置
redis 提供 6种数据淘汰策略:
- voltile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据
策略规则
- 如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用allkeys-lru
- 如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用allkeys-random
- volatile-lru策略和volatile-random策略适合我们将一个Redis实例既应用于缓存和又应用于持久化存储的时候,然而我们也可以通过使用两个Redis实例来达到相同的效果,
- 将key设置过期时间实际上会消耗更多的内存,因此我们建议使用allkeys-lru策略从而更有效率的使用内存
非精准的LRU
上面提到的LRU(Least Recently Used)策略,实际上Redis实现的LRU并不是可靠的LRU,也就是名义上我们使用LRU算法淘汰键,但是实际上被淘汰的键并不一定是真正的最久没用的,这里涉及到一个权衡的问题,如果需要在全部键空间内搜索最优解,则必然会增加系统的开销,Redis是单线程的,也就是同一个实例在每一个时刻只能服务于一个客户端,所以耗时的操作一定要谨慎 。为了在一定成本内实现相对的LRU,早期的Redis版本是基于采样的LRU,也就是放弃全部键空间内搜索解改为采样空间搜索最优解。自从Redis3.0版本之后,Redis作者对于基于采样的LRU进行了一些优化,目的是在一定的成本内让结果更靠近真实的LRU。
失效的内部实现
Redis 删除失效主键的方法主要有两种:
消极方法(passive way),在主键被访问时如果发现它已经失效,那么就删除它
积极方法(active way),周期性地从设置了失效时间的主键中选择一部分失效的主键删除
主动删除:当前已用内存超过maxmemory限定时,触发主动清理策略,该策略由启动参数的配置决定
主键具体的失效时间全部都维护在expires这个字典表中。
typedef struct redisDb { dict *dict; //key-value dict *expires; //维护过期key dict *blocking_keys; dict *ready_keys; dict *watched_keys; int id; } redisDb;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
passive way 消极方法
- 在passive way 中, redis在实现GET、MGET、HGET、LRANGE等所有涉及到读取数据的命令时都会调用 expireIfNeeded,它存在的意义就是在读取数据之前先检查一下它有没有失效,如果失效了就删除它。
expireIfNeeded函数中调用的另外一个函数propagateExpire,这个函数用来在正式删除失效主键之前广播这个主键已经失效的信息,这个信息会传播到两个目的地:
- 一个是发送到AOF文件,将删除失效主键的这一操作以DEL Key的标准命令格式记录下来;
- 另一个就是发送到当前Redis服务器的所有Slave,同样将删除失效主键的这一操作以DEL Key的标准命令格式告知这些Slave删除各自的失效主键。从中我们可以知道,所有作为Slave来运行的Redis服务器并不需要通过消极方法来删除失效主键,它们只需要对Master唯命是从就OK了!
代码段二:
int expireIfNeeded(redisDb *db, robj *key) {
获取主键的失效时间
long long when = getExpire(db,key);
假如失效时间为负数,说明该主键未设置失效时间(失效时间默认为-1),直接返回0
if (when < 0) return 0;
假如Redis服务器正在从RDB文件中加载数据,暂时不进行失效主键的删除,直接返回0
if (server.loading) return 0;
假如当前的Redis服务器是作为Slave运行的,那么不进行失效主键的删除,因为Slave
上失效主键的删除是由Master来控制的,但是这里会将主键的失效时间与当前时间进行
一下对比,以告知调用者指定的主键是否已经失效了
if (server.masterhost != NULL) {
return mstime() > when;
}
如果以上条件都不满足,就将主键的失效时间与当前时间进行对比,如果发现指定的主键
还未失效就直接返回0
if (mstime() <= when) return 0;
如果发现主键确实已经失效了,那么首先更新关于失效主键的统计个数,然后将该主键失
效的信息进行广播,最后将该主键从数据库中删除
server.stat_expiredkeys++;
propagateExpire(db,key);
return dbDelete(db,key);
}
void propagateExpire(redisDb *db, robj *key) {
robj *argv[2];
shared.del是在Redis服务器启动之初就已经初始化好的一个常用Redis对象,即DEL命令
argv[0] = shared.del;
argv[1] = key;
incrRefCount(argv[0]);
incrRefCount(argv[1]);
检查Redis服务器是否开启了AOF,如果开启了就为失效主键记录一条DEL日志
if (server.aof_state != REDIS_AOF_OFF)
feedAppendOnlyFile(server.delCommand,db->id,argv,2);
检查Redis服务器是否拥有Slave,如果是就向所有Slave发送DEL失效主键的命令,这就是
上面expireIfNeeded函数中发现自己是Slave时无需主动删除失效主键的原因了,因为它
只需听从Master发送过来的命令就OK了
if (listLength(server.slaves))
replicationFeedSlaves(server.slaves,db->id,argv,2);
decrRefCount(argv[0]);
decrRefCount(argv[1]);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
Active Way
消极方法的缺点是,如果key 迟迟不被访问,就会占用很多内存空间. 所以就产生的积极的方式(Active Way):此方法利用了redis的时间事件,即每隔一段时间就中断一下完成一些指定操作,其中就包括检查并删除失效主键。
- 时间事件
创建时间事件, 回调函数就是serverCron,它在Redis服务器启动时创建,每秒的执行次数由宏定义REDIS_DEFAULT_HZ来指定,默认每秒钟执行10次。
- 使用activeExpireCycle 清除失效key
其实现原理是从Redis中每个数据库的expires字典表中,随机抽样REDIS_EXPIRELOOKUPS_PER_CRON(默认值为10)个设置了失效时间的主键,检查它们是否已经失效并删除掉失效的主键,如果失效主键个数占本次抽样个数的比例超过25%,它会继续进行下一轮的随机抽样和删除,直到刚才的比例低于25%才停止对当前数据库的处理,转向下一个数据库。
注意,activeExpireCycle函数不会试图一次性处理Redis中的所有数据库,而是最多只处理REDIS_DBCRON_DBS_PER_CALL(默认值为16),此外activeExpireCycle函数还有处理时间上的限制,不是想执行多久就执行多久,凡此种种都只有一个目的,那就是避免失效主键删除占用过多的CPU资源。