布景介绍

Redis作为一种高功能的内存NoSQL数据库,其容量受限于最大内存的约束。用户在运用阿里云Redis时,除了对功能和稳定性有较高的要求外,对内存占用也十分灵敏。但是,在实践运用中,一些用户或许会发现他们的线上实例的内存占用比预期的要大。

内存较高的场景

在运用Redis时,以下是一些或许导致内存占用较高的因素:

【Redis深度专题】「核心技术进步」从源码视点探求Redis服务的内存运用、收拾以及逐出等底层完成原理

  • 数据存储格局:Redis支撑不同的数据结构,如字符串、哈希、列表等。不同数据结构的存储方法和编码格局或许会导致不同的内存占用。确保挑选合适的数据结构和存储方法,可以有用地操控内存占用。

  • 数据过期时刻:Redis答应为存储的数据设置过期时刻。假如数据没有正确设置过期时刻或过期时刻设置不合理,或许会导致数据过多地积累在内存中,然后添加了内存占用。

  • 内存碎片化:Redis运用内存分配机制来存储数据,而这或许导致内存碎片化。当有键过期或被删除时,内存不会当即开释,而是会留下一些碎片空间。内存碎片化或许导致整体内存占用率的添加。

下降Redis的内存占用

经过合理的数据结构挑选、设置合理的数据过期时刻和优化内存运用等办法,可以协助下降Redis的内存占用,进步功能和稳定性。定时监控和优化内存运用是继续保持Redis在高功能和低内存占用之间平衡的要害。

【Redis深度专题】「核心技术进步」从源码视点探求Redis服务的内存运用、收拾以及逐出等底层完成原理

  • 优化数据结构和存储方法:依据实践需求挑选合适的数据结构和对应的存储方法,例如运用紧缩列表来存储较短的列表。

  • 合理设置数据过期时刻:为存储的数据设置合适的过期时刻,确保数据按需进行过期,并及时开释内存空间。

  • 运用字节筛选机制:装备合适的内存筛选机制,以便及时收回和开释过期数据和不活跃数据占用的内存空间。

  • 监控和优化内存运用:定时监控Redis实例的内存运用状况,及时发现和处理占用较高的问题,并进行恰当的调优。

额定内存较高的场景

实践上,在Redis实例中,除了保存原始键值对所需的内存开支之外,还存在一些运行时发生的额定内存开支。其中,两个主要因素是:

【Redis深度专题】「核心技术进步」从源码视点探求Redis服务的内存运用、收拾以及逐出等底层完成原理

  • 废物数据和过期键所占空间:在Redis中,当键被删除或过期时,并不会当即开释相应的内存空间。因此,即使键已过期或被删除,仍然会占用内存空间,这或许导致废物数据和过期键占有内存。

  • 渐进式Rehash导致的未及时删除:当Redis进行渐进式Rehash进程时,会在新哈希槽中分配内存空间,并逐步搬迁旧哈希槽中的键。但是,在搬迁期间,假如有新的写入操作,或许会导致旧哈希槽中的键未被及时删除,然后占有了额定的内存空间。

为了优化和下降Redis实例的内存占用,可以考虑以下优化办法:

定时收拾废物数据和过期键

经过恰当设置数据的过期时刻,或运用定时任务进行收拾操作,及时收拾过期的键和废物数据,以开释占用的内存空间。

履行Rehash后进行内存收回

在Redis履行Rehash或运用槽位分配器时,可以经过履行KEYS指令或运用SCAN迭代器等方法,确保未及时搬迁的键被删除,然后开释旧哈希槽中的内存空间。

监控和优化Redis的内存运用状况是要害。定时检查内存占用,并依据需求进行调整和优化,有助于进步Redis实例的功能和稳定性,并避免额定的内存开支。一起,合理设置键的过期时刻,可以避免不必要的内存占用

渐进式Rehash操作

Redis中的字典渐进式Rehash是指在字典扩容时,Redis会逐步搬迁旧的哈希桶中的键值对到新的更大的哈希桶中的进程。

渐进式Rehash操作的原因

会出现渐进式Rehash的原因是当字典中的键值对数量添加时,为了保持哈希表的功能,Redis会定时扩容字典。而在字典扩容的进程中,Redis会一起创建一个新的更大的哈希桶,并将旧哈希桶中的键值对逐步搬迁到新的哈希桶中。

详细的渐进式Rehash进程

渐进式Rehash,Redis可以避免在一次性进行哈希桶搬迁时形成的功能问题,一起可以确保旧的哈希桶仍然可以被正常访问。

【Redis深度专题】「核心技术进步」从源码视点探求Redis服务的内存运用、收拾以及逐出等底层完成原理

  1. Redis在旧哈希桶中挑选一个非空的桶进行rehash。

  2. 将挑选的桶中的键值对逐一搬迁到新的哈希桶中。

  3. 在搬迁的进程中,关于新哈希桶中的每个桶,将新桶指向旧桶的下一个桶,使得搬迁的键值对可以被访问到。

  4. 当挑选的桶中的一切键值对都搬迁到新哈希桶后,将旧哈希桶删除。

  5. 重复以上进程,直到一切的桶都被搬迁到新哈希桶中。

