Redis expiration and elimination strategy

Redis expiration and elimination strategy

The expiration and elimination strategy of redis is a problem that is very worthwhile to understand and study. Many users are often not satisfied with what they want, and often stay at the level of copying what others say. If there is no accident in production, they will pass by paddling water, but when the production data disappears inexplicably, or the reids service crashes, they are helpless. This article attempts to analyze the expiration strategy of redis from the shallower to the deeper, hoping to help the author and readers view the expiration strategy from a more systematic perspective.

As a cache database, the underlying data structure of redis is mainly composed of two dictionaries, dict and expires. The dict dictionary is responsible for saving key-value pairs, and the expires dictionary is responsible for saving the expiration time of the key. The cost of accessing disk space is much higher than the cost of accessing the cache, so memory costs more than disk space. In actual use, the cache space is often extremely limited, so in order to make the best use of the limited capacity, it is necessary to control the cache capacity.

memory policy

Redis maxmemoryconfigures the maximum capacity (threshold) through configuration. When the data occupied space exceeds the set value, the internal memory elimination strategy (memory release) will be triggered. So which data should be eliminated to best meet the business needs? Or within the business tolerance? In order to solve this problem, redis provides a configurable elimination strategy, so that users can configure an elimination strategy suitable for their own business scenarios. If not configured, it is used by default volatile-lru.

  • noeviction: When the memory is not enough to accommodate the newly written data, the new write operation will report an error.
  • allkeys-lru: Remove the least recently used key in the keyspace when the memory is insufficient for newly written data.
  • allkeys-random: When the memory is not enough to accommodate newly written data, in the key space, remove a key randomly.
  • volatile-lru: When the memory is not enough to accommodate newly written data, in the key space with the expiration time set, remove the least recently used key.
  • Volatile-random: When the memory is not enough to accommodate newly written data, a key is randomly removed in the key space with the expiration time set.
  • volatile-ttl: When the memory is not enough to accommodate newly written data, in the key space with the expiration time set, the key with an earlier expiration time is removed first.

If the number of key-value pairs saved by redis is not large (such as dozens of pairs), then when the memory exceeds the threshold, it is not harmful to check all the keys in the entire memory space. However, in actual use, the number of key-values ​​saved by redis is far more than that. If the memory usage exceeds the threshold, do you check one by one whether it meets the expiration policy? Randomly traverse 100,000 keys? Obviously not, otherwise, why is redis known for its high performance? However, the question of how many keys to check does exist. In order to solve this problem, the designers of redis introduced a configuration item maxmemory-samplescalled expired detection sample, the default value is 3, and it is used to save the country.

How does the expired detection sample cooperate with redis for data cleaning?

When the mem_usedmemory has exceeded maxmemorythe setting, for all read and write requests, the redis.c/freeMemoryIfNeededfunction will be triggered to clean up the excess memory. 注意这个清理过程是阻塞的, until enough memory is cleared. Therefore, if it is reached maxmemoryand the caller is still writing, the active cleanup strategy may be triggered repeatedly, resulting in a certain delay in the request.

During cleaning, appropriate cleaning will be done according to the policy configured by the user maxmemory(usually LRU or TTL). The LRU or TTL strategy here is not for all keys of redis, but uses the maxmemorysample keys in the configuration file as the sample pool. Sampling clean up.

The redis designer defaults this value to 3. If this value is increased, the accuracy of LRU or TTL will be improved. The test result of the redis author is that when this configuration is 10, it is very close to the accuracy of the full LRU, and the maxmemorysampling is increased. It will cause more CPU time to be consumed during active cleaning, so you must carefully control the setting of this value, and make a trade-off between business requirements and performance. suggestions below:

  • Try not to trigger maxmemory, preferably after the mem_usedmemory usage reaches maxmemorya certain percentage, you need to consider increasing the Hertz to speed up elimination, or perform cluster expansion.
  • If you can control the memory, you don't need to modify the maxmemory-samplesconfiguration. If redis itself is used as an LRU cache service (this service is generally in the maxmemorystate for a long time, and redis automatically performs LRU elimination), the maxmemorysample can be appropriately adjusted.

FreeMemoryIfNeeded source code interpretation

