Redis数据过期和淘汰策略详解

背景

Redis作为一个高性能的内存NoSQL数据库,其容量受到最大内存限制的限制。

用户在使用阿里云Redis时,除了对性能,稳定性有很高的要求外,对内存占用也比较敏感。在使用过程中,有些用户会觉得自己的线上实例内存占用比自己预想的要大。

事实上,实例中的内存除了保存原始的键值对所需的开销外,还有一些运行时产生的额外内存,包括:

  1. 垃圾数据和过期Key所占空间
  2. 字典渐进式Rehash导致未及时删除的空间
  3. Redis管理数据,包括底层数据结构开销,客户端信息,读写缓冲区等
  4. 主从复制,bgsave时的额外开销
  5. 其它

本系列文章主要分析这些在Redis中产生的原因,带来的影响和规避的方式。

本文主要分析第一项Redis过期策略对内存的影响。

Redis过期数据清理策略

过期数据清理时机

为了防止一次性清理大量过期Key导致Redis服务受影响,Redis只在空闲时清理过期Key。

具体Redis逐出过期Key的时机为:

  1. 访问Key时,会判断Key是否过期,逐出过期Key;

    robj lookupKeyRead(redisDb db, robj *key) {
    robj *val;
    expireIfNeeded(db,key);
    val = lookupKey(db,key);
    ...
    return val;
    }
    
  2. CPU空闲时在定期serverCron任务中,逐出部分过期Key;
        aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL)
    
        int serverCron(struct aeEventLoop eventLoop, long long id, void clientData) {
            ...
            databasesCron();
            ...
        }
    
        void databasesCron(void) {
            /* Expire keys by random sampling. Not required for slaves
             + as master will synthesize DELs for us. */
            if (server.active_expire_enabled && server.masterhost == NULL)
                activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
            ...
        }
    
  3. 每次事件循环执行的时候,逐出部分过期Key;
        void aeMain(aeEventLoop *eventLoop) {
            eventLoop->stop = 0;
            while (!eventLoop->stop) {
                if (eventLoop->beforesleep != NULL)
                    eventLoop->beforesleep(eventLoop);
                aeProcessEvents(eventLoop, AE_ALL_EVENTS);
            }
        }
    
        void beforeSleep(struct aeEventLoop *eventLoop) {
            ...
            /* Run a fast expire cycle (the called function will return
             - ASAP if a fast cycle is not needed). */
            if (server.active_expire_enabled && server.masterhost == NULL)
                activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST);
            ...
        }
    

过期数据清理算法

Redis过期Key清理的机制对清理的频率和最大时间都有限制,在尽量不影响正常服务的情况下,进行过期Key的清理,以达到长时间服务的性能最优.

Redis会周期性的随机测试一批设置了过期时间的key并进行处理。测试到的已过期的key将被删除。具体的算法如下:

  1. Redis配置项hz定义了serverCron任务的执行周期,默认为10,即CPU空闲时每秒执行10次;
  2. 每次过期key清理的时间不超过CPU时间的25%,即若hz=1,则一次清理时间最大为250ms,若hz=10,则一次清理时间最大为25ms;
  3. 清理时依次遍历所有的db;
  4. 从db中随机取20个key,判断是否过期,若过期,则逐出;
  5. 若有5个以上key过期,则重复步骤4,否则遍历下一个db;
  6. 在清理过程中,若达到了25%CPU时间,退出清理过程;

这是一个基于概率的简单算法,基本的假设是抽出的样本能够代表整个key空间,redis持续清理过期的数据直至将要过期的key的百分比降到了25%以下。这也意味着在长期来看任何给定的时刻已经过期但仍占据着内存空间的key的量最多为每秒的写操作量除以4.

  • 由于算法采用的随机取key判断是否过期的方式,故几乎不可能清理完所有的过期Key;
  • 调高hz参数可以提升清理的频率,过期key可以更及时的被删除,但hz太高会增加CPU时间的消耗;Redis作者关于hz参数的一些讨论

