每台redis的服务器的内存都是有限的,而且也不是所有的内存都用来存储信息。而且redis的实现并没有在内存这块做太多的优化,所以实现者为了防止内存过于饱和,采取了一些措施来管控内存。
文章结构:(1)内存策略;
(2)内存释放机制原理;
(3)项目中如何合理应用淘汰策略;
(4)单机版Redis内存优化注意点。
一、内存策略:
最大内存的设置是通过设置maxmemory来完成的,格式为maxmemory bytes ,当目前使用的内存超过了设置的最大内存,就要进行内存释放了, 当需要进行内存释放的时候,需要用某种策略对保存的的对象进行删除。Redis有六种策略(默认的策略是volatile-lru。)
redis中当内存超过限制时,按照配置的策略,淘汰掉相应的key-value,使得内存可以继续留有足够的空间保存新的数据。redis 确定驱逐某个键值对后,会删除这个数据并,并将这个数据变更消息发布到本地(AOF 持久化)和从机(主从连接)。
(1)volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。
(2)volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
(3)volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
(4)allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
(5)allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
除此之外还有一个配置项,就是maxmemory-samples,默认值是3,因为上面的策略代码实现的都是近似算法,所以不管是lru算法,还是ttl,都并不是在数据库中所有的数据为基础的算法,因为当数据库的数据很多的时候,这样效率太低,所以代码中都是基于maxmemory-samples个数据的近似算法。详情请读下文。
1)客户端执行一条新命令,导致数据库需要增加数据(比如set key value)
2)Redis会检查内存使用,如果内存使用超过maxmemory,就会按照置换策略删除一些key
3)新的命令执行成功
注意:
如果我们持续的写数据会导致内存达到或超出上限maxmemory,但是置换策略会将内存使用降低到上限以下。
如果一次需要使用很多的内存(比如一次写入一个很大的set),那么,Redis的内存使用可能超出最大内存限制一段时间。
二、内存释放机制原理:
(1)概述:
当
mem_used
内存已经超过
maxmemory
的设定,对于所有的读写请求,都会触发
redis.c/freeMemoryIfNeeded(void)
函数以清理超出的内存。注意这个清理过程是阻塞的,直到清理出足够的内存空间。所以如果在达到
maxmemory
并且调用方还在不断写入的情况下,可能会反复触发主动清理策略,导致请求会有一定的延迟。
清理时会根据用户配置的
maxmemory-policy
来做适当的清理(一般是
LRU
或
TTL
),这里的
LRU
或
TTL
策略并不是针对
redis
的所有
key
,而是以配置文件中的
maxmemory-samples
个
key
作为样本池进行抽样清理。
在
redis-5.0.6
中的默认配置为
5
,如果增加,会提高
LRU
或
TTL
的精准度,
redis
作者测试的结果是当这个配置为
10
时已经非常接近全量
LRU
的精准度了,并且增加
maxmemory-samples
会导致在主动清理时消耗更多的
CPU
时间,有如下建议:
1
)尽量不要触发
maxmemory
,最好在
mem_used
内存占用达到
maxmemory
的一定比例后,需要考虑调大
hz
以加快淘汰,或者进行集群扩容。
2
)如果能够控制住内存,则可以不用修改
maxmemory-samples
配置;如果
Redis
本身就作为
LRU cache
服务(这种服务一般长时间处于
maxmemory
状态,由
Redis
自动做
LRU
淘汰),可以适当调大
maxmemory-samples
。
(2)内存管理源码解析:
Redis
释放内存是由函数
freeMemoryIfNeeded
完成的,
redis
用
processCommand
函数处理每条命令,函数中在真正处理命令之前都会调用
freeMemoryIfNeeded
函数,这个函数会判断当前使用的内存是否超过了最大使用内存,如果超过,就会根据内存释放策略释放内存。
freeMemoryIfNeeded
函数首先会计算出当前使用了多少内存,注意,这里并不会包括
slaves
输出缓存以及
AOF
缓存,源码如下:
int freeMemoryIfNeeded(void) {
size_t mem_used, mem_tofree, mem_freed;
int slaves = listLength(server.slaves);
/* Remove the size of slaves output buffers and AOF buffer from the
* count of used memory.
*/
//
计算占用内存大小时,并不计算
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
,
/* Check if we are over the memory limit. */
if (mem_used <= server.maxmemory) return REDIS_OK;
//
当超过了最大使用内存时,就要判断此时
redis
到底采用的是那种内存释放策略,根据不同的策略,采取不同的手段。
//
(
1
)首先判断是否是为
no-enviction
策略,如果是,则返回
REDIS_ERR,
然后
redis
就不再接受任何写命令了。
if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)
return REDIS_ERR; /* We need to free memory, but policy forbids. */
/* Compute how much memory we need to free. */
mem_tofree = mem_used – server.maxmemory;
mem_freed = 0;
//
(
2
)接下来就判断淘汰策略是基于所有的键还是只是基于设置了过期时间的键,如果是针对所有的键,就从
server.db[j].dict
中取数据,如果是针对设置了过期时间的键,就从
server.db[j].expires
中取数据。
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;
//
(
3
)然后判断是不是
random
策略,包括
volatile-random
和
allkeys-random
,这两种策略是最简单的,就是在上面的数据集中随便去一个键,然后删掉。
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;
//
接着又判断
allkeys-random
还是
volatile-ttl
策略
/* 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 = dictGetEntryKey(de);
}//
如果是
random delete,
则从
dict
中随机选一个
key
//
然后就是判断是
lru
策略还是
ttl
策略,如果是
lru
策略就采用
lru
近似算法
/* 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 = 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 */
if (bestkey == NULL || thisval > bestval) {
bestkey = thiskey;
bestval = thisval;
}
}//
为了减少运算量
,redis
的
lru
算法和
expire
淘汰算法一样,都是非最优解,
lru
算法是在相应的
dict
中,选择
maxmemory_samples(
默认设置是
3)
份
key
,挑选其中
lru
的,进行淘汰
}
//
如果是
ttl
策略。
ttl
策略很简单,就是取
maxmemory_samples
个键,然后比较他们的过期时间,然后从这些键中找到最快过期的那个键,就是我们将要删除的键。
/* 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 = 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;
}
}//
注意
ttl
实现和上边一样,都是挑选出
maxmemory_samples
份进行挑选
}
//
根据不同的策略,我们找到了将要删除的键,下面就是将他们删除的时候了,删除选定的键值对
/* Finally remove the selected 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 mu
ch 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;
}
此函数是在执行特定命令之前进行调用的,并且在当前占用内存低于限制后即返回
OK
。因此可能在后续执行命令后,
redis
占用的内存就超过了
maxmemory
的限制。因此
,maxmemory
是
redis
执行命令所需保证的最大内存占用,而非
redis
实际的最大内存占用。(在不考虑
slave buffer
和
aof buffer
的前提下)。
redis
数据集数据结构中保存了键值对过期时间的表,即
redisDb.expires
。
定义:
从过期时间的表中随机挑选几个键值对,取出其中
ttl
最大的键值对淘汰。同样你会发现,
redis
并不是保证取得所有过期时间的表中最快过期的键值对,而只是随机挑选的几个键值对中的。
freeMemoryIfNeeded
函数关于
TTL
的源码:
//
挑选将要过期的数据
else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {
// server.maxmemory_samples
为随机挑选键值对次数
//
随机挑选
server.maxmemory_samples
个键值对,驱逐最快要过期的数据
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;
}
}
}
LRU
数据淘汰机制
:
在服务器配置中保存了 lru 计数器 server.lrulock,会定时(redis 定时程序 serverCorn())更新,server.lrulock 的值是根据 server.unixtime 计算出来的。另外,从 struct redisObject 中可以发现,每一个 redis 对象都会设置相应的 lru。可以想象的是,每一次访问数据的时候,会更新 redisObject.lru。
LRU 数据淘汰机制定义:
在数据集中随机挑选几个键值对,取出其中 lru 最小的键值对淘汰。所以,你会发现,redis
并不是保证取得所有数据集中最近最少使用(LRU)的键值对,而只是随机挑选的几个键值对中的键值对。
freeMemoryIfNeeded函数关于LRU的源码:
//
不同的策略,操作的数据集不同
if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
{
dict = server.db[j].dict;
} else {//
操作的是设置了过期时间的
key
集
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 */
//
具体的
LRU
算法
else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
{
struct evictionPoolEntry *pool = db->eviction_pool;
while(bestkey == NULL) {
//
选择随机样式,并从样本中作用
LRU
算法选择需要淘汰的数据
evictionPoolPopulate(dict, db->dict, db->eviction_pool);
/* Go backward from best to worst element to evict. */
for (k = REDIS_EVICTION_POOL_SIZE-1; k >= 0; k–) {
if (pool[k].key == NULL) continue;
de = dictFind(dict,pool[k].key);
sdsfree(pool[k].key);
//
将
pool+k+1
之后的元素向前平移一个单位
memmove(pool+k,pool+k+1,
sizeof(pool[0])*(REDIS_EVICTION_POOL_SIZE-k-1));
/* Clear the element on the right which is empty
* since we shifted one position to the left. */
pool[REDIS_EVICTION_POOL_SIZE-1].key = NULL;
pool[REDIS_EVICTION_POOL_SIZE-1].idle = 0;
//
选择了需要淘汰的数据
if (de) {
bestkey = dictGetKey(de);
break;
} else {
/* Ghost… */
continue;
}
}
}
}
LRU算法实现在evictionPoolPopulate方法内.(有兴趣的朋友再进行相关查阅了解吧)
修改方式有两种:
1.
通过
CONFIG SET
设定:
127.0.0.1:6379> CONFIG GET maxmemory
1) “maxmemory”
2) “0”
127.0.0.1:6379> CONFIG SET maxmemory 80MB
OK
127.0.0.1:6379> CONFIG GET maxmemory
1) “maxmemory”
2) “83886080”
2.
修改配置文件
redis.conf
:
maxmemory 80mb
注意:在
64bit
系统下,
maxmemory
设置为
0
表示不限制
Redis
内存使用,在
32bit
系统下,
maxmemory
隐式不能超过
3GB
。
当
Redis
内存使用达到指定的限制时,就需要选择一个置换的策略。
当
Redis
内存使用达到
maxmemory
时,就会使用设置好的
maxmemory-policy
进行对老数据的置换。
设置
maxmemory-policy
的方法和设置
maxmemory
方法类似,通过
redis.conf
或是通过
CONFIG SET
动态修改。
如果没有匹配到可以删除的
key
,那么
volatile-lru
、
volatile-random
和
volatile-ttl
策略和
noeviction
替换策略一样
——
不对任何
key
进行置换。
选择合适的置换策略是很重要的,这主要取决于你的应用的访问模式,当然你也可以动态的修改置换策略,并通过用
Redis
命令
——INFO
去输出
cache
的命中率情况,进而可以对置换策略进行调优。
1)allkeys-lru:如果我们的应用对缓存的访问符合幂律分布(也就是存在相对热点数据),或者我们不太清楚我们应用的缓存访问分布状况,我们可以选择allkeys-lru策略。
在所有的key都是最近最经常使用,那么就需要选择allkeys-lru进行置换最近最不经常使用的key,如果你不确定使用哪种策略。
设置是失效时间expire会占用一些内存,而采用allkeys-lru就没有必要设置失效时间,进而更有效的利用内存
2)allkeys-random:如果我们的应用对于缓存key的访问概率相等,则可以使用这个策略。
如果所有的key的访问概率都是差不多的,那么可以选用allkeys-random策略去置换数据。
3)volatile-ttl:这种策略使得我们可以向Redis提示哪些key更适合被eviction。
如果对数据有足够的了解,能够为key指定hint(通过expire/ttl指定),那么可以选择volatile-ttl进行置换
4)volatile-lru策略和volatile-random策略适合我们将一个Redis实例既应用于缓存和又应用于持久化存储的时候,然而我们也可以通过使用两个Redis实例来达到相同的效果,值得一提的是将key设置过期时间实际上会消耗更多的内存,因此我们建议使用allkeys-lru策略从而更有效率的使用内存。
概述:
很多数据类型都可以通过特殊编码的方式来进行存储空间的优化。其中,Hash、List和由Integer组成的Sets都可以通过该方式来优化存储结构,以便占用更少的空间,在有些情况下,可以省去9/10的空间。
这些特殊编码对于Redis的使用而言是完全透明的,事实上,它只是CPU和内存之间的一个交易而言。如果内存使用率方面高一些,那么在操作数据时消耗的CPU自然要多一些,反之亦然。在Redis中提供了一组配置参数用于设置与特殊编码相关的各种阈值。
阈值详细设置:
#
如果
Hash
中字段的数量小于该参数值,
Redis
将对该
Key
的
Hash Value
采用特殊编码。
hash-max-zipmap-entries 64
#
如果
Hash
中各个字段的最大长度不超过
512
字节,
Redis
也将对该
Key
的
Hash Value
采用特殊编码方式。
hash-max-zipmap-value 512
#
下面两个参数的含义基本等同于上面两个和
Hash
相关的参数,只是作用的对象类型为
List
。
list-max-ziplist-entries 512
list-max-ziplist-value 64
#
如果
set
中整型元素的数量不超过
512
时,
Redis
将会采用该特殊编码。
set-max-intset-entries 512
#
如果
zset
中的元素数量小于
128
或者各字段长度不超过
64
,
redis
会对
zset
采用特殊编码
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
倘若某个已经被编码的值再经过修改之后超过了配置信息中的最大限制,那么Redis会自动将其转换为正常编码格式,这一操作是非常快速的,但是如果反过来操作,将一个正常编码的较大值转换为特殊编码,Redis的建议是,在正式做之前最好先简单测试一下转换效率,因为这样的转换往往是非常低效的。
(2)使用bit位级别操作和byte字节级别操作来减少不必要的内存使用:
bit位级别操作:GETRANGE, SETRANGE, GETBIT and SETBIT
byte字节级别操作:GETRANGE and SETRANGE
Redis提供了GETRANGE/SETRANGE/GETBIT/SETBIT四个用于字符串类型Key/Value的命令。通过这些命令,我们便可以像操作数组那样来访问String类型的值数据了。比如唯一标识用户身份的ID,可能仅仅是String值的其中一段子字符串。这样就可以通过GETRANGE/SETRANGE命令来方便的提取。再有就是可以使用BITMAP来表示用户的性别信息,如1表示male,0表示female。用这种方式来表示100,000,000个用户的性别信息时,也仅仅占用12MB的存储空间,与此同时,在通过SETBIT/GETBIT命令进行数据遍历也是非常高效的。
(3)尽可能地使用hashes哈希,因为小Hashes会被编码成一个非常小的空间。
由于小的Hash类型数据占用的空间相对较少,因此我们在实际应用时应该尽可能的考虑使用Hash类型,比如用户的注册信息,这其中包括姓名、性别、email、年龄和口令等字段。我们当然可以将这些信息以Key的形式进行存储,而用户填写的信息则以String Value的形式存储。然而Redis则更为推荐以Hash的形式存储,以上信息则以Field/Value的形式表示。
现在我们就通过学习Redis的存储机制来进一步证明这一说法。在该篇博客的开始处已经提到了特殊编码机制,其中有两个和Hash类型相关的配置参数:hash-max-zipmap-entries和hash-max-zipmap-value。至于它们的作用范围前面已经给出,这里就不再过多的赘述了。现在我们先假设存储在Hash Value中的字段数量小于hash-max-zipmap-entries,而每个元素的长度又同时小于hash-max-zipmap-value。这样每当有新的Hash类型的Key/Value存储时,Redis都会为Hash Value创建定长的空间,最大可预分配的字节数为:
total_bytes = hash
–
max
-zipmap-entries * hash-
max
-zipmap-value
1
2
这样一来,Hash中所有字段的位置已经预留,并且可以像访问数组那样随机的访问Field/Value,他们之间的步长间隔为hash-max-zipmap-value。只有当Hash Value中的字段数量或某一新元素的长度分别超过以上两个参数值时,Redis才会考虑将他们以Hash Table的方式进行重新存储,否则将始终保持这种高效的存储和访问方式。不仅如此,由于每个Key都要存储一些关联的系统信息,如过期时间、LRU等,因此和String类型的Key/Value相比,Hash类型极大的减少了Key的数量(大部分的Key都以Hash字段的形式表示并存储了),从而进一步优化了存储空间的使用效率。
如果maxmemory没有设置的Redis会继续分配内存,因为它认为合适的,因此它可以(逐渐)吃了你的全部可用内存。因此,通常建议配置一些限制。您可能还需要设置maxmemory策略,默认的是:noeviction(这不是在一些旧版本的Redis的默认值)。
这使得Redis的返回内存不足的错误写命令,如果当它到达了极限 – 这反过来可能会导致应用程序错误,但不会导致因为内存饥饿而整机死亡。
(7)一些真正的实际Redis内存设计方案会在本系列的后面写出。比如:共享对象,【string】–>【hash】–>【segment-hash】的优化