七叶笔记 » 数据库 » 详解Redis 缓存删除机制(源码解析)

详解Redis 缓存删除机制(源码解析)

删除的范围 过期的 key 在内存满了的情况下,如果继续执行 set 等命令,且所有 key 都没有过期,那么会按照缓存淘汰策略选中的 key

过期删除

redis 中设置了过期时间的 key 会单独存储一份

设置有效期

Redis 中有 4 个命令可以给 key 设置过期时间,分别是 expire pexpire expireat pexpireat 

设置相对时间

expire <key> <ttl>:将 key 值的过期时间设置为 ttl 秒。

pexpire <key> <ttl>:将 key 值的过期时间设置为 ttl 毫秒。

设置绝对时间

expireat <key> <timestamp>:将 key 值的过期时间设置为指定的 timestamp 秒数。

pexpireat <key> <timestamp>:将 key 值的过期时间设置为指定的 timestamp 毫秒数。

以上 4 种方法最终都会调用下面的通用函数 expireGenericCommand :

设置过期时间的操作由 setExpire 执行,他将 dictEntry 的 union v 中的 s64 设为过期时间

db->expires 中存储的  dictEntry 表示的是过期 key 和过期时间,存储过期时间的 v 是一个 union ,可见在 redis 中不同使用场景或不同编码下 v 的意义不同

查询过期时间

ttl key 返回 key 剩余过期秒数。

pttl key 返回 key 剩余过期的毫秒数。

以上 2 种查看方式最终都会调用下面的通用函数 ttlGenericCommand :

获取过期时间的操作由 getExpire 执行,在 db->expires 中查询到对象后,获取 union v 中的成员 s64 

过期策略

Redis 综合使用 惰性删除 和 定期扫描 实现

惰性删除

每次访问时会调用 expireIfNeeded 判断 key 是否过期,如果过期就删除该键,否则返回键对应的值。单独使用这种策略可能会浪费很多内存。

特殊处理

在 master 节点执行 Lua 脚本时

参考 GitHub Issue #1525,对于 master,在执行 Lua Script 的过程中,可能会用某个 key 是否存在当作判断条件。为了避免一个脚本中前后条件不一致,将当前时间强制设为脚本开始时间。 例如多次执行如下 Lua 脚本 /tmp/myscript.lua 出现的结果可能不一致

具体复现操作可以参考下面的 bash 脚本:

对于 slave 节点

在 slave 节点上,key 的删除操作由 master 发来的 DEL 执行,因此这里只返回是否过期的结果给客户端,而不执行删除操作

正在从 RDB 和 AOF 读取数据时跳过这个步骤

定期扫描

系统每隔一段时间就定期扫描一次,发现过期的键就进行删除。单独使用这种策略可能出现键已经过期但没有删除的情况 Redis 默认每 100ms 执行一次(通过 hz 参数配置,执行周期为 1s/hz)过期扫描。由于 redisDb 中设置了过期时间的 key 会单独存储,所以不会出现扫描所有 key 的情况 具体步骤由 activeExpireCycle 函数执行

activeExpireCycle、incrementallyRehash 等后台操作都是由 databasesCron 触发的

随机抽样由 dictGetRandomKey 执行

缓存淘汰

配置最大内存限制

在 redis.conf 中配置

redis server 启动时加载配置文件和命令行参数中的 maxmemory ,存入 Server 对象的 maxmemory 字段 main 中在 redis server 启动时执行初始化等操作,其中会执行加载配置文件的 loadServerConfig 函数

loadServerConfig 中将配置文件、stdin、命令行参数加载到 config 字符串中,然后调用 loadServerConfigFromString 

loadServerConfigFromString 从上一步中的 config 字符串中逐行读取配置,并写入 server 对象

使用 CONFIG SET 命令配置

Redis Server 接收到客户端的 CONFIG SET 命令后调用 configSetCommand 函数 服务端接收到命令时将命令和参数存入 Redis Server 的 argc 和 argv

动态配置 maxmemory 后会立即尝试触发内存回收,而修改其他内存相关配置(例如: maxmemory_policy)时不会触发

32 位机器的内存限制

对于 64 位机器,将 maxmemory 设置为 0 表示不限制内存,但由于 32 位寻址空间最多只有 4 GB,所以默认内存限制设为 3 GB,缓存淘汰策略设为 noeviction

淘汰策略

淘汰策略使用 CONFIG SET maxmemory-policy 配置

默认:

**noeviction: **内存满了后对于 set 等命令直接返回错误

针对所有 key: 

allkeys-lru: 在所有 key 的范围内使用 LRU 算法执行删除,如果内存仍然不够,则报错 **allkeys-lfu: **在所有 key 的范围内使用 LRU 算法执行删除,如果内存仍然不够,则报错 **allkeys-random: **在所有 key 的范围内随机执行删除,如果内存仍然不够,则报错

针对设置了过期时间的 key: 

**volatile-lru: **在设置了过期时间的 key 中使用 LRU 算法执行删除,如果内存仍然不够,则报错 **volatile-lfu: **在设置了过期时间的 key 中使用 LRU 算法执行删除,如果内存仍然不够,则报错 **volatile-random: **在设置了过期时间的 key 中随机执行删除,如果内存仍然不够,则报错 **volatile-ttl: **删除即将过期的 key,如果内存仍然不够,则报错

