引言
Redis中将数据存储在内存中, 内存是有限的, 如果一直往里面放数据, 总会有放满的一天, 如果不进行特殊处理, 放满之后再进行放元素则会OOM(个人感觉), 所以Redis有很多中淘汰策略, 以下是具体提供的淘汰策略
- noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
- allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键
- volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键
- allkeys-random:加入键的时候如果过限,从所有key随机删除
- volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐
- volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
- volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
- allkeys-lfu:从所有键中驱逐使用频率最少的键
实现
那么具体的淘汰策略算法是如何实现的, 拿LRU(Least Recently Used)最近最少使用淘汰策略 来举例, 这也是面试当中经常被问到的题目. 具体的场景就是设计一个容器, 容器提供以下操作
- 初始化: 可以设置容器存放元素的大小
- 新增: 往容器里新增元素, 如果容器满了, 需要淘汰之前最早使用到的元素
- 查询: 在容器中查询某个元素
- 删除: 在容器中移除某个元素
双向链表法
我们很容易就能想到用链表在实现LRU淘汰策略, 具体的实现就是维护一个链表, 每次操作时, 需要遍历链表来判断当前的元素是否存在在链表中, 然后进行一些移除、将元素放入链表头的操作, 具体实现如下
/**
* @program:
* @description: 用双向链表实现LRU淘汰策略
* @create: 2021/7/12 下午11:00
**/
public class SimpleLRU<K, V> {
private static final int DEFAULT_CAPACITY = 10;
private int capacity; // 容量
private int length; // 当前长度
private Node<K, V> headNode; // 链表的头指针
private Node<K, V> tailNode; // 链表的尾指针
public SimpleLRU() {
this(DEFAULT_CAPACITY);
}
public SimpleLRU(int capacity) {
this.capacity = capacity;
headNode = new Node<>();
tailNode = new Node<>();
this.length = 0;
}
/**
* 新增
* @param key
* @param value
*/
public void add(K key, V value) {
Node<K, V> node = getNode(key);
if (node != null) {
node.value = value;
moveToHead(node);
} else { // 不存在
Node<K, V> newNode = new Node<>(key, value);
if (++length > capacity) { // 如果超出容量的话,需要移除尾结点
popTail(); // 移除链表最后一个节点
--length;
}
addNode(newNode);
}
}
private void addNode(Node<K,V> node) {
headNode.next.prev = node;
node.next = headNode.next;
node.prev = headNode;
headNode.next = node;
}
private void popTail() {
tailNode.prev.prev.next = tailNode;
tailNode.prev = tailNode.prev.prev;
}
private void moveToHead(Node<K,V> node) {
headNode.next.prev = node;
node.next = headNode.next;
headNode.next = node;
node.prev = headNode;
}
/**
* 查询
* @param key
* @return
*/
public V get(K key) {
Node<K, V> node = getNode(key);
if (node == null) return null;
moveToHead(node);
return node.value;
}
public Node getNode(K key) {
Node<K, V> dummyNode = headNode.next; // 当前指针
// 查找链表中是否存在当前的 key
while (dummyNode != null && dummyNode.key != key) {
dummyNode = dummyNode.next;
}
if (dummyNode == null) return null;
return dummyNode;
}
/**
* 移除结点数据
* @param key
*/
public void remove(K key) {
Node<K, V> node = getNode(key);
if (node == null) return;
removeNode(node);
}
private void removeNode(Node<K,V> node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
static class Node<K, V> {
K key;
V value;
Node prev;
Node next;
public Node() {}
public Node(K key, V value) {
this.key = key;
this.value = value;
}
}
}
散列表+双向链表法
由于每次操作都需要遍历整个链表, 所以时间复杂度时O(N), 这种实现在实际应用场景肯定是不行的, 容器里元素越多, 操作越慢. 重点就是每次操作都需要遍历整个链表, 依次比较来获取我们想要的元素, 这时可以引入散列表, 这样根据key获取value, 时间复杂度是O(1), 性能更好, 具体实现如下
/**
* @program:
* @description:
* 核心:通过前驱和后驱指针
* Least Recently Used
* 时间复杂度O(1), 通过散列表和双向链表完成
* 提供的操作:
* 1。 往集合中添加元素
* 2。 最后使用最先淘汰
* 3。 有初始化的一个长度
*
* @create: 2021/7/9 下午10:56
**/
public class LRUDemo<K, V> {
private static final Integer DEFAULT_CAPACITY = 10;
private int length;
private int capacity;
private DNode<K, V> headNode;
private DNode<K, V> tailNode;
private Map<K, DNode<K, V>> table;
public LRUDemo() {
this(DEFAULT_CAPACITY);
}
public LRUDemo(int capacity) {
this.capacity = capacity;
length = 0;
headNode = new DNode<>();
tailNode = new DNode<>();
headNode.next = tailNode;
tailNode.prev = headNode;
table = new HashMap<>();
}
static class DNode<K, V> {
private K key;
private V value;
private DNode prev;
private DNode next;
DNode() { }
DNode(K key, V value) {
this.key = key;
this.value = value;
}
}
/**
* 新增
* @param key
* @param value
*/
public void add(K key, V value) {
DNode<K, V> node = table.get(key);
if (node == null) {
DNode<K, V> newNode = new DNode<>(key , value);
if (++length > capacity) {
DNode<K, V> tail = popTail();
removeNode(tail);
}
table.put(key, newNode);
addNode(node);
} else {
node.value = value;
moveToHead(node);
}
}
private DNode<K,V> popTail() {
DNode<K, V> tail = tailNode.prev;
removeNode(tail);
return tail;
}
private void addNode(DNode<K,V> node) {
headNode.next.prev = node;
node.next = headNode.next;
this.headNode.next = node;
node.prev = headNode;
}
private void moveToHead(DNode<K,V> node) {
removeNode(node);
addNode(node);
}
/**
* 获取结点数据
* @param key
* @return
*/
public V get(K key) {
DNode<K, V> node = table.get(key);
if (node == null) {
return null;
}
moveToHead(node);
return node.value;
}
/**
* 移除结点
* @param
*/
public void removeNode(DNode<K, V> node) {
node.prev.next = node.next;
node.next.prev = node.prev;
table.remove(node.key);
}
}
总结
一般这种算法题, 都是通过引入数据结构, 通过一定的算法进行实现, 链表类的话, 就需要自己多在纸上画图, 节点和节点之间的prev和next指针, 图一画, 就很清晰, 接下来要做的就是指针赋值即可