代码分析如下:

void activeExpireCycle(int type) {
    ...
    /* We can use at max ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC percentage of CPU time
     * per iteration. Since this function gets called with a frequency of
     * server.hz times per second, the following is the max amount of
      microseconds we can spend in this function. /
    // 最多允许25%的CPU时间用于过期Key清理
    // 若hz=1,则一次activeExpireCycle最多只能执行250ms
    // 若hz=10,则一次activeExpireCycle最多只能执行25ms
    timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
    ...
    // 遍历所有db
    for (j = 0; j < dbs_per_call; j++) {
        int expired;
        redisDb *db = server.db+(current_db % server.dbnum);

        /* Increment the DB now so we are sure if we run out of time
         * in the current DB we'll restart from the next. This allows to
          distribute the time evenly across DBs. /
        current_db++;

        /* Continue to expire if at the end of the cycle more than 25%
          of the keys were expired. /
        do {
            ...
            // 一次取20个Key,判断是否过期
            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;

            while (num--) {
                dictEntry *de;
                long long ttl;

                if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                ttl = dictGetSignedIntegerVal(de)-now;
                if (activeExpireCycleTryExpire(db,de,now)) expired++;
            }

            if ((iteration & 0xf) == 0) { / check once every 16 iterations. /
                long long elapsed = ustime()-start;
                latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
                if (elapsed > timelimit) timelimit_exit = 1;
            }
            if (timelimit_exit) return;
            /* We don't repeat the cycle if there are less than 25% of keys
              found expired in the current DB. /
            // 若有5个以上过期Key,则继续直至时间超过25%的CPU时间
            // 若没有5个过期Key,则跳过。
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    }
}

Redis数据逐出策略

数据逐出时机

// 执行命令
int processCommand(redisClient *c) {
        ...
        /* Handle the maxmemory directive.
        **
        First we try to free some memory if possible (if there are volatile
        * keys in the dataset). If there are not the only thing we can do
         is returning an error. /
        if (server.maxmemory) {
            int retval = freeMemoryIfNeeded();
            ...
    }
    ...
}

数据逐出算法

在逐出算法中,根据用户设置的逐出策略,选出待逐出的key,直到当前内存小于最大内存值为主.

可选逐出策略如下:

  • volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用 的数据淘汰
  • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数 据淘汰
  • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据 淘汰
  • allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
  • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  • no-enviction(驱逐):禁止驱逐数据

具体代码如下

int freeMemoryIfNeeded() {
    ...
    // 计算mem_used
    mem_used = zmalloc_used_memory();
    ...

    / Check if we are over the memory limit. /
    if (mem_used <= server.maxmemory) return REDIS_OK;

    // 如果禁止逐出,返回错误
    if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)
        return REDIS_ERR; / We need to free memory, but policy forbids. /

    mem_freed = 0;
    mem_tofree = mem_used - server.maxmemory;
    long long start = ustime();
    latencyStartMonitor(latency);
    while (mem_freed < mem_tofree) {
        int j, k, keys_freed = 0;

        for (j = 0; j < server.dbnum; j++) {
            // 根据逐出策略的不同,选出待逐出的数据
            long bestval = 0; / just to prevent warning /
            sds bestkey = NULL;
            struct dictEntry *de;
            redisDb *db = server.db+j;
            dict *dict;

            if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
            {
                dict = server.db[j].dict;
            } else {
                dict = server.db[j].expires;
            }
            if (dictSize(dict) == 0) continue;

            / volatile-random and allkeys-random policy /
            if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||
                server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM)
            {
                de = dictGetRandomKey(dict);
                bestkey = dictGetKey(de);
            }

            / volatile-lru and allkeys-lru policy /
            else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
            {
                for (k = 0; k < server.maxmemory_samples; k++) {
                    sds thiskey;
                    long thisval;
                    robj *o;

                    de = dictGetRandomKey(dict);
                    thiskey = dictGetKey(de);
                    /* When policy is volatile-lru we need an additional lookup
                      to locate the real key, as dict is set to db->expires.  */
                    if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
                        de = dictFind(db->dict, thiskey);
                    o = dictGetVal(de);
                    thisval = estimateObjectIdleTime(o);

                    / Higher idle time is better candidate for deletion /
                    if (bestkey == NULL || thisval > bestval) {
                        bestkey = thiskey;
                        bestval = thisval;
                    }
                }
            }

            / volatile-ttl /
            else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {
                for (k = 0; k < server.maxmemory_samples; k++) {
                    sds thiskey;
                    long thisval;

                    de = dictGetRandomKey(dict);
                    thiskey = dictGetKey(de);
                    thisval = (long) dictGetVal(de);

                    /* Expire sooner (minor expire unix timestamp) is better
                      candidate for deletion */
                    if (bestkey == NULL || thisval < bestval) {
                        bestkey = thiskey;
                        bestval = thisval;
                    }
                }
            }

            / Finally remove the selected key. */
            // 逐出挑选出的数据
            if (bestkey ) {
                ...
                delta = (long long) zmalloc_used_memory();
                dbDelete(db,keyobj);
                delta -= (long long) zmalloc_used_memory();
                mem_freed += delta;
                ...
            }
        }
        ...
    }
    ...
    return REDIS_OK;
}

