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 缓存删除内容请搜索七叶笔记以前的文章或继续浏览下面的相关文章希望大家以后多多支持七叶笔记!