深入理解redis_memcached失效原理

         最近项目上出现了一个无法理解的BUG,用户默认每天享有一定次数的权限,使用完毕则无法享用,第二天才能再继续。本质就是redis缓存过期嘛,让它凌晨12点失效就好了。
         但是问题发生了,它就是没失效...深究其原因,竟是由于零界点没处理好的锅,服务器时间与request时间是有些许时间差的,key的expire到了一定数量处理也是需要时间哒,就这俩主因。加个存在判断,失效给他提前个10秒留个准备就好了~
        所以吸取教训,好好了解一下这个失效机理,同时也提醒各位别范这种低级错误。
    

 一。如何才能触发key的失效?


除了调用PERSIST命令外,还有没有其他情况会撤销一个主键的失效时间? 答案是肯定的。
    1.在通过 DEL 命令删除一个主键时
    2.在一个设置了失效时间的主键被更新覆盖时,该主键的失效时间也会被撤销。
    3.特殊的命令就是 RENAME,当我们使用 RENAME 对一个主键进行重命名后,之前关联的失效时间会自动传递给新的主键,但是如果一个主键是被RENAME所覆盖的话(如主键 hello 可能会被命令 RENAME world hello 所覆盖),这时被覆盖主键的失效时间会被自动撤销,而新的主键则继续保持原来主键的特性。
     注意这里所说的是主键被更新覆盖,而不是主键对应的 Value 被更新覆盖,因此 SET、MSET 或者是 GETSET 可能会导致主键被更新覆盖而像 INCR、DECR、LPUSH、HSET 等都是更新主键对应的值,这类操作是不会触碰主键的失效时间的


 二。Redis是如何管理和维护主键的


1).Redis的存储结构

typedef struct redisDb {
     dict *dict;//存储主键和值的映射
    dict *expires;//存储主键和过期时间的映射

    dict   * blocking_keys;
    dict  * ready_keys;
    dict  * watched_keys;
     int  id;
} redisDb;

主要看前两个结构就好,
dict:        用来维护一个 Redis 数据库中包含的所有 Key-Value 对(其结构可以理解为 dict[key]:value,即主键与值之间的映射)
expires:     用于维护一个 Redis 数据库中设置了失效时间的主键(其结构可以理解为 expires[key]:timeout,即主键与失效时间的映射)。
设置了失效时间的主键和具体的失效时间全部都维护在 expires 这个字典表中
    当我们使用 EXPIRE、EXPIREAT、PEXPIRE 和 PEXPIREAT 命令设置一个主键的失效时间时,
Redis 首先到 dict 这个字典表中查找要设置的主键是否存在,如果存在就将这个主键和失效时间添加到 expires 这个字典表。


2).消极方法
1.expireIfNeeded函数
 触发:
    这个函数在任何访问数据的函数中都会被调用
    Redis 在实现 GET、MGET、HGET、LRANGE 等所有涉及到读取数据的命令时都会调用它
 意义:
    在读取数据之前先检查一下该key有没有失效,如果失效了就删除它。

2.propagateExpire函数(在上边一个函数中调用) 主要函数
触发:
执行上一个函数时,它在其里边
意义:
用来在正式删除失效主键之前广播这个主键已经失效的信息
操作:
    (1).一个是发送到  AOF文件,将删除失效主键的这一操作以 DEL Key 的标准命令格式 记录下来;
    (2).发送到当前 Redis 服务器的所有 Slave,同样将删除失效主键的这一操作以 DEL Key 的标准命令格式 告知这些 Slave  删除各自的失效主键


     以上我们了解了 Redis 是如何以一种消极的方式删除失效主键的,但是仅仅通过这种方式显然是不够的,因为如果某些失效的主键迟迟等不到再次访问的话,Redis 就永远不会知道这些主键已经失效,也就永远也不会删除它们了,这无疑会导致内存空间的浪费。所以有了下边的方法。


3).积极方法:(该方法利用 Redis 的时间事件来实现,即每隔一段时间就中断一下完成一些指定操作)

 1.serverCron函数:
 触发:
它在 Redis 服务器启动时创建
 作用:
这个回调函数不仅要进行失效主键的检查与删除,还要进行统计信息的更新、客户端连接超时的控制、BGSAVE 和 AOF 的触发等等

2.activeExpireCycle函数:
触发:
    执行上一个函数时,它在其里边 每秒的执行次数由宏定义 【 REDIS_DEFAULT_HZ 】 来指定,默认每秒钟执行10次。

