Redis占用内存大小
我们知道Redis是基于内存的key-value数据库,因为系统的内存大小有限,所以我们在使用Redis的时候可以配置Redis能使用的最大的内存大小。
1、通过配置文件配置
通过在Redis安装目录下面的redis.conf配置文件中添加以下配置设置内存大小
//设置Redis最大占用内存大小为100M
maxmemory 100mb
redis的配置文件不一定使用的是安装目录下面的redis.conf文件,启动redis服务的时候是可以传一个参数指定redis的配置文件的
2、通过命令修改
Redis支持运行时通过命令动态修改内存大小
//设置Redis最大占用内存大小为100M
127.0.0.1:6379> config set maxmemory 100mb
//获取设置的Redis能使用的最大内存大小
127.0.0.1:6379> config get maxmemory
如果不设置最大内存大小或者设置最大内存大小为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存
Redis的内存淘汰
既然可以设置Redis最大占用内存大小,那么配置的内存就有用完的时候。那在内存用完的时候,还继续往Redis里面添加数据不就没内存可用了吗?
实际上Redis 3.0定义了几种策略用来处理这种情况:
-
noeviction(默认策略):对于写请求不再提供服务,直接返回错误(DEL请求和部分特殊请求除外)
-
allkeys-lru:从所有key中使用LRU算法进行淘汰
-
volatile-lru:从设置了过期时间的key中使用LRU算法进行淘汰
-
allkeys-random:从所有key中随机淘汰数据
-
volatile-random:从设置了过期时间的key中随机淘汰
-
volatile-ttl:在设置了过期时间的key中,根据key的过期时间进行淘汰,越早过期的越优先被淘汰
当使用volatile-lru、volatile-random、volatile-ttl这三种策略时,如果没有key可以被淘汰,则和noeviction一样返回错误
| maxmemory-policy | 含义 | 特性 |
|---|---|---|
| noeviction | 不淘汰 | 内存超限后写命令会返回错误(如OOM, del命令除外) |
| allkeys-lru | 所有key的LRU机制 | 在所有key中按照最近最少使用LRU原则剔除key,释放空间 |
| volatile-lru | 易失key的LRU | 仅以设置过期时间key范围内的LRU(如均为设置过期时间,则不会淘汰) |
| allkeys-random | 所有key随机淘汰 | 一视同仁,随机 |
| volatile-random | 易失Key的随机 | 仅设置过期时间key范围内的随机 |
| volatile-ttl | 易失key的TTL淘汰 | 按最小TTL的key优先淘汰 |
其中LRU(less recently used)经典淘汰算法在Redis实现中有一定优化设计,来保证内存占用与实际效果的平衡,这也体现了工程应用是空间与时间的平衡性。
PS:值得注意的,在主从复制模式Replication下,从节点达到maxmemory时不会有任何异常日志信息,但现象为增量数据无法同步至从节点。
如何获取及设置内存淘汰策略
获取当前内存淘汰策略:
127.0.0.1:6379> config get maxmemory-policy
通过配置文件设置淘汰策略(修改redis.conf文件):
maxmemory-policy allkeys-lru
通过命令修改淘汰策略:
127.0.0.1:6379> config set maxmemory-policy allkeys-lru
LRU算法
什么是LRU?
上面说到了Redis可使用最大内存使用完了,是可以使用LRU算法进行内存淘汰的,那么什么是LRU算法呢?
LRU(Least Recently Used),即最近最少使用,是一种缓存置换算法。
在使用内存作为缓存的时候,缓存的大小一般是固定的。当缓存被占满,这个时候继续往缓存里面添加数据,就需要淘汰一部分老的数据,释放内存空间用来存储新的数据。
这个时候就可以使用LRU算法了。其核心思想是:如果一个数据在最近一段时间没有被用到,那么将来被使用到的可能性也很小,所以就可以被淘汰掉。
使用java实现一个简单的LRU算法
public class LRUCache<k, v> {
//容量
private int capacity;
//当前有多少节点的统计
private int count;
//缓存节点
private Map<k, node> nodeMap;
private Node head;
private Node tail;
public LRUCache(int capacity) {
if (capacity < 1) {
throw new IllegalArgumentException(String.valueOf(capacity));
}
this.capacity = capacity;
this.nodeMap = new HashMap<>();
//初始化头节点和尾节点,利用哨兵模式减少判断头结点和尾节点为空的代码
Node headNode = new Node(null, null);
Node tailNode = new Node(null, null);
headNode.next = tailNode;
tailNode.pre = headNode;
this.head = headNode;
this.tail = tailNode;
}
public void put(k key, v value) {
Node node = nodeMap.get(key);
if (node == null) {
if (count >= capacity) {
//先移除一个节点
removeNode();
}
node = new Node<>(key, value);
//添加节点
addNode(node);
} else {
//移动节点到头节点
moveNodeToHead(node);
}
}
public Node get(k key) {
Node node = nodeMap.get(key);
if (node != null) {
moveNodeToHead(node);
}
return node;
}
private void removeNode() {
Node node = tail.pre;
//从链表里面移除
removeFromList(node);
nodeMap.remove(node.key);
count--;
}
private void removeFromList(Node node) {
Node pre = node.pre;
Node next = node.next;
pre.next = next;
next.pre = pre;
node.next = null;
node.pre = null;
}
private void addNode(Node node) {
//添加节点到头部
addToHead(node);
nodeMap.put(node.key, node);
count++;
}
private void addToHead(Node node) {
Node next = head.next;
next.pre = node;
node.next = next;
node.pre = head;
head.next = node;
}
public void moveNodeToHead(Node node) {
//从链表里面移除
removeFromList(node);
//添加节点到头部
addToHead(node);
}
class Node<k, v> {
k key;
v value;
Node pre;
Node next;
public Node(k key, v value) {
this.key = key;
this.value = value;
}
}
}
上面这段代码实现了一个简单的LUR算法,代码很简单,也加了注释,仔细看一下很容易就看懂。
LRU在Redis中的实现
近似LRU算法
Redis使用的是近似LRU算法,它跟常规的LRU算法还不太一样。
近似LRU算法通过随机采样法淘汰数据,每次随机出5(默认)个key,从里面淘汰掉最近最少使用的key。
可以通过maxmemory-samples参数修改采样数量:例:maxmemory-samples 10 maxmenory-samples配置的越大,淘汰的结果越接近于严格的LRU算法
Redis为了实现近似LRU算法,给每个key增加了一个额外增加了一个24bit的字段,用来存储该key最后一次被访问的时间。
Redis3.0对近似LRU的优化
Redis3.0对近似LRU算法进行了一些优化。新算法会维护一个候选池(大小为16),池中的数据根据访问时间进行排序,第一次随机选取的key都会放入池中
随后每次随机选取的key只有在访问时间小于池中最小的时间才会放入池中,直到候选池被放满。
当放满后,如果有新的key需要放入,则将池中最后访问时间最大(最近被访问)的移除。
当需要淘汰的时候,则直接从池中选取最近访问时间最小(最久没被访问)的key淘汰掉就行。
LRU算法的对比
Redis中LRU是近似LRU实现,并不能取出理想LRU理论中最佳淘汰Key,而是通过从小部分采样后的样本中淘汰局部LRU键。
Redis 3.0中近似LRU算法通过增加待淘汰元素池的方式进一步优化,最终实现与精确LRU非常接近的表现。
精确LRU会占用较大内存记录历史状态,而近似LRU则用较小内存支出实现近似效果。
我们可以通过一个实验对比各LRU算法的准确率,先往Redis里面添加一定数量的数据n,使Redis可用内存用完,再往Redis里面添加n/2的新数据,这个时候就需要淘汰掉一部分的数据
如果按照严格的LRU算法,应该淘汰掉的是最先加入的n/2的数据。
生成如下各LRU算法的对比图:

