Redis 的过期策略是如何实现的?

背景
链接:https://juejin.im/post/5da3dc4c518825647c513aa1
来源:掘金

为了减少占用内存空间 , 通常会对放到 redis 中的键通过 expire 设置一个过期时间 , 那 Redis 是怎么实现对过期键删除的呢?
设置过期时间
设置过期时间的四种方式
# 将 key 的过期时间设置为 ttl 秒expire <key> <ttl> # 将 key 的过期时间设置为 ttl 毫秒pexpire <key> <ttl># 将 key 的过期时间设置为 timestamp 指定的秒数时间戳expire <key> <timestamp># 将 key 的过期时间设置为 timestamp 指定的毫秒数时间戳pexpire <key> <timestamp>复制代码其中前三种方式都会转化为最后一种方式来实现过期时间
Redis 的过期策略是如何实现的?

文章插图
 

Redis 的过期策略是如何实现的?

文章插图
 
保存过期时间
我们看下 redisDb 的结构
typedef struct redisDb { dict *dict; /* The keyspace for this DB */ dict *expires; /* Timeout of keys with a timeout set */ ...}复制代码可见在 redisDb 结构的 expire 字典(过期字典)保存了所有键的过期时间
过期字典的键是一个指向键空间中的某个键对象的指针
过期字典的值保存了键所指向的数据库键的过期时间
Redis 的过期策略是如何实现的?

文章插图
 
注意
图中过期字段和键空间中键对象有重复 , 实际中不会出现重复对象 , 键空间的键和过期字典的键都指向同一个键对象
过期键的判断
通过查询过期字典 , 检查下面的条件判断是否过期
  1. 检查给定的键是否在过期字典中 , 如果存在就获取键的过期时间
  2. 检查当前 UNIX 时间戳是否大于键的过期时间 , 是就过期 , 否则未过期
过期键的删除策略
惰性删除
在取出该键的时候对键进行过期检查 , 即只对当前处理的键做删除操作 , 不会在其他过期键上花费 CPU 时间
缺点:对内存不友好 , 如果一哥键过期了 , 但会保存在内存中 , 如果这个键还不会被访问 , 那么久会造成内存浪费 , 甚至造成内存泄露
如何实现?
就是在执行 Redis 的读写命令前都会调用 expireIfNeeded 方法对键做过期检查
如果键已经过期 , expireIfNeeded 方法将其删除
如果键未过期 , expireIfNeeded 方法不做处理
Redis 的过期策略是如何实现的?

文章插图
 
对应源码 db.c/expireIfNeeded 方法
int expireIfNeeded(redisDb *db, robj *key) { // 键未过期返回0 if (!keyIsExpired(db,key)) return 0; // 如果运行在从节点上 , 直接返回1 , 因为从节点不执行删除操作 , 可以看下面的复制部分 if (server.masterhost != NULL) return 1; // 运行到这里 , 表示键带有过期时间且运行在主节点上 // 删除过期键个数 server.stat_expiredkeys++; // 向从节点和AOF文件传播过期信息 propagateExpire(db,key,server.lazyfree_lazy_expire); // 发送事件通知 notifyKeyspaceEvent(NOTIFY_EXPIRED, "expired",key,db->id); // 根据配置(默认是同步删除)判断是否采用惰性删除(这里的惰性删除是指采用后台线程处理删除操做 , 这样会减少卡顿) return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) : dbSyncDelete(db,key);}复制代码补充
我们通常说 Redis 是单线程的 , 其实 Redis 把处理网络收发和执行命令的操作都放到了主线程 , 但 Redis 还有其他后台线程在工作 , 这些后台线程一般从事 IO 较重的工作 , 比如刷盘等操作 。
上面源码中根据是否配置 lazyfree


    推荐阅读