操作:
    a).遍历处理 Redis 服务器中每个数据库的  expires 字典表中,从中尝试着随机抽样【 REDIS_EXPIRELOOKUPS_PER_CRON】(默认 值为10)个设置了失效时间的主键,
    b).检查它们是否已经失效并删除掉失效的主键,
    c).如果失效的主键个数占本次抽样个数的比例超过25%,Redis 会认为当前数据库中的失效主键依然很多,所以它会继续进行下一轮的随机抽样和删除,
    d).直到刚才的比例低于25%才停止对当前数据库的处理,转向下一个数据库。

其他:
    activeExpireCycle 函数避免失效主键删除占用过多的CPU资源,所以其 不会试图一次性处理Redis中的所有数据库,而是最多只处理  REDIS_DBCRON_DBS_PER_CALL (默认值为16)个库, 有处理时间上的限制



三。Memcached 删除失效主键的方法与 Redis 有何异同?

    首先,Memcached 在删除失效主键时也是采用的消极方法,即 Memcached 内部也不会监视主键是否失效,而是在通过 Get 访问主键时才会检查其是否已经失效。
    其次,Memcached 与 Redis 在主键失效机制上的最大不同是,Memcached 不会像 Redis 那样真正地去删除失效的主键,而只是简单地将失效主键占用的空间回收。这样当有新的数据写入到系统中时,Memcached 会优先使用那些失效主键的空间。如果失效主键的空间用光了,Memcached 还可以通过 LRU 机制来回收那些长期得不到访问的空间,
    因此 Memcached 并不需要像 Redis 中那样的周期性删除操作,这也是由 Memcached 使用 内存理机制决定的。同时,这里需要指出的是 Redis 在出现 OOM 时同样可以通过配置 maxmemory-policy 这个参数来决定是否采用 LRU 机制来回收内存空间
    

四。总结:

    redis 每秒执行10次 过期 检查,每次中,随机从 某个库 的expire表中 抽取10个key ,检测他是否失效,若失效则删除。当 失效比例超过1/4 ,本次重新执行随机抽取10key,不计入10次中的1次,直到这一秒的10次都执行完。

问:
    那么有人问了,万一,万一!失效的key做足够的多,1秒的这10次都没执行完又到下一秒了,咋整?
答:
    redis有检测机制的,不会让它把CPU拖死的:
    a.每次处理数据库个数的限制、
    b.activeExpireCycle 函数在一秒钟内执行次数的限制、
    c.分配给 activeExpireCycle函数CPU时间的限制、
    d.继续删除主键的失效主键数百分比的限制,
    Redis 已经大大降低了主键失效机制对系统整体性能的影响
    所以由此也可得,设置失效时间的原则:尽可能避免在同一时间点的大批量key失效,它是需要处理时间的。





消极失效主要函数 .propagateExpire函数

void propagateExpire(redisDb  * db, robj  * key ) {
    robj 
* argv [ 2 ] ;
    
// shared.del是在Redis服务器启动之初就已经初始化好的一个常用Redis对象,即DEL命令
    argv
[ 0 ]   =  shared.del;
    argv
[ 1 ]   =   key ;
    incrRefCount(argv
[ 0 ] );
    incrRefCount(argv
[ 1 ] );
    
// 检查Redis服务器是否开启了AOF,如果开启了就为失效主键记录一条DEL日志
    
if  (server.aof_state  !=  REDIS_AOF_OFF)
        feedAppendOnlyFile(server.delCommand,db
-> id,argv, 2 );
    
// 检查Redis服务器是否拥有Slave,如果是就向所有Slave发送DEL失效主键的命令,这就是
    
// 上面expireIfNeeded函数中发现自己是Slave时无需主动删除失效主键的原因了,因为它
    
// 只需听从Master发送过来的命令就OK了
    
if  (listLength(server.slaves))
        replicationFeedSlaves(server.slaves,db
-> id,argv, 2 );
    decrRefCount(argv
[ 0 ] );
    decrRefCount(argv
[ 1 ] );
}

 

积极失效主要函数.activeExpireCycle函数

