通过EXPIRE key seconds 命令来设置数据的过期时间。返回1表明设置成功,返回0表明key不存在或者不能成功设置过期时间。在key上设置了过期时间后key将在指定的秒数后被自动删除。被指定了过期时间的key在Redis中被称为是不稳定的。

Redis key过期的方式有三种:

  • 惰性删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key(无法保证冷数据被及时删掉)
  • 定期删除:Redis会定期主动淘汰一批已过期的key(随机抽取一批key检查)
  • 内存淘汰机制:当前已用内存超过maxmemory限定时,触发主动清理策略
     

 

1、如果没有设置有效期,即使内存用完,redis 自动回收机制也是看设置了有效期的,不会动没有设定有效期的,如果清理后内存还是满的,就不再接受写操作。

  1. Redis无论有没有设置expire,他都会遵循redis的配置好的删除机制,在配置文件里设置:
  2. redis最大内存不足"时,数据清除策略,默认为"volatile-lru"。
  3. volatile-lru  ->对"过期集合"中的数据采取LRU(近期最少使用)算法.如果对key使用"expire"指令指定了过期时间,那么此key将会被添加到"过期集合"中。将已经过期/LRU的数据优先移除.如果"过期集合"中全部移除仍不能满足内存需求,将OOM.
  4. allkeys-lru ->对所有的数据,采用LRU算法
  5. volatile-random ->对"过期集合"中的数据采取"随即选取"算法,并移除选中的K-V,直到"内存足够"为止. 如果如果"过期集合"中全部移除全部移除仍不能满足,将OOM
  6. allkeys-random ->对所有的数据,采取"随机选取"算法,并移除选中的K-V,直到"内存足够"为止
  7. volatile-ttl ->对"过期集合"中的数据采取TTL算法(最小存活时间),移除即将过期的数据.
  8. noeviction ->不做任何干扰操作,直接返回OOM异常

另外,如果数据的过期不会对"应用系统"带来异常,且系统中write操作比较密集,建议采取"allkeys-lru"。

由以上可以看出,对没设置expire的数据,产生影响的是allkeys-lru机制,allkeys-random。

所以redis没设置expire的数据是否会删除,是由你自己选择的删除机制决定的。

在Redis服务器占用内存达到maxmemory最大的情况下,当再想增加内存占用时,会按maxmemory-policy删除机制将老的数据删除。这里简单说一下volatile-lru,Redis会按LRU算法删除设置了过期时间但还没有过期的key,而对于没有设置过期时间的key,Redis是永远保留的。当然,如果你不想删除没有过期的key,那可以使用noeviction机制。
 

===================================================================

redis如何删除过期数据?

用一个可以 "find reference" 的 IDE, 沿着 setex( Set the value and expiration of a key ) 命令一窥究竟:

  1. void setexCommand(redisClient *c) {
  2. c->argv[3] = tryObjectEncoding(c->argv[3]);
  3. setGenericCommand(c,0,c->argv[1],c->argv[3],c->argv[2]);
  4. }

 

setGenericCommand 是一个实现 set,setnx,setex 的通用函数,参数设置不同而已。

  1. void setCommand(redisClient *c) {
  2. c->argv[2] = tryObjectEncoding(c->argv[2]);
  3. setGenericCommand(c,0,c->argv[1],c->argv[2],NULL);
  4. }
  5. void setnxCommand(redisClient *c) {
  6. c->argv[2] = tryObjectEncoding(c->argv[2]);
  7. setGenericCommand(c,1,c->argv[1],c->argv[2],NULL);
  8. }
  9. void setexCommand(redisClient *c) {
  10. c->argv[3] = tryObjectEncoding(c->argv[3]);
  11. setGenericCommand(c,0,c->argv[1],c->argv[3],c->argv[2]);
  12. }

 再看 setGenericCommand :

  1. void setGenericCommand(redisClient *c, int nx, robj *key, robj *val, robj *expire) {
  2. long seconds = 0; /* initialized to avoid an harmness warning */
  3. if (expire) {
  4. if (getLongFromObjectOrReply(c, expire, &seconds, NULL) != REDIS_OK)
  5. return;
  6. if (seconds <= 0) {
  7. addReplyError(c,"invalid expire time in SETEX");
  8. return;
  9. }
  10. }
  11. if (lookupKeyWrite(c->db,key) != NULL && nx) {
  12. addReply(c,shared.czero);
  13. return;
  14. }
  15. setKey(c->db,key,val);
  16. server.dirty++;
  17. if (expire) setExpire(c->db,key,time(NULL)+seconds);
  18. addReply(c, nx ? shared.cone : shared.ok);
  19. }

