目录
淘汰策略方案(8种)
LRU和LFU策略的区别
使用建议
手搓LRU算法
方式一
方式二
大家好,我是jstart千语。今天和大家回来聊一下redis,这次要讲的是它的淘汰策略。为什么需要淘汰策略呢,就是当redis里面的内存占满后,存不下数据了,那么新加入的数据该如何处理呢?这种处理的方式不同,就称为不同的数据淘汰策略。redis支持8种不同的淘汰策略。
淘汰策略方案(8种)
noeviction | 默认策略,不淘汰,当内存满时写入会报错(OOM)。适合读多写少的缓存场景 |
volatile-ttl | 从设置了过期时间的 key 中,优先淘汰即将过期的。 |
allkey-random | 随机淘汰一个 key。适用于不关心数据使用频率的场景。 |
volatile-random | 从设置了过期时间的 key 中随机淘汰一个。 |
allkeys-lru | (最近最少使用)从所有 key 中淘汰最少使用的(LRU:Least Recently Used)。 |
volatile-lru | (最近最少使用)从设置了过期时间的 key 中,淘汰最少使用的。 |
allkeys-lfu | (频率)从所有 key 中淘汰最不常用的(LFU:Least Frequently Used)。 |
volatile-lfu | (频率)从设置了过期时间的 key 中,淘汰最不常用的。 |
LRU和LFU策略的区别
LRU:最近最少使用,用当前时间减去最后一次访问的时间,这个值越大越先被淘汰。
例如:key1是5秒前被访问,key2是10秒前被访问,那么就是key2优先被淘汰。
LFU:最少频率使用,统计每个key的访问频率,值越小越先被淘汰。
例如:key1在5秒内被访问了3次,key2在10秒内被访问了5次。那么key2优先被淘汰。
使用建议
1、优先使用allkeys-lru策略。充分利用LRU算法的优势,把最近最常访问的数据留在缓存中。如果业务中有明显的冷热数据区分,建议使用该策略。
为什么这里不使用LFU呢?:
因为数据使用频率可能是不固定的,在某一段时间内访问频率高,另外一段时间访问频率低,可能导致误删。具体还要看业务需求
2、如果业务中数据访问频率区别不大,没有明显的冷热数据区分,建议使用allkeys-random,随机选择淘汰。
3、如果业务中有置顶需求,可以使用volatile-lru策略,同时置顶数据不设置过期时间,这些置顶的数据就一直不被删除,会淘汰其他设置过期时间的数据。
4、如果业务中有短时高频访问的数据,可以使用allkeys-lfu或者volatile-lfu策略。
手搓LRU算法
方式一
利用Java的LinkedHashMap天然支持按访问顺序排序,通过重写removeEldestEntry方法可实现LRU逻辑。
public class LRUCache<K, V> extends LinkedHashMap<K, V> {private final int capacity;public LRUCache(int capacity) {// 初始容量,负载因子,accessOrder=true表示按访问顺序排序super(capacity, 0.75f, true);this.capacity = capacity;}@Overrideprotected boolean removeEldestEntry(Map.Entry<K, V> eldest) {// 当大小超过容量时移除最旧元素return size() > capacity;}
}
使用示例:
LRUCache<String, String> cache = new LRUCache<>(3);
cache.put("a", "1");
cache.put("b", "2");
cache.get("a"); // 访问"a"使其变为最近使用
cache.put("c", "3");
cache.put("d", "4"); // 插入"d"时,容量超限,淘汰最久未使用的"b"
注意:非线程安全,需在单线程环境使用或通过Collections.synchronizedMap包装。
方式二
手动实现(哈希表 + 双向链表)。实现O(1)时间复杂度的get和put操作,需结合哈希表与双向链表。
步骤:
- 定义节点结构:包含前后指针及键值。
- 维护双向链表:头部放最新访问节点,尾部为待淘汰节点。
- 哈希表映射:快速定位节点位置。
import java.util.HashMap;
import java.util.Map;public class ManualLRUCache<K, V> {static class Node<K, V> {K key;V value;Node<K, V> prev;Node<K, V> next;Node(K key, V value) {this.key = key;this.value = value;}}private final int capacity;private final Map<K, Node<K, V>> cache = new HashMap<>();private Node<K, V> head; // 虚拟头节点private Node<K, V> tail; // 虚拟尾节点public ManualLRUCache(int capacity) {this.capacity = capacity;head = new Node<>(null, null);tail = new Node<>(null, null);head.next = tail;tail.prev = head;}public V get(K key) {Node<K, V> node = cache.get(key);if (node == null) return null;moveToHead(node); // 访问后移至头部return node.value;}public void put(K key, V value) {Node<K, V> node = cache.get(key);if (node != null) {node.value = value;moveToHead(node);} else {node = new Node<>(key, value);cache.put(key, node);addToHead(node);if (cache.size() > capacity) {Node<K, V> removed = removeTail();cache.remove(removed.key);}}}private void moveToHead(Node<K, V> node) {removeNode(node);addToHead(node);}private void addToHead(Node<K, V> node) {node.prev = head;node.next = head.next;head.next.prev = node;head.next = node;}private void removeNode(Node<K, V> node) {node.prev.next = node.next;node.next.prev = node.prev;}private Node<K, V> removeTail() {Node<K, V> res = tail.prev;removeNode(res);return res;}
}
线程安全优化:
- 使用ConcurrentHashMap替代HashMap。
- 对链表操作加锁(如ReentrantLock或synchronized),但可能影响性能。
- 考虑读写锁(ReadWriteLock),允许并发读,写时独占。