深入理解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);
  }
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 在windows系统下如何安装memcached的讲解

    Memcached 作为一个高性能的分布式内存对象缓存系统,通常被用于动态Web应用以减轻数据库负载.它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高动态.数据库驱动网站的速度.Memcached基于"Key=>Value"方式组织数据,基于网络连接方式完成服务.需要注意的是memcached使用内存管理数据,所以它是易失的,当服务器重启,或者memcached进程中止,数据便会丢失,所以memcached不能用来持久保存数据. 下面我们讲解一下在windows系统下

  • ThinkPHP框架中使用Memcached缓存数据的方法

    本文实例讲述了ThinkPHP框架中使用Memcached缓存数据的方法.分享给大家供大家参考,具体如下: ThinkPHP默认使用文件缓存数据,支持Memcache等其他缓存方式,有两个PHP扩展:Memcache和Memcached,Memcahe官方有说明,主要说一下Memcached. 相对于PHP Memcache,php Memcached是基于原生的c的libmemcached的扩展,更加完善,建议替换为php memcached. 版本3.2.2开始内置了Memcached驱动(

  • .NET Core中使用Redis与Memcached的序列化问题详析

    前言 在使用分布式缓存的时候,都不可避免的要做这样一步操作,将数据序列化后再存储到缓存中去. 序列化这一操作,或许是显式的,或许是隐式的,这个取决于使用的package是否有帮我们做这样一件事. 本文会拿在.NET Core环境下使用Redis和Memcached来当例子说明,其中,Redis主要是用StackExchange.Redis,Memcached主要是用EnyimMemcachedCore. 先来看看一些我们常用的序列化方法. 常见的序列化方法 或许,比较常见的做法就是将一个对象序列

  • 解决 .NET Core 中 GetHostAddressesAsync 引起的 EnyimMemcached 死锁问题

    在我们将站点从 ASP.NET + Windows 迁移至 ASP.NET Core + Linux 的过程中,目前遇到的最大障碍就是 -- 没有可用的支持 .NET Core 的 memcached 客户端. 我们一直用的是 EnyimMemcached ,在没有其它选择的情况下,我们自己尝试着将 EnyimMemcached 迁移至 .NET Core...基于 .NET Core 修改好了代码,在开发环境下测试通过,在 Linux 服务器上自己访问很正常(没有并发访问量),但是只要接入一定

  • Laravel使用memcached缓存对文章增删改查进行优化的方法

    本文实例讲述了Laravel使用memcached缓存对文章增删改查进行优化的方法.分享给大家供大家参考,具体如下: 这里我们将以文章的增删改查作为实例系统讲述缓存的使用,这个实例是对之前创建RESTFul风格控制器实现文章增删改查这篇教程的改造和升级,我们将在其基础上融合进Eloquent ORM和模型事件,将应用的场景直接拉到生成环境. 1.准备工作 路由及控制器 路由的定义和控制器的创建保持和创建RESTFul风格控制器实现文章增删改查中一样. 创建数据表 关于文章对应数据表我们在数据库部

  • PHP内存缓存功能memcached示例

    下文简单介绍了memcached类的应用示例,具有一定的参考价值,感兴趣的小伙伴们可以参考一下. 一.memcached 简介 在很多场合,我们都会听到 memcached 这个名字,但很多同学只是听过,并没有用过或实际了解过,只知道它是一个很不错的东东.这里简单介绍一下,memcached 是高效.快速的分布式内存对象缓存系统,主要用于加速 WEB 动态应用程序. 二.memcached 安装 首先是下载 memcached 了,目前最新版本是 1.1.12,直接从官方网站即可下载到 memc

  • 在Linux服务器上安装 memcached的基本操作

    一.memcached的安装 1.下载 memcached-1.4.33.tar.gz.libevent-2.0.22-stable.tar.gz 安装 memcached 依赖 libevent 2.安装 libevent a.解压 [root@iZ28b4kreuaZ webserver]# tar zxvf libevent-2.0.22-stable.tar.gz b.安装在 /usr/local/下 进入解压目录下:[root@iZ28b4kreuaZ libevent-2.0.22-

  • java 使用memcached以及spring 配置memcached完整实例代码

    Memcached是一个高性能的分布式内存对象缓存系统,本文介绍了java 使用memcached以及spring 配置memcached完整实例代码,分享给大家 本文涉及以下内容: 1,要使用的jar包 2,java 使用memcached 3,spring 配置memcached 导入jar java_memcached-release_2.6.6.jar commons-pool-1.5.6.jar slf4j-api-1.6.1.jar slf4j-simple-1.6.1.jar 示例

  • CentOS 7.x安装部署Memcached服务器的详细方法

    操作系统:CentOS 7.x 64位 实现目的:安装部署Memcached服务器 一.防火墙设置 CentOS 7.x默认使用的是firewall作为防火墙,这里改为iptables防火墙. 1.关闭firewall: systemctl stop firewalld.service #停止firewall systemctl disable firewalld.service #禁止firewall开机启动 2.安装iptables防火墙 yum install iptables-servi

  • Laravel Memcached缓存驱动的配置与应用方法分析

    本文实例讲述了Laravel Memcached缓存驱动的配置与应用方法.分享给大家供大家参考,具体如下: Memcached缓存配置在任何php环境下我们都可以配置使用来提升WEB的性能.对于大型网站(数据多,访问量大)而言,缓存系统是必备组件,其为减轻数据库负载.提高页面访问速度.提升系统性能立下汗马功劳.Laravel作为一个功能完善且强大的PHP框架,自然为缓存系统提供了支持.目前Laravle支持的缓存驱动包括文件.数组.数据库.APC.Memcached和Redis,并且为这些驱动提

随机推荐