void activeExpireCycle(void) {
    
// 因为每次调用activeExpireCycle函数不会一次性检查所有Redis数据库,所以需要记录下
    
// 每次函数调用处理的最后一个Redis数据库的编号,这样下次调用activeExpireCycle函数
    
// 还可以从这个数据库开始继续处理,这就是current_db被声明为static的原因,而另外一
    
// 个变量timelimit_exit是为了记录上一次调用activeExpireCycle函数的执行时间是否达
    
// 到时间限制了,所以也需要声明为static
    static unsigned 
int  current_db  =   0 ;
    static 
int  timelimit_exit  =   0 ;
    unsigned 
int  j, iteration  =   0 ;
    
// 每次调用activeExpireCycle函数处理的Redis数据库个数为REDIS_DBCRON_DBS_PER_CALL
    unsigned 
int  dbs_per_call  =  REDIS_DBCRON_DBS_PER_CALL;
    
long   long  start  =  ustime(), timelimit;
    
// 如果当前Redis服务器中的数据库个数小于REDIS_DBCRON_DBS_PER_CALL,则处理全部数据库,
    
// 如果上一次调用activeExpireCycle函数的执行时间达到了时间限制,说明失效主键较多,也
    
// 会选择处理全部数据库
    
if  (dbs_per_call  >  server.dbnum  ||  timelimit_exit)
        dbs_per_call 
=  server.dbnum;
    
// 执行activeExpireCycle函数的最长时间(以微秒计),其中REDIS_EXPIRELOOKUPS_TIME_PERC
    
// 是单位时间内能够分配给activeExpireCycle函数执行的CPU时间比例,默认值为25,server.hz
    
// 即为一秒内activeExpireCycle的调用次数,所以这个计算公式更明白的写法应该是这样的,即
    (
1000000   *  (REDIS_EXPIRELOOKUPS_TIME_PERC  /   100 ))  /  server.hz
    timelimit 
=   1000000 * REDIS_EXPIRELOOKUPS_TIME_PERC / server.hz / 100 ;
    timelimit_exit 
=   0 ;
    
if  (timelimit  <=   0 ) timelimit  =   1 ;
    
// 遍历处理每个Redis数据库中的失效数据
    
for  (j  =   0 ; j  <  dbs_per_call; j ++ ) {
        
int  expired;
        redisDb 
* db  =  server.db + (current_db  %  server.dbnum);
        
// 此处立刻就将current_db加一,这样可以保证即使这次无法在时间限制内删除完所有当前
        
// 数据库中的失效主键,下一次调用activeExpireCycle一样会从下一个数据库开始处理,
        
// 从而保证每个数据库都有被处理的机会
        current_db
++ ;
        
// 开始处理当前数据库中的失效主键
        do {
            unsigned 
long  num, slots;
            
long   long  now;
            
// 如果expires字典表大小为0,说明该数据库中没有设置失效时间的主键,直接检查下
            
// 一数据库
            
if  ((num  =  dictSize(db -> expires))  ==   0 break ;
            slots 
=  dictSlots(db -> expires);
            now 
=  mstime();
            
// 如果expires字典表不为空,但是其填充率不足1 % ,那么随机选择主键进行检查的代价
            
// 会很高,所以这里直接检查下一数据库
            
if  (num  &&  slots  >  DICT_HT_INITIAL_SIZE  &&
                (num
* 100 / slots  <   1 ))  break ;
            expired 
=   0 ;
            
// 如果expires字典表中的entry个数不足以达到抽样个数,则选择全部key作为抽样样本
            
if  (num  >  REDIS_EXPIRELOOKUPS_PER_CRON)
                num 
=  REDIS_EXPIRELOOKUPS_PER_CRON;
            
while  (num -- ) {
                dictEntry  * de;
                
long   long  t;
                
// 随机获取一个设置了失效时间的主键,检查其是否已经失效
                
if  ((de  =  dictGetRandomKey(db -> expires))  ==   NULL break ;
                t 
=  dictGetSignedIntegerVal(de);
                
if  (now  >  t) {
                    
// 发现该主键确实已经失效,删除该主键
                    sds 
key   =  dictGetKey(de);
                    robj 
* keyobj  =  createStringObject( key ,sdslen( key ));
                    
// 同样要在删除前广播该主键的失效信息
                    propagateExpire(db,keyobj);
                    dbDelete(db,keyobj);
                    decrRefCount(keyobj);
                    expired
++ ;
                    server.stat_expiredkeys
++ ;
                }
            }
            
// 每进行一次抽样删除后对iteration加一,每16次抽样删除后检查本次执行时间是否
            
// 已经达到时间限制,如果已达到时间限制,则记录本次执行达到时间限制并退出
            iteration
++ ;
            
if  ((iteration  &   0xf ==   0   &&
                (ustime()
- start)  >  timelimit)
            {
                timelimit_exit 
=   1 ;
                
return ;
            }
        
// 如果失效的主键数占抽样数的百分比大于25 % ,则继续抽样删除过程
        } 
while  (expired  >  REDIS_EXPIRELOOKUPS_PER_CRON / 4 );
    }
}

  


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值