int freeMemoryIfNeeded(void) {
    size_t mem_used, mem_tofree, mem_freed;
    int slaves = listLength(server.slaves);

    // 计算占用内存大小时,并不计算slave output buffer和aof buffer,
	// 因此maxmemory应该比实际内存小,为这两个buffer留足空间。
    mem_used = zmalloc_used_memory();
    if (slaves) {
        listIter li;
        listNode *ln;

        listRewind(server.slaves,&li);
        while((ln = listNext(&li))) {
            redisClient *slave = listNodeValue(ln);
            unsigned long obuf_bytes = getClientOutputBufferMemoryUsage(slave);
            if (obuf_bytes > mem_used)
                mem_used = 0;
            else
                mem_used -= obuf_bytes;
        }
    }
    if (server.appendonly) {
        mem_used -= sdslen(server.aofbuf);
        mem_used -= sdslen(server.bgrewritebuf);
    }
	// 判断已经使用内存是否超过最大使用内存,如果没有超过就返回REDIS_OK,
    if (mem_used <= server.maxmemory) return REDIS_OK;
	// 当超过了最大使用内存时,就要判断此时redis到底采用何种内存释放策略,根据不同的策略,采取不同的清除算法。
	// 首先判断是否是为no-enviction策略,如果是,则返回REDIS_ERR,然后redis就不再接受任何写命令了。
    if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)
        return REDIS_ERR; 
    // 计算需要清理内存大小
    mem_tofree = mem_used - server.maxmemory;
    mem_freed = 0;
   
    while (mem_freed < mem_tofree) {
        int j, k, keys_freed = 0;

        for (j = 0; j < server.dbnum; j++) {
            long bestval = 0;
            sds bestkey = NULL;
            struct dictEntry *de;
            redisDb *db = server.db+j;
            dict *dict;
			
			// 1、从哪个字典中剔除数据
            // 判断淘汰策略是基于所有的键还是只是基于设置了过期时间的键,
			// 如果是针对所有的键,就从server.db[j].dict中取数据,
			// 如果是针对设置了过期时间的键,就从server.db[j].expires(记录过期时间)中取数据。
			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;
			
			// 2、从是否为随机策略
			// 是不是random策略,包括volatile-random 和allkeys-random,这两种策略是最简单的,就是在上面的数据集中随便去一个键,然后删掉。
            if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||
                server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM)
            {
                de = dictGetRandomKey(dict);// 从方法名猜出是随机获取一个dictEntry
                bestkey = dictGetEntryKey(de);// 得到删除的key
            }
			
			// 3、判断是否为lru算法
			// 是lru策略还是ttl策略,如果是lru策略就采用lru近似算法
			// 为了减少运算量,redis的lru算法和expire淘汰算法一样,都是非最优解,
			// lru算法是在相应的dict中,选择maxmemory_samples(默认设置是3)份key,挑选其中lru的,进行淘汰
            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 = dictGetEntryKey(de);
                    /* When policy is volatile-lru we need an additonal 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); //因为dict->expires维护的数据结构里并没有记录该key的最后访问时间
                    o = dictGetEntryVal(de);
                    thisval = estimateObjectIdleTime(o);

                    /* Higher idle time is better candidate for deletion */
					// 找到那个最合适删除的key
					// 类似排序,循环后找到最近最少使用,将其删除
                    if (bestkey == NULL || thisval > bestval) {
                        bestkey = thiskey;
                        bestval = thisval;
                    }
                }
            }
			// 如果是ttl策略。
			// 取maxmemory_samples个键,比较过期时间,
			// 从这些键中找到最快过期的那个键,并将其删除
            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 = dictGetEntryKey(de);
                    thisval = (long) dictGetEntryVal(de);

                    /* Expire sooner (minor expire unix timestamp) is better candidate for deletion */
                    if (bestkey == NULL || thisval < bestval) {
                        bestkey = thiskey;
                        bestval = thisval;
                    }
                }
            }
			// 根据不同策略挑选了即将删除的key之后,进行删除
            if (bestkey) {
                long long delta;

                robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
                // 发布数据更新消息,主要是AOF 持久化和从机
                propagateExpire(db,keyobj); //将del命令扩散给slaves

				// 注意, propagateExpire() 可能会导致内存的分配,
				// propagateExpire() 提前执行就是因为redis 只计算
				// dbDelete() 释放的内存大小。倘若同时计算dbDelete()
				// 释放的内存和propagateExpire() 分配空间的大小,与此
				// 同时假设分配空间大于释放空间,就有可能永远退不出这个循环。
				// 下面的代码会同时计算dbDelete() 释放的内存和propagateExpire() 分配空间的大小
                /* We compute the amount of memory freed by dbDelete() alone.
                 * It is possible that actually the memory needed to propagate
                 * the DEL in AOF and replication link is greater than the one
                 * we are freeing removing the key, but we can't account for
                 * that otherwise we would never exit the loop.
                 *
                 * AOF and Output buffer memory will be freed eventually so
                 * we only care about memory used by the key space. */
				// 只计算dbDelete() 释放内存的大小
                delta = (long long) zmalloc_used_memory();
                dbDelete(db,keyobj);
                delta -= (long long) zmalloc_used_memory();
                mem_freed += delta;
                server.stat_evictedkeys++;
                decrRefCount(keyobj);
                keys_freed++;

                /* When the memory to free starts to be big enough, we may
                 * start spending so much time here that is impossible to
                 * deliver data to the slaves fast enough, so we force the
                 * transmission here inside the loop. */
                 // 将从机回复空间中的数据及时发送给从机
                if (slaves) flushSlavesOutputBuffers();
            }
        }//在所有的db中遍历一遍,然后判断删除的key释放的空间是否足够,未能释放空间,且此时redis 使用的内存大小依旧超额,失败返回
        if (!keys_freed) return REDIS_ERR; /* nothing to free... */
    }
    return REDIS_OK;
}

