文章目录
Redis内存管理
长期把Redis
做缓存用,总有一天Redis
内存总会满的。有没有思考过这个问题,Redis
内存满了会怎么样?在redis.conf
中把Redis
内存设置为1个字节,做一个测试:
// 默认单位就是字节 maxmemory 1
设置完之后重启是为了保证测试的准确性,重启一下Redis
,之后在用下面的命令,向Redis
中存入键值对,模拟Redis
打满的情况:
set k1 v1
执行完后会看到下面的信息:
(error) OOM command not allowed when used memory > 'maxmemory'.
大意:OOM
,当前内存大于最大内存时,这个命令不允许被执行。
Redis
也会出现OOM
,正因如此,我们才要避免这种情况发生。正常情况下,不考虑极端业务,Redis
只存放热点数据,Redis
不是MySQL
数据库,不能什么都往里边写。Redis
默认最大内存是全部的内存,我们在实际配置的时候,一般配实际服务器内存的3/4也就足够了。
删除策略
正因为Redis
内存打满后报OOM
,为了避免出现该情况所以要设置Redis
的删除策略。Redis
提供了几种删除策略:
- 定期删除:在后台定期扫描数据库并删除过期数据。这种方法能有效清理过期数据,防止积累大量无用数据,保持内存使用效率。定期删除可能导致性能开销增加,尤其是当数据量很大时。适用于需要清理过期数据的场景,如缓存系统中的临时数据管理。
- 惰性删除:在客户端访问数据时检查数据是否过期,如果过期则删除。实现简单,对 Redis 性能的影响较小,但可能导致过期数据在被访问前仍然存在,占用内存。适合对数据过期处理要求不严格的应用,尤其是在需要高性能的场景中。
- 过期时间:策略允许为每个键设置过期时间,数据到达指定时间后会自动删除。精确控制数据的生命周期,可以避免过期数据占用空间。需要管理过期时间,增加了一定的复杂度。适用于需要精确控制数据生命周期的应用,例如缓存数据和会话管理。
- 主动删除:通过应用逻辑或管理工具手动删除数据,可以根据业务需求自定义删除规则。提供了灵活的删除控制,但需要额外的管理和维护工作,删除操作可能影响 Redis 性能。适合需要手动管理或根据业务逻辑自定义删除规则的场景。
淘汰策略
Redis
的内存淘汰策略用于管理当 Redis 数据库达到最大内存限制时,决定如何处理额外的数据。淘汰策略默认,使用noeviction
,意思是不再驱逐的,即等着内存被打满。Redis
内存淘汰策略在Redis 4.0
版本之前有6种策略,4.0增加了两种,主要新增了LFU
算法。下图为Redis 6.2.0
版本的配置文件:
策略名称 | 描述 | 优点 | 缺点 | 使用场景 |
---|---|---|---|---|
noeviction | 当达到最大内存限制时,拒绝所有写操作。 | 保持数据完整性,不会自动删除任何数据。 | 一旦达到内存限制,所有新的写操作都会失败。 | 对数据完整性要求高,不希望数据被自动删除的场景。 |
allkeys-lru | 在所有键中使用 LRU 算法来淘汰数据。 | 能够有效释放内存空间,保留活跃数据。 | LRU 算法的性能可能会受到大规模数据集的影响。 | 数据访问模式有明显使用频率差异的场景。 |
allkeys-random | 在所有键中随机选择一些进行删除。 | 实现简单,开销较小。 | 删除的数据是随机的,重要数据可能被淘汰。 | 对数据重要性没有明确排序的场景。 |
volatile-lru | 只在设置了过期时间的键中使用 LRU 算法进行淘汰。 | 优先淘汰过期数据,能更好地利用内存。 | 对没有设置过期时间的键没有影响。 | 希望优先淘汰过期数据,同时保留重要数据的场景。 |
volatile-random | 只在设置了过期时间的键中随机选择一些进行删除。 | 实现简单,不需要计算键的使用频率。 | 删除的数据是随机的,可能会淘汰重要的过期数据。 | 需要删除过期数据,但对具体选择方式要求不高的场景。 |
volatile-ttl | 只在设置了过期时间的键中,优先淘汰即将过期的键。 | 能够控制内存使用,同时保留即将过期的数据。 | 可能会导致频繁的内存释放操作,增加 Redis 的管理开销。 | 优先删除即将过期的数据的场景。 |
在Redis
中,最常使用的内存淘汰策略通常是allkeys-lru
和volatile-lru
。
allkeys-lru
策略在 Redis 达到内存限制时,会在所有存储的键中使用LRU
算法进行数据淘汰。无论键是否设置了过期时间,Redis
都会选择最近最少使用的键进行删除,释放内存。这种策略适用于需要对所有数据进行管理的场景,例如web
缓存和会话管理,可以有效保留访问频率高的数据。
相比之下,volatile-lru
策略只对设置了过期时间的键使用 LRU 算法。当Redis
达到内存限制时,它会在所有设置了过期时间的键中选择最近最少使用的数据进行淘汰。未设置过期时间的键不会被考虑,这样能够保留重要的未过期数据。这种策略适合有明确过期需求的应用,如缓存系统和用户会话存储,能够有效管理内存而不会影响长期存在的数据。
区别在于处理数据的范围。allkeys-lru
适用于管理所有存储的键,考虑所有数据的使用情况;volatile-lru
专注于管理设置了过期时间的键,优先淘汰过期数据,保留未过期的键。volatile-lru
适合处理需要管理过期数据的场景,而allkeys-lru
适用于全面管理所有数据的情况。
LRU算法
LRU
是Least Recently Used
的缩写,即最近最少使用,是一种常用的页面置换算法。
页面置换算法:进程运行时,若其访问的页面不在内存而需将其调入,但内存已无空闲空间时,就需要从内存中调出一页程序或数据,送入磁盘的对换区,其中选择调出页面的算法就称为页面置换算法。
这个算法的思想是,如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。所以当指定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。LRU
算法常用于缓存管理,目的是在缓存满时,保留最常使用的数据,同时移除最久未被使用的数据。
使用哈希表与双向链表结合实现LRU
算法:
class LRUCache { private final int capacity; private final HashMap<Integer, Node> cache; private final DoublyLinkedList list; class Node { int key, value; Node prev, next; Node(int key, int value) { this.key = key; this.value = value; } } class DoublyLinkedList { private final Node head, tail; DoublyLinkedList() { head = new Node(-1, -1); tail = new Node(-1, -1); head.next = tail; tail.prev = head; } void addFirst(Node node) { node.next = head.next; node.prev = head; head.next.prev = node; head.next = node; } void remove(Node node) { node.prev.next = node.next; node.next.prev = node.prev; } Node removeLast() { if (tail.prev == head) return null; Node node = tail.prev; remove(node); return node; } } public LRUCache(int capacity) { this.capacity = capacity; this.cache = new HashMap<>(); this.list = new DoublyLinkedList(); } public int get(int key) { if (!cache.containsKey(key)) return -1; Node node = cache.get(key); list.remove(node); list.addFirst(node); return node.value; } public void put(int key, int value) { if (cache.containsKey(key)) { Node node = cache.get(key); list.remove(node); node.value = value; list.addFirst(node); } else { if (cache.size() >= capacity) { Node tail = list.removeLast(); if (tail != null) cache.remove(tail.key); } Node node = new Node(key, value); list.addFirst(node); cache.put(key, node); } } }
使用哈希表提供快速查找,同时利用链表维护访问顺序。哈希表保证了操作的平均时间复杂度为O(1)
,适合高效的缓存实现。