你可以看到图中有三种不同颜色的点:
- 按时间顺序接入不同键,此时最早写入也就是最佳淘汰键
- 浅灰色区域:被淘汰的键
- 灰色区域:未被淘汰的键
- 绿色区域:新增写入的键
总结图中展示规律,
- 图1
Theoretical LRU符合预期:最早写入键逐步被淘汰 - 图2
Approx LRU Redis 3.0 10 samples:Redis 3.0中近似LRU算法(采样值为10) - 图3
Approx LRU Redis 2.8 5 samples:Redis 2.8中近似LRU算法(采样值为5) - 图4
Approx LRU Redis 3.0 5 samples:Redis 3.0中近似LRU算法(采样值为5)
结论:
- 通过图4和图3对比:得出相同采样值下,3.0比2.8的LRU淘汰机制更接近理论LRU
- 通过图4和图2对比:得出增加采样值,在3.0中将进一步改善LRU淘汰效果逼近理论LRU
- 对比图2和图1:在3.0中采样值为10时,效果非常接近理论LRU
采样值设置通过maxmemory-samples指定,可通过CONFIG SET maxmemory-samples <count>动态设置,也可启动配置中指定maxmemory-samples <count>
我们能看到Redis3.0采样数是10生成的图最接近于严格的LRU。而同样使用5个采样数,Redis3.0也要优于Redis2.8。
源码解析
int freeMemoryIfNeeded(void){
while (mem_freed < mem_tofree) {
if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)
return REDIS_ERR; /* We need to free memory, but policy forbids. */
if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
{......}
/* volatile-random and allkeys-random policy */
if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||
server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM)
{......}
/* volatile-lru and allkeys-lru policy */
else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
{
// 淘汰池函数
evictionPoolPopulate(dict, db->dict, db->eviction_pool);
while(bestkey == NULL) {
evictionPoolPopulate(dict, db->dict, db->eviction_pool);
// 从后向前逐一淘汰
for (k = REDIS_EVICTION_POOL_SIZE-1; k >= 0; k--) {
if (pool[k].key == NULL) continue;
de = dictFind(dict,pool[k].key); // 定位目标
/* Remove the entry from the pool. */
sdsfree(pool[k].key);
/* Shift all elements on its right to left. */
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 the key exists, is our pick. Otherwise it is
* a ghost and we need to try the next element. */
if (de) {
bestkey = dictGetKey(de); // 确定删除键
break;
} else {
/* Ghost... */
continue;
}
}
}
}
/* volatile-ttl */
else if (server.maxmemory_policy == EDIS_MAXMEMORY_VOLATILE_TTL) {......}
// 最终选定待删除键bestkey
if (bestkey) {
long long delta;
robj *keyobj = createStringObject(bestkey,sdslenbestkey)); // 目标对象
propagateExpire(db,keyobj);
latencyStartMonitor(eviction_latency); // 延迟监控开始
dbDelete(db,keyobj); // 从db删除对象
latencyEndMonitor(eviction_latency);// 延迟监控结束
latencyAddSampleIfNeeded("eviction-del",iction_latency); // 延迟采样
latencyRemoveNestedEvent(latency,eviction_latency);
delta -= (long long) zmalloc_used_memory();
mem_freed += delta; // 释放内存计数
server.stat_evictedkeys++; // 淘汰key计数,info中可见
notifyKeyspaceEvent(REDIS_NOTIFY_EVICTED, "evicted", keyobj, db->id); // 事件通知
decrRefCount(keyobj); // 引用计数更新
keys_freed++;
// 避免删除较多键导致的主从延迟,在循环内同步
if (slaves) flushSlavesOutputBuffers();
}
}
}
Redis 4.0中新的LFU算法
LFU算法是Redis4.0里面新加的一种淘汰策略。它的全称是Least Frequently Used,提供更好缓存命中率。LFU(Least Frequently Used)通过记录键使用频率来定位最可能淘汰的键。
它的核心思想是根据key的最近被访问的频率进行淘汰,很少被访问的优先被淘汰,被访问的多的则被留下来。
LFU算法能更好的表示一个key被访问的热度。假如你使用的是LRU算法,一个key很久没有被访问到,只刚刚是偶尔被访问了一次,那么它就被认为是热点数据,不会被淘汰,而有些key将来是很有可能被访问到的则被淘汰了。
如果使用LFU算法则不会出现这种情况,因为使用一次并不会使一个key成为热点数据。
对比LRU与LFU的差别:
- 在LRU中,某个键很少被访问,但在刚刚被访问后其被淘汰概率很低,从而出现这类异常持续存在的缓存;相对的,其他可能被访问的键会被淘汰
- 而LFU中,按访问频次淘汰最少被访问的键
Redis 4.0中新增两种LFU淘汰机制:
- volatile-lfu:设置过期时间的键按LFU淘汰
- allkeys-lfu:所有键按LFU淘汰
LFU使用Morris counters计数器占用少量位数来评估每个对象的访问频率,并随时间更新计数器。此机制实现与近似LRU中采样类似。但与LRU不同,LFU提供明确参数来指定计数更新频率。
- lfu-log-factor:0-255之间,饱和因子,值越小代表饱和速度越快
- lfu-decay-time:衰减周期,单位分钟,计数器衰减的分钟数
设置使用这两种淘汰策略跟前面讲的一样,不过要注意的一点是这两周策略只能在Redis4.0及以上设置,如果在Redis4.0以下设置会报错
The decay time is the obvious one, it is the amount of minutes a counter should be decayed, when sampled and found to be older than that value. A special value of 0 means: always decay the counter every time is scanned, and is rarely useful.
The counter logarithm factor changes how many hits are needed in order to saturate the frequency counter, which is just in the range 0-255. The higher the factor, the more accesses are needed in order to reach the maximum. The lower the factor, the better is the resolution of the counter for low accesses
1 2 3 4 5 6 7 8 9 10 11+--------+------------+------------+------------+------------+------------+ | factor | 100 hits | 1000 hits | 100K hits | 1M hits | 10M hits | +--------+------------+------------+------------+------------+------------+ | 0 | 104 | 255 | 255 | 255 | 255 | +--------+------------+------------+------------+------------+------------+ | 1 | 18 | 49 | 255 | 255 | 255 | +--------+------------+------------+------------+------------+------------+ | 10 | 10 | 18 | 142 | 255 | 255 | +--------+------------+------------+------------+------------+------------+ | 100 | 8 | 11 | 49 | 143 | 255 | +--------+------------+------------+------------+------------+------------+
这两个因子形成一种平衡,通过少量访问 VS 多次访问 的评价标准最终形成对键重要性的评判。
本文深入探讨Redis的内存管理,包括配置最大内存限制的方法、内存淘汰策略的选择及其工作原理,特别聚焦于LRU和LFU算法的实现与优化。
4063

被折叠的 条评论
为什么被折叠?



