一文搞懂Redis内存策略,面试再也不怕被问了

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 种数据淘汰策略:

  1. volatile-lru(least recently used) :从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。(Redis3.0之前,默认的内存淘汰策略)

  2. volatile-ttl :从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。

  3. volatile-random :从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。

  4. allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key。

  5. allkeys-random :从数据集(server.db[i].dict)中任意选择数据淘汰。

  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。(Redis3.0之后,默认的内存淘汰策略)

4.0 版本后增加以下两种:

  1. volatile-lfu(least frequently used) :从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰。

  2. allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key。

相关推荐
sam-1231 小时前
k8s上部署redis高可用集群
redis·docker·k8s
看山还是山,看水还是。2 小时前
Redis 配置
运维·数据库·redis·安全·缓存·测试覆盖率
谷新龙0012 小时前
Redis运行时的10大重要指标
数据库·redis·缓存
精进攻城狮@2 小时前
Redis缓存雪崩、缓存击穿、缓存穿透
数据库·redis·缓存
avenue轩7 小时前
gdb调试redis。sudo
c++·redis
不惑_7 小时前
Redis:发布(pub)与订阅(sub)实战
前端·redis·bootstrap
cui_win7 小时前
Redis高可用-Sentinel(哨兵)
redis·bootstrap·sentinel
cui_win10 小时前
Redis高可用-主从复制
redis·git·github·主从复制·哨兵
三杯温开水12 小时前
基于 CentOS7.6 的 Docker 下载常用的容器(MySQL&Redis&MongoDB),解决拉取容器镜像失败问题
redis·mysql·docker
袁庭新12 小时前
LuaRocks如何安装数据库驱动?
java·数据库·redis·lua·luarocks·袁庭新