相关最佳实践

  • 不要放垃圾数据,及时清理无用数据
    实验性的数据和下线的业务数据及时删除;
  • key尽量都设置过期时间
    对具有时效性的key设置过期时间,通过redis自身的过期key清理策略来降低过期key对于内存的占用,同时也能够减少业务的麻烦,不需要定期手动清理了.
  • 单Key不要过大
    给用户排查问题时遇到过单个string的value有43M的,也有一个list 100多万个大成员占了1G多内存的。这种key在get的时候网络传输延迟会比较大,需要分配的输出缓冲区也比较大,在定期清理的时候也容易造成比较高的延迟. 最好能通过业务拆分,数据压缩等方式避免这种过大的key的产生。
  • 不同业务如果公用一个业务的话,最好使用不同的逻辑db分开
    从上面的分析可以看出,Redis的过期Key清理策略和强制淘汰策略都会遍历各个db。将key分布在不同的db有助于过期Key的及时清理。另外不同业务使用不同db也有助于问题排查和无用数据的及时下线.
时间: 2024-09-17 04:19:36

Redis数据过期和淘汰策略详解的相关文章

ApsaraDB for Redis之内存去哪儿了(一)数据过期与逐出策略

背景 Redis作为一个高性能的内存NoSQL数据库,其容量受到最大内存限制的限制. 用户在使用阿里云Redis时,除了对性能,稳定性有很高的要求外,对内存占用也比较敏感.在使用过程中,有些用户会觉得自己的线上实例内存占用比自己预想的要大. 事实上,实例中的内存除了保存原始的键值对所需的开销外,还有一些运行时产生的额外内存,包括: 垃圾数据和过期Key所占空间 字典渐进式Rehash导致未及时删除的空间 Redis管理数据,包括底层数据结构开销,客户端信息,读写缓冲区等 主从复制,bgsave时

通达OA 使用Ajax和工作流插件实现根据人力资源系统数据增加OA账号(图文详解)_AJAX相关

本次小飞鱼开发的程序主要解决某下属公司在人力系统中增加账号不能马上审批完毕的问题,可以通过这个流程审批后由插件在后台判断自动增加OA账号,增加机制与hr与OA系统同步相同. 只进行增加操作,没有修改.删除的操作.原有已经进行了两个系统的数据自动同步开发,因此这次的开发属于一个补充的内容,仅在此提供一个应用的思路和开发过程的探讨. 前端发起人申请时填写hr系统中已经分配的工号,即可对应查询出其他相关数据.为了避免查出数据后对工号修改,增加一个确认工号输入框.其他信息由Ajax自动获取为只读形式.这