From the source code analysis, we can see how redis manages the cleanup key-value when the memory exceeds the set threshold, which involves the storage structure of redis. At the beginning, it was mentioned that the underlying data structure of redis is composed of two dictionaries: dict and expires. Through a picture, you can clearly understand the key-value storage structure with expiration time in redis, and you can more deeply understand the memory management mechanism of redis .

Enter image description

From the memory management mechanism of redis, we can see that when the memory used exceeds the set threshold, memory cleanup will be triggered. So do we have to wait until the memory exceeds the threshold before performing memory cleanup? Have to make amends? The designer of redis obviously took this problem into consideration. When redis is in use, it deletes some expired keys by itself, and tries to ensure that no cleanup events that exceed the memory threshold are triggered.

Effective time

  • expire/pexpire key time (in seconds/milliseconds) -- this is the most common way (Time To Live is called TTL)
  • setex(String key, int seconds, String value)--a unique way to strings

When using the expiration time, it is necessary to pay attention to the following three points:

  • In addition to the unique method of setting the expiration time of the string itself, other methods need to rely on the expire method to set the time
  • If no time is set, the cache will never expire
  • If you set an expiration time and then want the cache to never expire, use the persist key

Expired key auto-deletion policy

1. Timed deletion (active deletion strategy): By using a timer (time event, implemented by an unordered linked list), data is deleted regularly. The timing deletion strategy can ensure that expired keys will be deleted as quickly as possible, and release the memory occupied by expired key locks.

  • Benefit: The most memory friendly.
  • Disadvantage: It is not friendly to CPU time. In the case of many expired keys, the act of deleting expired keys will take up a considerable part of CPU time. When memory is not tight but CPU is very tight, CPU is applied to delete and current Task-independent expiration keys will undoubtedly affect the response time and throughput of the server.

2. Lazy deletion (passive deletion strategy): The program checks whether the key is expired every time it is used, and if it expires, deletes it and returns empty.

  • Benefit: friendly to CPU time, always only operate on keys related to the current task.
  • Disadvantage: A large number of expired keys may be left in memory without being deleted, causing memory leaks.

3. Periodic deletion (active deletion): Periodically perform a period of deletion of expired keys at regular intervals, and reduce the impact of deletion operations on CPU time by limiting the execution time and frequency of deletion operations. In addition, regular execution can also reduce the impact of the long-term memory of expired keys and reduce the possibility of memory leaks.

  • Benefit: You can control how often expired deletions are performed
  • Disadvantage: The server must reasonably set the operation time and frequency of the deletion of expired keys.

Redis' expired key deletion strategy

The two strategies of lazy deletion and periodic deletion actually used by the redis server: By using the two deletion strategies together, the server can strike a balance between rationally using CPU time and avoiding wasting memory space.

Implementation of lazy deletion strategy

Redis provides a expireIfNeededfunction, so the commands to read and write to the database must call the expireIfNeeded function before executing them. (if the key exists)

  • If expired --> delete
  • If it is not expired --> execute the command (the expireIfNeeded function does not take action)

Implementation of Periodic Deletion Policy

Periodic deletion is activeExpireCycleimplemented with a function function, and the periodic deletion function is executed whenever the redis server calls serverCornthe function. It 规定时间will traverse each database in the server multiple times, and find 随机检查the expiration time of a part of the keys in the expire dictionary of the database, and delete the expired key.

Traverse the database (that is, the number of "database" configured in redis.conf, the default is 16)

  • Check the specified number of keys in the current library (the default is to check 20 keys per library, note that this is equivalent to 20 executions of the loop)
  • If no key in the current library has an expiration time set, execute the traversal of the next library directly
  • Randomly obtain a key with an expiration time set, check whether the key has expired, and delete the key if it expires
  • Determines whether the periodical deletion operation has reached the specified duration, and if so, exit the periodical deletion directly.

References:

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325453693&siteId=291194637