定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
在这三种策略中,第一种和第三种为主动删除策略,而第二种为被动侧除策略。而无论是哪种策略都有其优点和缺点。
对于定时删除来说:
优点是可以保证过期键会尽可能快的被删除,并释放过期键所占用的内存;
缺点则是会占用一部分 CPU 时间,尤其是当键非常多的时候,占用的 CPU 时间也会增多,这是不可忍受的。
对于惰性删除来说:
程序只会在取出键是进行检查,所以优点是几乎不不占用 CPU 时间;
缺点则是可能会造成内存泄漏,比如当键过期之后永远不再访问,这时候就是内存泄漏了。
对于定期删除来说,则是以上两种策略的一种整合,定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响;除此之外,通过定期删除过期键,定期删除策略有效地减少了因为过期键而带来的内存浪费。
定期删除策略的难点是确定删除操作执行的时长和频率:
如果删除操作执行得太频繁,或者执行的时间太长,定期删除策略就会退化成定时删除策略,以至于将 CPU 时间过多地消耗在侧除过期键上面。
如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除策略一样,出现浪费内存的情况。
因此,如果采用定期侧除策略的话,服务器必须根据情况,合理地设置删除操作的执行时长和执行频率。
而 Redis 中则使用了定期删除和惰性删除两种策略,很好的在 CPU 和内存上面取得了一个平衡。
惰性删除过期键的惰性删除策略由 db.c/expireIfNeeded 函数实现,所有读写数据库的 Redis 命令在执行之前都会调用 expireIfNeeded 函数对输入键进行检查:
如果输人键已经过期,那么 expireIfNeeded 函数将输入键从数据库中删除。
如果输人键未过期,那么 expireIfNeeded 函数不做动作。
expireIfNeeded 函数就像一个过滤器,它可以在命令真正执行之前,过滤掉过期的输人键,从而避免命令接触到过期键。函数的具体代码如下:
/* * 检查 key 是否已经过期,如果是的话,将它从数据库中删除。 * * 返回 0 表示键没有过期时间,或者键未过期。 * * 返回 1 表示键已经因为过期而被删除了。 */ int expireIfNeeded(redisDb *db, robj *key) { // 取出键的过期时间 mstime_t when = getExpire(db,key); mstime_t now; // 没有过期时间 if (when < 0) return 0; /* No expire for this key */ // 如果服务器正在进行载入,那么不进行任何过期检查 if (server.loading) return 0; /* If we are in the context of a Lua script, we claim that time is * blocked to when the Lua script started. This way a key can expire * only the first time it is accessed and not in the middle of the * script execution, making propagation to slaves / AOF consistent. * See issue #1525 on Github for more information. */ now = server.lua_caller ? server.lua_time_start : mstime(); /* If we are running in the context of a slave, return ASAP: * the slave key expiration is controlled by the master that will * send us synthesized DEL operations for expired keys. * * Still we try to return the right information to the caller, * that is, 0 if we think the key should be still valid, 1 if * we think the key is expired at this time. */ // 当服务器运行在 replication 模式时 // 附属节点并不主动删除 key // 它只返回一个逻辑上正确的返回值 // 真正的删除操作要等待主节点发来删除命令时才执行 // 从而保证数据的同步 if (server.masterhost != NULL) return now > when; // 运行到这里,表示键带有过期时间,并且服务器为主节点 /* Return when this key has not expired */ // 如果未过期,返回 0 if (now <= when) return 0; /* Delete the key */ server.stat_expiredkeys++; // 向 AOF 文件和附属节点传播过期信息 propagateExpire(db,key); // 发送事件通知 notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED, "expired",key,db->id); // 将过期键从数据库中删除 return dbDelete(db,key); }命令调用 expireIfNeeded 来删除过期键的过程和 get 命令的执行过程如下(出自《Redis设计与实现第二版》第九章:数据库):
定期删除