python类:class创建、数据方法属性及访问控制详解_python

在Python中,可以通过class关键字定义自己的类,然后通过自定义的类对象类创建实例对象. python中创建类 创建一个Student的类,并且实现了这个类的初始化函数"__init__": class Student(object):     count = 0     books = []     def __init__(self, name):         self.name = name 接下来就通过上面的Student类来看看Python中类的相关内容. 类构造和

使用Java构造和解析Json数据的两种方法(详解二)_java

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,采用完全独立于语言的文本格式,是理想的数据交换格式.同时,JSON是 JavaScript 原生格式,这意味着在 JavaScript 中处理 JSON数据不须要任何特殊的 API 或工具包. 在www.json.org上公布了很多JAVA下的json构造和解析工具,其中org.json和json-lib比较简单,两者使用上差不多但还是有些区别.下面接着介绍用org.json构造和解析Json数据的方法

Redis 数据过期策略

1.设置过期时间 expire key time(以秒为单位)--这是最常用的方式 setex(String key, int seconds, String value)–字符串独有的方式 注意: 除了字符串自己独有设置过期时间的方法外,其他方法都需要依靠expire方法来设置时间 如果没有设置时间,那缓存就是永不过期 如果设置了过期时间,之后又想让缓存永不过期,使用persist key 2.三种过期策略 Redis key过期的方式有三种:     被动删除:当读/写一个已经过期的key时

MySQL的数据类型和建库策略详解

mysql|策略|数据|数据类型|详解 无论是在小得可怜的免费数据库空间或是大型电子商务网站,合理的设计表结构.充分利用空间是十分必要的.这就要求我们对数据库系统的常用数据类型有充分的认识.下面我就将我的一点心得写出来跟大家分享. 一.数字类型.数字类型按照我的分类方法分为三类:整数类.小数类和数字类. 我所谓的"数字类",就是指DECIMAL和NUMERIC,它们是同一种类型.它严格的说不是一种数字类型,因为他们实际上是将数字以字符串形式保存的:他的值的每一位(包括小数点)占一个字节

深入分析redis cluster 集群安装配置详解

Redis 集群是一个提供在多个Redis间节点间共享数据的程序集.redis3.0以前,只支持主从同步的,如果主的挂了,写入就成问题了.3.0出来后就可以很好帮我们解决这个问题. 目前redis 3.0还不稳定,如果要用在生产环境中,要慎重. 一,redis服务器说明 192.168.10.219 6379  192.168.10.219 6380  192.168.10.219 6381    192.168.10.220 6382  192.168.10.220 6383  192.168

预告:大数据(hadoop\spark)解决方案构建详解,以阿里云E-MapReduce为例

在2016年09月22日 20:00 - 21:00笔者将在 CSDN学院,分享<大数据解决方案构建详解 :以阿里云E-MapReduce为例>欢迎大家报名,报名链接:http://edu.csdn.net/huiyiCourse/detail/186 ppt地址:https://yq.aliyun.com/attachment/download/?spm=0.0.0.0.PpnciK&filename=%E5%A4%A7%E6%95%B0%E6%8D%AE%E8%A7%A3%E5%8

Redis SORT排序命令使用方法详解

  对于Redis SORT排序命令 我相信大家都不怎么了解了,因此小编整理了一些Redis SORT排序命令使用方法与例子,希望例子可以对各位玩家带来帮助哦. Redis SORT是由Redis提供的一个排序命令.集合中的标签是无序的,可以使用SORT排序.如: redis>SADD jihe 5 (integer) 1 redis>SADD jihe 1 (integer) 1 redis>SADD jihe 2 (integer) 1 redis>SADD jihe 8 (i