`
san_yun
  • 浏览: 2654611 次
  • 来自: 杭州
文章分类
社区版块
存档分类
最新评论

redis expires key不会自动删除的问题

 
阅读更多

最近发现线上的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

  • 大小: 37.6 KB
分享到:
评论

相关推荐

    面试官:Redis 过期删除策略和内存淘汰策略有什么区别?.doc

    定时删除策略是在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作。惰性删除策略是在每次访问 key 时,检查该 key 是否已经过期,如果已经过期,则删除该 key。...

    redis客户端连接、spring boot整合、分布式锁.zip

    分布式锁是 Redis 在分布式系统中的重要应用场景,它可以解决多节点共享资源的问题,防止并发操作导致的数据不一致。Redis 提供了 SETNX (Set if Not eXists) 和 EXPIRE 命令组合实现简单版本的分布式锁。在 Java ...

    redis数据库查找key在内存中的位置的方法

    Redis 是一个广泛使用的开源键值存储...了解这些内部机制对于优化 Redis 应用和排查问题至关重要。在实际应用中,还可以通过调整哈希表负载因子、合理设置数据库数量和监控 rehash 进程来提高 Redis 的性能和稳定性。

    深入理解redis_memcached失效原理(小结)

    1. Redis 的内部数据结构包含两个重要的字典:`dict` 存储 key-value 映射,`expires` 存储 key 和其失效时间。当使用 `EXPIRE` 系列命令设置 key 的失效时间时,Redis 会在两个字典中分别更新对应条目。 2. Redis ...

    Springboot整合redis使用技巧.pdf

    Spring Boot 整合 Redis 是现代 Web 应用中常见的数据缓存策略,它极大地提高了应用程序的性能和响应速度。Spring Boot 提供了便捷的方式与 Redis 集成,利用其强大的缓存功能。以下是关于 Spring Boot 整合 Redis ...

    Redisdemo.zip

    **设置键的时效性**:Redis支持为键设置过期时间,使用`KeyExpire()`或`StringSet()`的`expires`参数: ```csharp // 使用KeyExpire db.KeyExpire("key", DateTimeOffset.UtcNow.AddSeconds(10)); // 或者在设置键...

    Redis安装步骤

    Redis是一种分布式的、非关系型的(key-value)数据库系统,因其高效性和灵活性,在缓存、消息队列处理等方面有着广泛的应用。Redis支持多种数据结构如字符串(strings)、散列(hashes)、列表(lists)、集合(sets...

    redis过期策略和内存淘汰机制.pdf

    Redis的内存管理机制是通过redisDb结构体来实现的,该结构体包含了dict、expires两个成员变量,其中dict用于存储key-value数据,expires用于存储key的过期时间。 在Redis中,freeMemoryIfNeeded()函数用于释放不...

    Redis面试题及答案.docx

    Redis 是一个基于内存的高性能 key-value 数据库,它的特点是支持保存多种数据结构,并且性能非常出色,每秒可以处理超过 10 万次读写操作。下面是对 Redis 的详细介绍: Redis 的特点 Redis 是一个基于内存的高...

    redis内存存储结构分析

    4. **数据生命周期管理**:利用Redis提供的过期机制自动删除不再使用的数据。 5. **精细配置**:合理设置Redis的最大内存限制,并结合内存淘汰策略进行管理。 通过对Redis内存存储结构的深入理解以及采取适当的优化...

    Redis 35 道面试题及答案.docx

    * 丰富的特性:可用于缓存,消息,按 key 设置过期时间,过期后将会自动删除。 知识点3:Redis 和 Memcached 的区别 Redis 和 Memcached 都是内存数据库,但是它们有以下区别: * 存储方式:Memecache 把数据全部...

    Redis V3.0 中文文档

    自动创建和删除键. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 Redis 哈希/散列 (Hashes). . . . . . . . . . . . . . . . . . . . ....

    Springboot整合redis使用技巧.docx

    @Cacheable(value = "cache", key = "key") // 使用默认的 RedisCacheManager @Cacheable(cacheNames = "guavaCacheManager", value = "guavaCache", key = "key") // 使用 GuavaCacheManager ``` #### 五、Redis ...

    Redis 3.0 中文版 - v1.1.pdf

    自动创建和删除键. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32 Redis 哈希/散列 (Hashes). . . . . . . . . . . . . . . . . . . . ....

    Redis过期-淘汰机制的解析和内存占用过高的解决方案.docx

    定期过期是指每隔一定的时间,会扫描一定数量的数据库的 expires 字典中一定数量的key,并清除其中已过期的 key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下...

Global site tag (gtag.js) - Google Analytics