Redis 在执行淘汰之前会计算部分对象的 idle 值,使用不同淘汰策略时计算 idle 值的方法也不同, idle 值越大表示这个值越需要优先删除。

下面主要介绍 LRU 和 LFU 中 idle 值的计算方法

LRU 淘汰策略

抽样删除,样本数量通过 CONFIG SET maxmemory-samples 100  控制,对应 RedisObject 中的 maxmemory_samples 参数,抽样数量越多和传统的 LRU 算法越接近

优化策略

为了避免传统的 LRU 算法通常使用 hashmap + 链表实现带来的开销,Redis 进行了如下优化:

RedisObject 结构中设置了一个 lru 字段,用来记录数据的访问时间戳,而不是每次调整对象在链表中的位置

使用抽样数组代替链表,后续在候选集合中根据 lru 字段值的大小进行筛选,避免了链表带来的开销。候选集合中的对象用 evictionPoolEntry 表示

计算方法

全局对象 lru_clock 记录了当前的 unix 时间戳,由 serverCron 调用  updateCachedTime 默认每 100 ms 更新一次。更新频率与 hz 参数有关, 1s/hz 即为更新间隔时间。 LRU_CLOCK_RESOLUTION 的值为 1000,因此使用 LRU_CLOCK 函数获取 lru_clock 时,如果每秒更新频率在 1 次以上,会使用全局变量中缓存的 lrulcock

如果更新频率不到每秒 1 次,则会用函数 getLRUClock 实时计算 lruclock 

其中 LRU_CLOCK_MAX 表示 lru_clock  最大的可能值,这个值与 redisObject 中 lru 最大的可能值一样,定义如下:

所以最终比较时 lru_clock 和 robj.lru 的值都在 [0, LRU_CLOCK_MAX] 的范围内。 从逻辑上讲, 当前时间戳应该永远大于上次访问的时间戳 ,因此正常的计算规则应该是 lru_clock-robj.lru 。 但是由于 lru_clock 和 robj.lru 是当前时间戳取模后的值,所以可能出现 lru_clock 小于 robj.lru 的情况,所以这种情况下计算规则应该改为 lru_clock+194天-robj.lru  但是对于 lru_clock 和 robj.lru 间隔超过 194 天的情况仍然无法判断,所以更能存在删除不准确的情况。 将上述的逻辑组合起来就是 LRU 算法下获取 idle 值的函数:

在 Redis 3.0 中,当取样数量设为 10 时,已经和传统的 LRU 算法效果很接近了

LFU 淘汰策略

LFU 算法复用 robj.lru 字段,将这个 24 bit 的字段拆分成了两部分:

ldt(last decrement time,单位:分钟):lru 字段的前 16bit,表示数据的访问时间戳,最多只能存储 45 天。 counter 值:lru 字段的后 8bit,表示数据的访问频率

递增策略

counter 能表示的最大值是 255,因此 counter 与访问次数不能是线性关系,这里采用的计算步骤如下:

随机取 0 到 1 之间的随机数 r 比较 r 与 1/((counter-LFU_INIT_VAL)*lfu_log_factor+1) 的大小,其中 LFU_INIT_VAL 是常量,默认为 5,lfu_log_factor 是可配置参数,默认为 10 如果 r 小则 counter 增加 1,否则 counter 不变

实现代码如下:

访问次数与 counter 值之间大概是对数关系,counter 值越大,增速越低

衰减策略

除了访问对象时 counter 需要增加,对于一段时间内没有访问的对象还要相应地减少 counter 值,递减的速率由 lfu-decay-time 参数控制。 counter 衰减步骤如下:

取当前时间戳(单位:分钟)的低 16 位记为 now ,计算与 ldt  的差值。这里与 LRU 算法中计算 lru_clock 和 robj.lru 时可能出现一样的问题,由于 ldt 最多只能表示 45 天,所以如果距离对象上次访问超过 45 天,则无法准确计算访问的时间间隔

如果 lfu_decay_time 为 0,则步修改 counter,否则将 counter 减少 LFUTimeElapsed(ldt)/lfu_decay_time

例如,在 lfu_decay_time 为 1 的情况下,如果有 N 分钟没有访问这个对象,那么 counter 值减 N 每次访问一个对象时都会调用 updateLFU 更新 counter 的值:

执行淘汰

当 Redis 需要淘汰一批数据时,会调用 evictionPoolPopulate 获取一批待删除对象,根据设置的淘汰范围的不同,会决定传递给 evictionPoolPopulate 的 sampledict 参数是存有全部数据的 db->dict 还是只有设置了过期时间的数据的 db->expires 

完成上述操作后,pool 中剩下的就是新筛选出来的最需要淘汰的对象了。

在 freeMemoryIfNeeded 中会调用 evictionPoolPopulate 来筛选需要淘汰的对象,每次删除一个,直到释放了足够的内存。如果始终无法达到内存需求,则会报错。

到此这篇关于Redis 缓存删除机制(源码解析)的文章就介绍到这了,更多相关Redis 缓存删除内容请搜索七叶笔记以前的文章或继续浏览下面的相关文章希望大家以后多多支持七叶笔记!

相关文章