本文大部分内容引自《Redis深度历险:核心原理和应用实践》,感谢作者!!!
Redis内存不足的解决策略
1、Redis内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换(swap);交换会让Redis的性能急剧下降,对于访问量频繁的Redis是不可接受的
2、使用配置参数maxmemory限制Redis使用的内存上限,当实际使用的内存超出maxmemory时,Redis会提供LRU策略(maxmemory-policy)让用户决定如何腾出新的内存空间
LRU策略
- noeviction 不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略
- volatile-lru 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失
- volatile-ttl 跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl 越小越优先被淘汰(ttl - time to live)
- volatile-random 跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key
- allkeys-lru 区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰
- allkeys-random 跟上面一样,不过淘汰的策略是随机的 key
- volatile-xxx 策略只会针对带过期时间的 key 进行淘汰,allkeys-xxx 策略会对所有的 key 进行淘汰。如果你只是拿 Redis 做缓存,那应该使用 allkeys-xxx,客户端写缓存时不必携带过期时间。如果你还想同时使用 Redis 的持久化功能,那就使用 volatile-xxx 策略,这样可以保留没有设置过期时间的 key,它们是永久的 key 不会被 LRU 算法淘汰
LRU算法
实现LRU算法除了需要key/value之外还需要附加一个链表,链表中的元素按照元素最近被访问的时间顺序排序;当字典中的某个元素被访问时会被移动到链表头,当链表的空间满了之后会移除链表尾部的元素,因为链表尾部的元素是不被重复使用的元素
class LRUCache {
// 双向链表节点定义
class Node {
int key;
int val;
Node prev;
Node next;
}
private int capacity;
//保存链表的头节点和尾节点
private Node first;
private Node last;
private Map<Integer, Node> map;
public LRUCache(int capacity) {
this.capacity = capacity;
map = new HashMap<>(capacity);
}
public int get(int key) {
Node node = map.get(key);
//为空返回-1
if (node == null) {
return -1;
}
moveToHead(node);
return node.val;
}
private void moveToHead(Node node) {
if (node == first) {
return;
} else if (node == last) {
last.prev.next = null;
last = last.prev;
} else {
node.prev.next = node.next;
node.next.prev = node.prev;
}
node.prev = first.prev;
node.next = first;
first.prev = node;
first = node;
}
public void put(int key, int value) {
Node node = map.get(key);
if (node == null) {
node = new Node();
node.key = key;
node.val = value;
if(map.size() == capacity) {
removeLast();
}
addToHead(node);
map.put(key, node);
} else {
node.val = value;
moveToHead(node);
}
}
private void addToHead(Node node) {
if (map.isEmpty()) {
first = node;
last = node;
} else {
node.next = first;
first.prev = node;
first = node;
}
}
private void removeLast() {
map.remove(last.key);
Node prevNode = last.prev;
if (prevNode != null) {
prevNode.next = null;
last = prevNode;
}
}
@Override
public String toString() {
return map.keySet().toString();
}
public static void main(String[] args) {
LRUCache cache = new LRUCache(3);
cache.put(1, 1);
cache.put(2, 2);
cache.put(3, 3);
cache.get(1);
cache.put(4, 3);
System.out.println(cache);
}
}
public class LRUCache<K,V> extends LinkedHashMap<K, V>{
//首先设定最大缓存空间 MAX_ENTRIES 为 3;
private static final int MAX_ENTRIES = 3;
//之后使用LinkedHashMap的构造函数将 accessOrder设置为 true,开启 LRU顺序;
public LRUCache() {
super(MAX_ENTRIES, 0.75f, true);
}
//最后覆盖removeEldestEntry()方法实现,在节点多于 MAX_ENTRIES 就会将最近最少使用的数据移除。
//因为这个函数默认返回false,不重写的话缓存爆了的时候无法删除最近最久未使用的节点
@Override
protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
//在容量超过最大允许节点数的时候返回true,使得在afterNodeInsertion函数中能执行removeNode()
return size() > MAX_ENTRIES;
}
public static void main(String[] args) {
LRUCache<Integer, Integer> cache = new LRUCache<>();
cache.put(1, 1);
cache.put(2, 2);
cache.put(3, 3);
cache.get(1);
cache.put(4, 4);
System.out.println(cache.keySet());
}
}
近似LRU算法
1、Redis采用近似LRU算法进行缓存淘汰,不使用LRU算法的原因是需要消耗大量的内存且要对现有的数据结构进行较大的改造;
2、Redis近似LRU算法的实现,每个key增加了一个长度24bit的字段用于存放最后一次访问的时间戳
3、Redis LRU淘汰采用懒惰处理且只有懒惰处理,当Redis执行写操作时如果使用的内存超过maxmemory的配置时就会执行一次LRU淘汰算法,随机采样出5(可以配置)key,然后删除最旧的key,如果淘汰后的内存使用量还是超过了maxmemory就继续随机采样直到内存低于maxmemory为止(采样策略使用maxmemory-policy:volatile-lru、volatile-ttl、volatile-random、noeviction、all-lru、all-random,采样数量使用maxmemory_samples)
4、采样数量越大近似LRU算法的效果越严格接近LRU算法,Redis3.0优化近似LRU算法新增了淘汰池;淘汰池是一个数组,它的大小是maxmemory_samples,在每次淘汰循环中新随机出来的key列表会和淘汰池中的key列表进行融合,淘汰掉最旧的一个key之后保留剩余较旧的key列表放入淘汰池中留待下一个循环