作为我们经常使用的内存数据库,redis在其内存管理以及主键失效的实现是怎么样的呢?我们一起通过粗略的阅读源代码(使用版本:3.2.2)来看看其实现机制:
第一种方式:主动轮询删除
首先其核心文件server.c的main函数里的initServer()函数里的相关代码:
void initServer(void) {
....
/* Create the serverCron() time event, that's our main way to process
* background operations. */
if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create the serverCron time event.");
exit(1);
}
....
}
从注释可以看到其创建了一个时间事件的回调函数serverCron,我们继续看看serverCron里的相关代码:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
....
/* Handle background operations on Redis databases. */
databasesCron();
....
}
继续追databasesCron:
void databasesCron(void) {
....
/* Expire keys by random sampling. Not required for slaves
* as master will synthesize DELs for us. */
if (server.active_expire_enabled && server.masterhost == NULL)
activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
....
}
继续activeExpireCycle:
void activeExpireCycle(int type) {
....
do {
long long t = dictGetSignedIntegerVal(de);
if (now > t) {
//删除前将要删除的key广播出去
propagateExpire(db,keyobj);
//删除该key
dbDelete(db,keyobj);
}
/* We don't repeat the cycle if there are less than 25% of keys
* found expired in the current DB. */
} while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
....
}
即当数据库中失效的key大于25%时,在限制的时间内轮询执行删除key操作,并且在删除key之前,将要删除的key广播出去,广播去哪里了呢,我们继续看代码(此代码位于db.c):
void propagateExpire(redisDb *db, robj *key) {
....
if (server.aof_state != AOF_OFF)
feedAppendOnlyFile(server.delCommand,db->id,argv,2);
replicationFeedSlaves(server.slaves,db->id,argv,2);
....
}
可以看到,广播给了可能开启的AOF持久化功能,还有自己的slave(这里我们回顾一下在执行activeExpireCycle函数时候的一个判断语句有一个 server.masterhost == NULL,即自己是主库的情况下才会主动去删除过期key并广播给从库,也就是说,这种轮询方式下从库是不需要主动清理过期key的)
第二种方式:被动消极删除
第一种方式虽然能保证大部分过期key的清理,但是我们也明显可以看到其是有弊端的,即并不能实时清理过期的key,总有一些漏网之鱼,那么这些key在访问的时候就会造成错误,所以redis还有一种被动的删除,即访问时判断是否过期,以下罗列一些源码:
/* Lookup a key for read operations, or return NULL if the key is not found
* in the specified DB.
*/
robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
....
if (expireIfNeeded(db,key) == 1) {
....
}
....
}
以上代码监听key的读取
/* Lookup a key for write operations, and as a side effect, if needed, expires
* the key if its TTL is reached.
*/
robj *lookupKeyWrite(redisDb *db, robj *key) {
expireIfNeeded(db,key);
return lookupKey(db,key,LOOKUP_NONE);
}
以上代码监听key的写入
/* EXISTS key1 key2 ... key_N.
* Return value is the number of keys existing. */
void existsCommand(client *c) {
....
for (j = 1; j < c->argc; j++) {
expireIfNeeded(c->db,c->argv[j]);
if (dbExists(c->db,c->argv[j])) count++;
}
....
}
以上代码查看key是否存在
还有其他一些源码就不一一列出了,这几段源码我们可以看到其中都调用了expireIfNeeded()函数,那么它是做什么的呢,追下源码:
int expireIfNeeded(redisDb *db, robj *key) {
mstime_t when = getExpire(db,key);
mstime_t now;
//key不过期,直接返回
if (when < 0) return 0;
//正在进行rdb数据恢复,直接返回
if (server.loading) return 0;
//如果是从库,只返回是否过期即可
if (server.masterhost != NULL) return now > when;
//广播
propagateExpire(db,key);
//删除
return dbDelete(db,key);
}
可以看到,其实在redis的每一次对key的操作的同时都会去检查一下这个key是否过期了,如果过期了就会做和轮询时一样的事情,广播并且删除该key。
第三种方式:OOM时的无奈删除
我们知道,以上两种方式已经可以完全保证数据的准确性了,但是如果当发生OOM(Out of Memory)时redis又该怎么办呢,是写不进去,还是如何处理?实际上,当OOM发生时,redis的LRU机制就会启动,老的数据将会被删除,我们通过redis的配置文件(config.c)看下redis都支持哪些LRU算法:
configEnum maxmemory_policy_enum[] = {
{"volatile-lru", MAXMEMORY_VOLATILE_LRU},
{"volatile-random",MAXMEMORY_VOLATILE_RANDOM},
{"volatile-ttl",MAXMEMORY_VOLATILE_TTL},
{"allkeys-lru",MAXMEMORY_ALLKEYS_LRU},
{"allkeys-random",MAXMEMORY_ALLKEYS_RANDOM},
{"noeviction",MAXMEMORY_NO_EVICTION},
{NULL, 0}
};
看下redis的作者对这些配置的解释吧==> Redis as an LRU cache。