渐进式Rehash可以在很长的时刻内进行,逐渐将键值对从旧哈希桶搬迁到新哈希桶,削减了操作的复杂性和对功能的影响。

其他方面开支

Redis在办理数据时涉及到底层数据结构的开支、客户端信息和读写缓冲区等方面的办理。此外,在Redis的主从仿制进程中,进行bgsave操作或许会发生一些额定开支。

【Redis深度专题】「核心技术进步」从源码视点探求Redis服务的内存运用、收拾以及逐出等底层完成原理

  • 客户端信息办理:在运用Redis时,应合理办理客户端的衔接和会话信息。避免创建过多的无效衔接或长时刻闲置的衔接,以减轻服务器负载,并进步资源利用率。

  • 读写缓冲区办理:在数据读写进程中,Redis运用缓冲区来进步功能。可以经过恰当装备缓冲区的巨细和处理战略,以平衡内存运用和功能需求。依据实践状况,可以调整Redis的装备参数,如 client-output-buffer-limitclient-query-buffer-limit,以优化缓冲区办理。

  • Redis主从仿制:在Redis的主从仿制进程中,进行bgsave操作或许会导致额定的开支,因为bgsave会将数据快照保存到磁盘中。为了削减对主节点的影响,可以考虑在从节点上履行bgsave操作,避免主节点的功能影响。此外,合理设置Redis的仿制战略和装备参数可对主从仿制功率进行优化。

Redis过期数据收拾战略

为了避免大量过期键的一次性收拾对Redis服务功能形成负面影响,Redis现已优化,在空闲时收拾过期键。这种战略可以有用削减对服务功能的干扰,使得收拾作业更加平稳和高效。

过期数据收拾机遇

  1. Redis在逐出过期键时,详细的机遇是在访问键时进行判别,假如发现键已过期,则将其逐出。这种机制确保了对过期键的及时收拾,一起也确保了数据访问时的高效性和实时性。
robj *lookupKeyRead(redisDb *db, robj *key) {
    robj *val;
    expireIfNeeded(db,key);
    val = lookupKey(db,key);
    ...
    return val;
}
  1. 在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);
            ...
        }
}
  1. 每次事情循环履行时,会逐出部分过期的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的过期频率,动态调整收拾的时刻间隔,确保可以及时收拾过期Key,一起避免频频的收拾操作影响正常服务。

  • 经过合理设置最大时刻约束并进行并行处理: 针对收拾操作的最大时刻约束,可以引进并行化处理机制,将收拾操作分批进行,并行处理以进步收拾功率,可以确保收拾操作在限定时刻内完成,而且不会对正常服务发生显着影响。

  • 经过合理设置Redis的内存碎片收拾战略:可以削减内存碎片对过期Key收拾功率的影响,进一步优化收拾功能。

  • 主张监控体系的运行状况:定时检查体系的收拾功率和对服务的影响程度,并依据实践状况进行调整和优化,以确保体系长时刻运行时的功能最优。

收拾数据详细的流程

  1. Redis装备项 hz,定义了 serverCron 任务的履行周期,默以为 10,即在 CPU 空闲时每秒履行 10 次。

  2. 每次过期键收拾的时刻不超越 CPU 时刻的 25%。例如,假如 hz=1,则每次收拾的最大时刻为 250 毫秒;假如 hz=10,则每次收拾的最大时刻为 25 毫秒。

  3. 收拾进程会依次遍历一切的数据库(db)。

  4. 在收拾进程中,从每个数据库随机挑选 20 个键,并判别它们是否过期。假如键过期,则会被删除。假如有超越 5 个键过期,会重新履行进程 4;不然,会继续遍历下一个数据库。

  5. 在履行收拾进程时,假如达到了 CPU 时刻的 25%,就会退出收拾进程。

收拾内容的完成

Redis的收拾进程,包含履行周期、过期键收拾的时刻约束、遍历数据库和随机挑选键进行过期判别的进程。一起,提及了在收拾进程中约束 CPU 时刻的机制,以确保不过度耗费 CPU 资源。这样有助于了解收拾进程的作业原理并优化 Redis 的功能。

关于hz参数的一些讨论

调高hz参数可以进步收拾的频率,过期key可以更及时的被删除,但hz太高会添加CPU时刻的耗费;

代码剖析如下:

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);
    }
}

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

由于算法选用的随机取key判别是否过期的方法,故几乎不或许收拾完一切的过期Key;

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;
}

总结归纳

  • 不要放废物数据,及时收拾无用数据:定时收拾废物数据和下线的事务数据,避免占用不必要的内存空间和资源。

  • 设置过期时刻:对具有时效性的键设置合理的过期时刻,利用Redis本身的过期键收拾战略,下降过期键对内存的占用,并避免手动收拾的麻烦。

  • 操控单个键的巨细:避免单个键过大,避免网络传输推迟和内存资源耗费过多。经过事务拆分、数据紧缩等方法,操控键的巨细。

  • 运用不同逻辑数据库(db)分隔事务:在不同事务同享Redis时,最好运用不同的逻辑数据库进行分隔。这有助于过期键的及时收拾,并方便问题排查和无用数据的下线。