最近发现线上的session 服务器每隔一段时间内存占用就达到24G,通过redis info查看发现expires key没有被删除:
db1:keys=101177370,expires=101165505
研究了一下才发现,只有在配置文件中设置了最大内存时候才会调用这个函数,而设置这个参数的意义是,你把当做一个内存而不是数据库。
redis如何删除过期数据?
用一个可以 "find reference" 的 IDE, 沿着 setex( Set the value and expiration of a key ) 命令一窥究竟:
void setexCommand(redisClient *c) { c->argv[3] = tryObjectEncoding(c->argv[3]); setGenericCommand(c,0,c->argv[1],c->argv[3],c->argv[2]); }
setGenericCommand 是一个实现 set,setnx,setex 的通用函数,参数设置不同而已。
void setCommand(redisClient *c) {
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,0,c->argv[1],c->argv[2],NULL);
}
void setnxCommand(redisClient *c) {
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,1,c->argv[1],c->argv[2],NULL);
}
void setexCommand(redisClient *c) {
c->argv[3] = tryObjectEncoding(c->argv[3]);
setGenericCommand(c,0,c->argv[1],c->argv[3],c->argv[2]);
}
再看 setGenericCommand :
void setGenericCommand(redisClient *c, int nx, robj *key, robj *val, robj *expire) { long seconds = 0; /* initialized to avoid an harmness warning */ if (expire) { if (getLongFromObjectOrReply(c, expire, &seconds, NULL) != REDIS_OK) return; if (seconds <= 0) { addReplyError(c,"invalid expire time in SETEX"); return; } } if (lookupKeyWrite(c->db,key) != NULL && nx) { addReply(c,shared.czero); return; } setKey(c->db,key,val); server.dirty++; if (expire) setExpire(c->db,key,time(NULL)+seconds); addReply(c, nx ? shared.cone : shared.ok); }
13 行处理 "Set the value of a key, only if the key does not exist" 的场景, 17 行插入这个 key , 19 行设置它的超时,注意时间戳已经被设置成了到期时间。这里要看一下 redisDb ( 即 c ->db ) 的定义:
typedef struct redisDb { dict *dict; /* The keyspace for this DB */ dict *expires; /* Timeout of keys with a timeout set */ dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) */ dict *io_keys; /* Keys with clients waiting for VM I/O */ dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */ int id; } redisDb;
仅关注 dict 和 expires ,分别来存 key-value 和它的超时,也就是说如果一个 key-value 是有超时的,那么它会存在 dict 里,同时也存到 expires 里,类似这样的形式: dict[key]:value,expires[key]:timeout.
当然 key-value 没有超时, expires 里就不存在这个 key 。 剩下 setKey 和 setExpire 两个函数无非是插数据到两个字典里,这里不再详述。
那么 redis 是如何删除过期 key 的呢。
通过查看 dbDelete 的调用者, 首先注意到这一个函数,是用来删除过期 key 的。
int expireIfNeeded(redisDb *db, robj *key) { time_t when = getExpire(db,key); if (when < 0) return 0; /* No expire for this key */ /* Don't expire anything while loading. It will be done later. */ if (server.loading) return 0; /* 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. */ if (server.masterhost != NULL) { return time(NULL) > when; } /* Return when this key has not expired */ if (time(NULL) <= when) return 0; /* Delete the key */ server.stat_expiredkeys++; propagateExpire(db,key); return dbDelete(db,key); }
ifNeed 表示能删则删,所以 4 行没有设置超时不删, 7 行在 "loading" 时不删, 16 行非主库不删, 21 行未到期不删。 25 行同步从库和文件。
再看看哪些函数调用了 expireIfNeeded ,有 lookupKeyRead , lookupKeyWrite , dbRandomKey , existsCommand , keysCommand 。通过这些函数命名可以看出,只要访问了某一个 key ,顺带做的事情就是尝试查看过期并删除,这就保证了用户不可能访问到过期的 key 。但是如果有大量的 key 过期,并且没有被访问到,那么就浪费了许多内存。 Redis 是如何处理这个问题的呢。
dbDelete 的调用者里还发现这样一个函数:
1 /* Try to expire a few timed out keys. The algorithm used is adaptive and 2 * will use few CPU cycles if there are few expiring keys, otherwise 3 * it will get more aggressive to avoid that too much memory is used by 4 * keys that can be removed from the keyspace. */ 5 void activeExpireCycle(void) { 6 int j; 7 8 for (j = 0; j < server.dbnum; j++) { 9 int expired; 10 redisDb *db = server.db+j; 11 12 /* Continue to expire if at the end of the cycle more than 25% 13 * of the keys were expired. */ 14 do { 15 long num = dictSize(db->expires); 16 time_t now = time(NULL); 17 18 expired = 0; 19 if (num > REDIS_EXPIRELOOKUPS_PER_CRON) 20 num = REDIS_EXPIRELOOKUPS_PER_CRON; 21 while (num--) { 22 dictEntry *de; 23 time_t t; 24 25 if ((de = dictGetRandomKey(db->expires)) == NULL) break; 26 t = (time_t) dictGetEntryVal(de); 27 if (now > t) { 28 sds key = dictGetEntryKey(de); 29 robj *keyobj = createStringObject(key,sdslen(key)); 30 31 propagateExpire(db,keyobj); 32 dbDelete(db,keyobj); 33 decrRefCount(keyobj); 34 expired++; 35 server.stat_expiredkeys++; 36 } 37 } 38 } while (expired > REDIS_EXPIRELOOKUPS_PER_CRON/4); 39 } 40 } 41
这个函数的意图已经有说明: 删一点点过期 key ,如果过期 key 较少,那也只用一点点 cpu 。 25 行随机取一个 key , 38 行删 key 成功的概率较低就退出。这个函数被放在一个 cron 里,每毫秒被调用一次。这个算法保证每次会删除一定比例的 key ,但是如果 key 总量很大,而这个比例控制的太大,就需要更多次的循环,浪费 cpu ,控制的太小,过期的 key 就会变多,浪费内存——这就是时空权衡了。
最后在 dbDelete 的调用者里还发现这样一个函数:
/* This function gets called when 'maxmemory' is set on the config file to limit * the max memory used by the server, and we are out of memory. * This function will try to, in order: * * - Free objects from the free list * - Try to remove keys with an EXPIRE set * * It is not possible to free enough memory to reach used-memory < maxmemory * the server will start refusing commands that will enlarge even more the * memory usage. */ void freeMemoryIfNeeded(void)
这个函数太长就不再详述了,注释部分说明只有在配置文件中设置了最大内存时候才会调用这个函数,而设置这个参数的意义是,你把 redis 当做一个内存 cache 而不是 key-value 数据库。
以上 3 种删除过期 key 的途径,第二种定期删除一定比例的 key 是主要的删除途径,第一种“读时删除”保证过期 key 不会被访问到,第三种是一个当内存超出设定时的暴力手段。由此也能看出 redis 设计的巧妙之处,
参考:http://www.cppblog.com/richbirdandy/archive/2011/11/29/161184.html