概述
在使用redis时,我们往往会设置一个最大的内存上限,来保证其他应用有足够的内存。当redis使用的内存达到最大的内存上限时会自动的对数据进行淘汰以便有更新的数据能够进来,在redis中可以对淘汰数据的算法进行配置。
本文分析redis的数据淘汰功能的实现原理。
配置redis最大内存限制
配置文件中的最大内存删除策略
在redis的配置文件中,可以设置redis内存使用的最大值,当redis使用内存达到最大值时,redis会根据配置文件中的策略选取要删除的key,并删除这些key-value的值。若根据配置的策略,没有符合策略的key,也就是说内存已经容不下新的key-value了,但此时又不能删除key,那么这时候写的话,将会出现写错误。
最大内存参数设置
在redis.conf文件中对redis使用的最大内存进行配置,配置参数如下:
maxmemory 100mb
若maxmemory参数设置为0,则分两种情况:
- 在64位系统上,表示没有限制。
- 在32为系统上,是3G,redis官方文档的说法是,32位系统最大内存是4G,预留1G给系统。而且策略会自动设置为noeviction(当内存使用到达最大上限时,导致内存使用增加的命令会报错)。
- 也就是说在32位系统上,若maxmemory设置为0,则默认是3G,当到达3G,再往reidis中写入时,则会报错。
到达最大内存时的几种删除key的策略
- volatile-lru
仅从已经设置了超时时间的key中删除key,并通过LRU算法(最少最近没使用)来选取key。 - allkeys-lru
根据LRU算法,从所有key的集合中来选取需要删除的key,不论该key是或否设置了超时时间。 - volatile-random
随机删除一个设置了expire时间的key。 - allkeys-random
从所有key集合中随机删除任意一个key,不论这个key是否设置了expire时间。 - volatile-ttl
删除具有最近终止时间值(TTL)的key。 - noeviction
若没有任何key被淘汰,而此时执行的命令要求执行申请更多的内存,返回一个错误。
若如果没有任何可以淘汰的key,则volatile-lru,volatile-random和volatile-ttl的行为类似于noeviction。也就是说:当内存的使用达到上限时,返回客户端一个错误。
配置内存最大限制策略
以下这些命令的默认策略是:volatile-lru
# At the date of writing this commands are: set setnx setex append
# incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd
# sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby
# zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby
# getset mset msetnx exec sort
#
# The default is:
# maxmemory-policy volatile-lru
配置要删除key的检测样本个数
maxmemory-samples 5
由于LRU和最小TTL算法都是不是精确的算法。因此你可以选择要检测样本的个数。例如,默认情况下redis将会检查3个key,并从这3个key中选取一个最近没有使用的key。当然你可以修改检查样本的个数的值。
要修改这个值,可以通过在配置文件中设置参数:
maxmemory-samples 3
redis数据淘汰实现分析
redis数据淘汰总体流程
- 客户端运行新命令,导致添加更多数据。
- Redis检查内存使用情况,如果它大于maxmemory限制,它会根据策略删除key(key-value)。
- 执行新命令,依此类推。
问题: 若内存到达上限,每处理一条命令可能都需要:选key,删除key-value,这样是否会影响命令的性能?
代码实现分析
redis的数据淘汰策略的实现是在函数 freeMemoryIfNeeded(void) 中完成的。下面具体讲解每种策略是如何实现的。
什么时候淘汰key-value
当设置了maxmemory-policy策略后,什么时候会去删除key呢?
实际上,当设置了maxmemory参数后,在处理每个命令的时候都会根据maxmemory-policy去删除对应的key值。
processCommand函数用来处理客户端发送过来的命令。
代码如下:
// 注意:处理客户端的每个命令,都会调用这个函数
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. */
// 以上意思是:若存在可以删除的key,就释放一些内存,若不存在,给客户端返回一个错误。
if (server.maxmemory) { //若maxmemory不为0,则调用以下函数,释放其中一些key
int retval = freeMemoryIfNeeded();
// 根据配置策略删除key
if ((c->cmd->flags & REDIS_CMD_DENYOOM) && retval == REDIS_ERR) {
// 若出错,就终止处理命令,把错误返回给客户端
flagTransaction(c);
addReply(c, shared.oomerr);
return REDIS_OK;
}
}
... ...
}
实战中的考虑
- 实战1
若没有设置maxmemory变量,即使设置了maxmemory-policy,也不会起作用。 - 实战2
若没有设置maxmemory变量,在处理命令时将不会调用释放策略,会加速命令的处理过程。
删除key的总体流程
当内存达到最大值时需要按策略删除老的key-value,所有的删除操作和删除策略的实现都是在函数freeMemoryIfNeeded()中实现的。
在执行删除策略之前,先要选取db和查找key。总体步骤如下:
- 当客户端暂停时,直接返回OK。因为,当客户端暂停时,数据集应该是静态的,客户端不能写入,也不能执行key的淘汰。
- 获取slave的个数,若slave个数不为0(存在slave),当内存到达最大上限时,还需要释放给slave同步时的缓存区。
- 再次检查已使用的总内存是否超出maxmemory,若没有超出,直接返回OK。
- 若设置了eviction参数,则不允许释放内存,返回错误。
- 根据数据淘汰策略选择key
- 根据数据淘汰策略,从对应库中删除key
volatile-lru和allkeys-lru key淘汰机制的实现
redis中的LRU key淘汰机制
对于LRU机制,redis的官方文档有这样的解释:
Redis LRU algorithm is not an exact implementation. This means that Redis is not able to pick the best candidate for eviction, that is, the access that was accessed the most in the past. Instead it will try to run an approximation of the LRU algorithm, by sampling a small number of keys, and evicting the one that is the best (with the oldest access time) among the sampled keys.
However since Redis 3.0 (that is currently in beta) the algorithm was improved to also take a pool of good candidates for eviction. This improved the performance of the algorithm, making it able to approximate more closely the behavior of a real LRU algorithm.
What is important about the Redis LRU algorithm is that you are able to tune the precision of the algorithm by changing the number of samples to check for every eviction. This parameter is controlled by the following configuration directive:
maxmemory-samples 5
The reason why Redis does not use a true LRU implementation is because it costs more memory. However the approximation is virtually equivalent for the application using Redis. The following is a graphical comparison of how the LRU approximation used by Redis compares with true LRU.
大意是说,redis的LRU算法不是正真意思上的LRU。而是使用另外一种方式实现的。也就意味着,redis并不能每次都选择一个最好的key来删除。没有使用正真的LRU算法的原因是,它可能会消耗更多的内存。该算法和正真的LRU算法效果大概相同。
redis是在一小部分key中选择最优的要删除的key。这一小部分key的个数可以指定,可以在配置文件中设置参数maxmemory-samples 。
通过以上分析可以知,redis的数据淘汰机制主要是在freeMemoryIfNeeded()函数中实现的。该函数首先要计算最大空余内存和目前已经使用的内存大差值,若不够了,就要释放老的key-value。
若使用的是LRU策略,就会走以下代码,先进行最优删除key的选择,然后进行删除操作:
实现分析
前面分析已经提到redis数据淘汰策略的实现都是在函数int freeMemoryIfNeeded(void) 中实现。我们一起来分析一下lru的key淘汰算法的实现。
lur的key淘汰算法有两种模式,一种是volatile-lru,从设置了超时时间的key集合中选择淘汰的key-value;一种是allkeys-lru,从所有的key集合中选择淘汰的key-value。
- 在freeMemoryIfNeeded(void) 中会先在所有的数据库中查找需要淘汰的key,针对每个数据库会通过evictionPoolPopulate函数来获取需要淘汰的key集合。
- evictionPoolPopulate函数会从给定的数据库中(可能是主库,也可能是过期库expired对应volatile-lru策略)中根据一种抽样算法来选择需要淘汰的key。从该函数的实现来看,会调用dictGetSomeKeys函数来选择key。
- 要注意的是,dictGetSomeKeys函数是根据某种抽样算法对key进行随机抽样的,再从抽样出来的key中进行择优淘汰。所以,择优淘汰,就是选择随机抽出的key中,剩余的生存时间最少的。
- 选出key后,就可以删除该key-value了,删除的方式有两种,一种是同步删除,一种是异步删除。同步删除会直接在对应的数据库删除该key-value,异步删除,会把key放到一个数组中,并通知后台的一个线程来删除该key。在数据量很大的情况下采用异步方式可能会提升命令处理的效率。
通过以上的分析可知,redis的LRU淘汰算法不是严格意义上的LRU,而只是一种接近LRU的算法。
- 实战考虑
所以,对于需要严格依赖LRU进行key-value的淘汰的应用,选择通过redis的LRU来进行key淘汰是不太合适的。
volatile-random机制和allkeys-random的实现
前面说过,key淘汰算法都是在freeMemoryIfNeeded函数中实现的。
- 若是allkeys-random策略,则会在所有主库中选择key进行删除。若策略是volatile-random,则会在expires库中选择key进行删除。
- 遍历所有的库,使用dictGetRandomKey从hash表中随机选择一个key-value实体,实际上找到后会返回一个dictEntry的指针。该实体的具体数据结构可以参考我的这篇文章。
对于random的淘汰算法实现的代码如下:
/* volatile-random and allkeys-random policy */
else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM ||
server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM)
{
/* When evicting a random key, we try to evict a key for
* each DB, so we use the static 'next_db' variable to
* incrementally visit all DBs. */
// 为了能够公平遍历所有的库,通过一个静态自增变量next_db并和库id取余的方式来获取当前应该访问的库
for (i = 0; i < server.dbnum; i++) {
j = (++next_db) % server.dbnum;
db = server.db+j;
// 根据淘汰策略选择一个需要淘汰的库
dict = (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM) ?
db->dict : db->expires;
if (dictSize(dict) != 0) {
// 通过随机算法随机选择一个dictEntry的指针
de = dictGetRandomKey(dict);
// 获取dictEntry的key
bestkey = dictGetKey(de);
bestdbid = j;
break;
}
}
}
/* Finally remove the selected key. */
if (bestkey) { // 删除选出来的key-value
...
}
总结
本文分析了redis 的数据淘汰机制的实现。通过以上分析可以看出,redis的lru是一个大致的算法,不是精确的,在实际应用中需要考虑到这一点。
另外,若内存已经到达最大上限,每次处理客户端请求时都会选择一个key释放其key-value。
思考:能否把每次都选择key,让后释放key的过程进行优化,变成每次都释放n个key-value,这个n可以由用户进行配置。