Redis 内存优化
Redis是内存型数据库,其容量大小一般是GB级别。如果我们持续不断地往Redis中存入数据,那么内存将会无限增长,最后直接OOM。为了解决这个问题,Redis提供了一些策略实现内存回收:
-
内存过期策略
-
内存淘汰策略
内存过期策略
什么是内存过期
为了减少Redis 内存使用,一般情况下我们在往Redis中写入数据的时候都会对Redis key设置一个过期时间。Redis 提供了给数据设置过期时间的功能:
-
EXPIRE key seconds
-
SETEX key seconds value
C
// example 1
set name sam
expire name 10 // 10秒后get name 结果为nil
// example 2
setex name 10 sam
get name // 10秒后get name 结果为nil
通过上面的例子可以发现,当key的TTL到期以后,再次访问name返回的是nil,说明这个key已经不存在了,对应的内存也得到释放。从而起到内存回收的目的。
对Redis 的 key设置过期时间除了有助于减少内存的使用。在业务上也有其他的作用。比如有的业务场景就是需要某个数据具体有效期:如短信验证码有效期为60秒。
Redis 判断数据过期的实现原理
在Redis database结构体中,有两个Dict:一个用来记录key-value;另一个用来记录key-TTL。
C++
typedef struct redisDb {
dict *dict; /* 存放所有key及value的地方,也被称为keyspace*/
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; /* 记录平均TTL时长 */
unsigned long expires_cursor; /* expire检查时在dict中抽样的索引位置. */
list *defrag_later; /* 等待碎片整理的key列表. */
} redisDb;
Redis是如何知道一个key是否过期呢?
-
利用两个Dict分别记录key-value对及key-ttl对
- dict: 数据库键空间,保存着数据库中所有键值对
- expire:过期字典,保存着键的过期时间。 其中过期字典的键指向Redis数据库中的某个key(键),过期字典的值是一个long long类型的整数,这个整数保存了key所指向的数据库键的过期时间(毫秒精度的UNIX时间戳)
-
是不是TTL到期就立即删除了呢?:不是
-
惰性删除:并不是在TTL到期后就立刻删除,而是在访问一个key的时候,检查该key的存活时间,如果已经过期才执行删除。
-
周期删除:通过一个定时任务,周期性的抽样部分过期的key,然后执行删除。执行周期有两种:
- Redis服务初始化函数initServer()中设置定时任务,模式为SLOW。SLOW模式执行频率默认为10,每次不超过25ms
- Redis的每个事件循环前会调用beforeSleep()函数,执行过期key清理,模式为FAST。FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms
-
定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 定期删除 + 惰性删除 。
-
-
Redis为什么没有采用定时删除呢?即比如设置60s后过期,60s后自动触发任务执行key的删除
- 这是因为redis扫描key是需要花费时间的,会存在一定的时延。因此无法保证删除的时效性和正确性。因此Redis目前并没有使用定时删除策略。
- redis的定时删除可能导致某些key 集中过期,阻塞cpu。
SLOW模式规则:
-
执行频率受server.hz影响,默认为10,即每秒执行10次,每个执行周期100ms。
-
执行清理耗时不超过一次执行周期的25%.默认slow模式耗时不超过25ms
-
逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期
-
如果没达到时间上限(25ms)并且过期key比例大于10%,再进行一次抽样,否则结束
FAST模式规则(过期key比例小于10%不执行 ):
-
执行频率受beforeSleep()调用频率影响,但两次FAST模式间隔不低于2ms
-
执行清理耗时不超过1ms
-
逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期
-
如果没达到时间上限(1ms)并且过期key比例大于10%,再进行一次抽样,否则结束
内存淘汰机制
什么是内存淘汰
通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。怎么解决这个问题呢?答案就是:Redis 内存淘汰机制。
内存淘汰:就是当Redis内存使用达到设置的上限时,主动挑选部分key删除以释放更多内存的流程。Redis会在处理客户端命令的方法processCommand()中尝试做内存淘汰:
C++
int processCommand(client *c) {
// 如果服务器设置了server.maxmemory属性,并且并未有执行lua脚本
if (server.maxmemory && !server.lua_timedout) {
// 尝试进行内存淘汰performEvictions
int out_of_memory = (performEvictions() == EVICT_FAIL);
// ...
if (out_of_memory && reject_cmd_on_oom) {
rejectCommand(c, shared.oomerr);
return C_OK;
}
// ....
}
}
内存淘汰策略
可以使用
config get maxmemory-policy
命令,来查看当前 Redis 的内存淘汰策略
C++
127.0.0.1:6379> config get maxmemory-policy
maxmemory-policy: noeviction
Redis 提供 6 种数据淘汰策略:
-
volatile-lru(least recently used) :从已设置过期时间的数据集(
server.db[i].expires
)中挑选最近最少使用的数据淘汰。(Redis3.0之前,默认的内存淘汰策略) -
volatile-ttl :从已设置过期时间的数据集(
server.db[i].expires
)中挑选将要过期的数据淘汰。 -
volatile-random :从已设置过期时间的数据集(
server.db[i].expires
)中任意选择数据淘汰。 -
allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key。
-
allkeys-random :从数据集(
server.db[i].dict
)中任意选择数据淘汰。 -
no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。(Redis3.0之后,默认的内存淘汰策略)
4.0 版本后增加以下两种:
-
volatile-lfu(least frequently used) :从已设置过期时间的数据集(
server.db[i].expires
)中挑选最不经常使用的数据淘汰。 -
allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key。