13 行处理 "Set the value of a key, only if the key does not exist" 的场景, 17 行插入这个key , 19 行设置它的超时,注意时间戳已经被设置成了到期时间。这里要看一下 redisDb ( 即 c ->db) 的定义:

  1. typedef struct redisDb {
  2. dict *dict; /* The keyspace for this DB */
  3. dict *expires; /* Timeout of keys with a timeout set */
  4. dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) */
  5. dict *io_keys; /* Keys with clients waiting for VM I/O */
  6. dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
  7. int id;
  8. } 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 的。

  1. int expireIfNeeded(redisDb *db, robj *key) {
  2. time_t when = getExpire(db,key);
  3. if (when < 0) return 0; /* No expire for this key */
  4. /* Don't expire anything while loading. It will be done later. */
  5. if (server.loading) return 0;
  6. /* If we are running in the context of a slave, return ASAP:
  7. * the slave key expiration is controlled by the master that will
  8. * send us synthesized DEL operations for expired keys.
  9. *
  10. * Still we try to return the right information to the caller,
  11. * that is, 0 if we think the key should be still valid, 1 if
  12. * we think the key is expired at this time. */
  13. if (server.masterhost != NULL) {
  14. return time(NULL) > when;
  15. }
  16. /* Return when this key has not expired */
  17. if (time(NULL) <= when) return 0;
  18. /* Delete the key */
  19. server.stat_expiredkeys++;
  20. propagateExpire(db,key);
  21. return dbDelete(db,key);
  22. }

ifNeed 表示能删则删,所以 4 行没有设置超时不删, 7 行在 "loading" 时不删, 16 行非主库不删,21 行未到期不删。 25 行同步从库和文件。

再看看哪些函数调用了 expireIfNeeded ,有 lookupKeyRead , lookupKeyWrite ,dbRandomKey , existsCommand , keysCommand 。通过这些函数命名可以看出,只要访问了某一个 key ,顺带做的事情就是尝试查看过期并删除,这就保证了用户不可能访问到过期的 key 。但是如果有大量的 key 过期,并且没有被访问到,那么就浪费了许多内存。 Redis 是如何处理这个问题的呢。

dbDelete 的调用者里还发现这样一个函数:

  1. 1 /* Try to expire a few timed out keys. The algorithm used is adaptive and
  2. 2 * will use few CPU cycles if there are few expiring keys, otherwise
  3. 3 * it will get more aggressive to avoid that too much memory is used by
  4. 4 * keys that can be removed from the keyspace. */
  5. 5 void activeExpireCycle(void) {
  6. 6 int j;
  7. 7
  8. 8 for (j = 0; j < server.dbnum; j++) {
  9. 9 int expired;
  10. 10 redisDb *db = server.db+j;
  11. 11
  12. 12 /* Continue to expire if at the end of the cycle more than 25%
  13. 13 * of the keys were expired. */
  14. 14 do {
  15. 15 long num = dictSize(db->expires);
  16. 16 time_t now = time(NULL);
  17. 17
  18. 18 expired = 0;
  19. 19 if (num > REDIS_EXPIRELOOKUPS_PER_CRON)
  20. 20 num = REDIS_EXPIRELOOKUPS_PER_CRON;
  21. 21 while (num--) {
  22. 22 dictEntry *de;
  23. 23 time_t t;
  24. 24
  25. 25 if ((de = dictGetRandomKey(db->expires)) == NULL) break;
  26. 26 t = (time_t) dictGetEntryVal(de);
  27. 27 if (now > t) {
  28. 28 sds key = dictGetEntryKey(de);
  29. 29 robj *keyobj = createStringObject(key,sdslen(key));
  30. 30
  31. 31 propagateExpire(db,keyobj);
  32. 32 dbDelete(db,keyobj);
  33. 33 decrRefCount(keyobj);
  34. 34 expired++;
  35. 35 server.stat_expiredkeys++;
  36. 36 }
  37. 37 }
  38. 38 } while (expired > REDIS_EXPIRELOOKUPS_PER_CRON/4);
  39. 39 }
  40. 40 }
  41. 41

这个函数的意图已经有说明: 删一点点过期 key ,如果过期 key 较少,那也只用一点点 cpu 。 25 行随机取一个 key , 38 行删 key 成功的概率较低就退出。这个函数被放在一个 cron 里,每毫秒被调用一次。这个算法保证每次会删除一定比例的 key ,但是如果 key 总量很大,而这个比例控制的太大,就需要更多次的循环,浪费 cpu ,控制的太小,过期的 key 就会变多,浪费内存——这就是时空权衡了。

 

最后在 dbDelete 的调用者里还发现这样一个函数:

  1. /* This function gets called when 'maxmemory' is set on the config file to limit
  2. * the max memory used by the server, and we are out of memory.
  3. * This function will try to, in order:
  4. *
  5. * - Free objects from the free list
  6. * - Try to remove keys with an EXPIRE set
  7. *
  8. * It is not possible to free enough memory to reach used-memory < maxmemory
  9. * the server will start refusing commands that will enlarge even more the
  10. * memory usage.
  11. */
  12. void freeMemoryIfNeeded(void)

这个函数太长就不再详述了,注释部分说明只有在配置文件中设置了最大内存时候才会调用这个函数,而设置这个参数的意义是,你把 redis 当做一个内存 cache 而不是 key-value 数据库。

 

以上 3 种删除过期 key 的途径,第二种定期删除一定比例的 key 是主要的删除途径,第一种“读时删除”保证过期 key 不会被访问到,第三种是一个当内存超出设定时的暴力手段。由此也能看出 redis 设计的巧妙之处。