前言

java从零手写实现redis(一)如何实现固定大小的缓存?

java从零手写实现redis(三)redis expire 过期原理

java从零手写实现redis(三)内存数据如何重启不丢失?

java从零手写实现redis(四)添加监听器

java从零手写实现redis(五)过期策略的另一种实现思路

java从零手写实现redis(六)AOF 持久化原理详解及实现

java从零手写实现redis(七)LRU 缓存淘汰策略详解

从零开始手写 redis(八)朴素 LRU 淘汰算法性能优化

前两节我们分别实现了 LRU 算法,并且进行了性能优化。

本节作为 LRU 算法的最后一节,主要解决一下缓存污染的问题。

LRU 基础知识

是什么

LRU算法全称是最近最少使用算法(Least Recently Use),广泛的应用于缓存机制中。

当缓存使用的空间达到上限后,就需要从已有的数据中淘汰一部分以维持缓存的可用性,而淘汰数据的选择就是通过LRU算法完成的。

LRU算法的基本思想是基于局部性原理的时间局部性:

如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。

拓展阅读

Apache Commons LRUMAP 源码详解

Redis 当做 LRU MAP 使用

java 从零开始手写 redis(七)redis LRU 驱除策略详解及实现

朴素 LRU 算法的不足

当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。

扩展算法

1. LRU-K

LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。

LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。

相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。

当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。

数据第一次被访问时,加入到历史访问列表,如果数据在访问历史列表中没有达到K次访问,则按照一定的规则(FIFO,LRU)淘汰;

当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列中删除,将数据移到缓存队列中,并缓存数据,缓存队列重新按照时间排序;

缓存数据队列中被再次访问后,重新排序,需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即“淘汰倒数K次访问离现在最久的数据”。

LRU-K具有LRU的优点,同时还能避免LRU的缺点,实际应用中LRU-2是综合最优的选择。

由于LRU-K还需要记录那些被访问过、但还没有放入缓存的对象,因此内存消耗会比LRU要多。

2. two queue

Two queues(以下使用2Q代替)算法类似于LRU-2,不同点在于2Q将LRU-2算法中的访问历史队列(注意这不是缓存数据的)改为一个FIFO缓存队列,即:2Q算法有两个缓存队列,一个是FIFO队列,一个是LRU队列。

当数据第一次访问时,2Q算法将数据缓存在FIFO队列里面,当数据第二次被访问时,则将数据从FIFO队列移到LRU队列里面,两个队列各自按照自己的方法淘汰数据。

新访问的数据插入到FIFO队列中,如果数据在FIFO队列中一直没有被再次访问,则最终按照FIFO规则淘汰;

如果数据在FIFO队列中再次被访问到,则将数据移到LRU队列头部,如果数据在LRU队列中再次被访问,则将数据移动LRU队列头部,LRU队列淘汰末尾的数据。

3. Multi Queue(MQ)

MQ算法根据访问频率将数据划分为多个队列,不同的队列具有不同的访问优先级,其核心思想是:优先缓存访问次数多的数据

详细的算法结构图如下,Q0,Q1....Qk代表不同的优先级队列,Q-history代表从缓存中淘汰数据,但记录了数据的索引和引用次数的队列:

新插入的数据放入Q0,每个队列按照LRU进行管理,当数据的访问次数达到一定次数,需要提升优先级时,将数据从当前队列中删除,加入到高一级队列的头部;为了防止高优先级数据永远不会被淘汰,当数据在指定的时间里没有被访问时,需要降低优先级,将数据从当前队列删除,加入到低一级的队列头部;需要淘汰数据时,从最低一级队列开始按照LRU淘汰,每个队列淘汰数据时,将数据从缓存中删除,将数据索引加入Q-history头部。

如果数据在Q-history中被重新访问,则重新计算其优先级,移到目标队列头部。

Q-history按照LRU淘汰数据的索引。

MQ需要维护多个队列,且需要维护每个数据的访问时间,复杂度比LRU高。

LRU算法对比

对比点 对比
命中率 LRU-2 > MQ(2) > 2Q > LRU
复杂度 LRU-2 > MQ(2) > 2Q > LRU
代价 LRU-2 > MQ(2) > 2Q > LRU

个人理解

实际上上面的几个算法,思想上大同小异。

核心目的:解决批量操作导致热点数据失效,缓存被污染的问题。

实现方式:增加一个队列,用来保存只访问一次的数据,然后根据次数不同,放入到 LRU 中。

只访问一次的队列,可以是 FIFO 队列,可以是 LRU,我们来实现一下 2Q 和 LRU-2 两种实现。

2Q

实现思路

实际上就是我们以前的 FIFO + LRU 二者的结合。

代码实现

基本属性

public class CacheEvictLru2Q<K,V> extends AbstractCacheEvict<K,V> {

    private static final Log log = LogFactory.getLog(CacheEvictLru2Q.class);

    /**
* 队列大小限制
*
* 降低 O(n) 的消耗,避免耗时过长。
* @since 0.0.13
*/
private static final int LIMIT_QUEUE_SIZE = 1024; /**
* 第一次访问的队列
* @since 0.0.13
*/
private Queue<K> firstQueue; /**
* 头结点
* @since 0.0.13
*/
private DoubleListNode<K,V> head; /**
* 尾巴结点
* @since 0.0.13
*/
private DoubleListNode<K,V> tail; /**
* map 信息
*
* key: 元素信息
* value: 元素在 list 中对应的节点信息
* @since 0.0.13
*/
private Map<K, DoubleListNode<K,V>> lruIndexMap; public CacheEvictLru2Q() {
this.firstQueue = new LinkedList<>();
this.lruIndexMap = new HashMap<>();
this.head = new DoubleListNode<>();
this.tail = new DoubleListNode<>(); this.head.next(this.tail);
this.tail.pre(this.head);
} }

数据淘汰

数据淘汰的逻辑:

当缓存大小,已经达到最大限制时执行:

(1)优先淘汰 firstQueue 中的数据

(2)如果 firstQueue 中数据为空,则淘汰 lruMap 中的数据信息。

这里有一个假设:我们认为被多次访问的数据,重要性高于被只访问了一次的数据。

@Override
protected ICacheEntry<K, V> doEvict(ICacheEvictContext<K, V> context) {
ICacheEntry<K, V> result = null;
final ICache<K,V> cache = context.cache();
// 超过限制,移除队尾的元素
if(cache.size() >= context.size()) {
K evictKey = null;
//1. firstQueue 不为空,优先移除队列中元素
if(!firstQueue.isEmpty()) {
evictKey = firstQueue.remove();
} else {
// 获取尾巴节点的前一个元素
DoubleListNode<K,V> tailPre = this.tail.pre();
if(tailPre == this.head) {
log.error("当前列表为空,无法进行删除");
throw new CacheRuntimeException("不可删除头结点!");
}
evictKey = tailPre.key();
}
// 执行移除操作
V evictValue = cache.remove(evictKey);
result = new CacheEntry<>(evictKey, evictValue);
}
return result;
}

数据删除

当数据被删除时调用:

这个逻辑和以前类似,只是多了一个 FIFO 队列的移除。

/**
* 移除元素
*
* 1. 获取 map 中的元素
* 2. 不存在直接返回,存在执行以下步骤:
* 2.1 删除双向链表中的元素
* 2.2 删除 map 中的元素
*
* @param key 元素
* @since 0.0.13
*/
@Override
public void removeKey(final K key) {
DoubleListNode<K,V> node = lruIndexMap.get(key);
//1. LRU 删除逻辑
if(ObjectUtil.isNotNull(node)) {
// A<->B<->C
// 删除 B,需要变成: A<->C
DoubleListNode<K,V> pre = node.pre();
DoubleListNode<K,V> next = node.next();
pre.next(next);
next.pre(pre);
// 删除 map 中对应信息
this.lruIndexMap.remove(node.key());
} else {
//2. FIFO 删除逻辑(O(n) 时间复杂度)
firstQueue.remove(key);
}
}

数据的更新

当数据被访问时,提升数据的优先级。

(1)如果在 lruMap 中,则首先移除,然后放入到头部

(2)如果不在 lruMap 中,但是在 FIFO 队列,则从 FIFO 队列中移除,添加到 LRU map 中。

(3)如果都不在,直接加入到 FIFO 队列中即可。

/**
* 放入元素
* 1. 如果 lruIndexMap 已经存在,则处理 lru 队列,先删除,再插入。
* 2. 如果 firstQueue 中已经存在,则处理 first 队列,先删除 firstQueue,然后插入 Lru。
* 1 和 2 是不同的场景,但是代码实际上是一样的,删除逻辑中做了二种场景的兼容。
*
* 3. 如果不在1、2中,说明是新元素,直接插入到 firstQueue 的开始即可。
*
* @param key 元素
* @since 0.0.13
*/
@Override
public void updateKey(final K key) {
//1.1 是否在 LRU MAP 中
//1.2 是否在 firstQueue 中
DoubleListNode<K,V> node = lruIndexMap.get(key);
if(ObjectUtil.isNotNull(node)
|| firstQueue.contains(key)) {
//1.3 删除信息
this.removeKey(key);
//1.4 加入到 LRU 中
this.addToLruMapHead(key);
return;
}
//2. 直接加入到 firstQueue 队尾
// if(firstQueue.size() >= LIMIT_QUEUE_SIZE) {
// // 避免第一次访问的列表一直增长,移除队头的元素
// firstQueue.remove();
// }
firstQueue.add(key);
}

这里我想到了一个优化点,限制 firstQueue 的一直增长,因为遍历的时间复杂度为 O(n),所以限制最大的大小为 1024。

如果超过了,则把 FIFO 中的元素先移除掉。

不过只移除 FIFO,不移除 cache,会导致二者的活跃程度不一致;

如果同时移除,但是 cache 的大小还没有满足,可能会导致超出用户的预期,这个可以作为一个优化点,暂时注释掉。

测试

代码

ICache<String, String> cache = CacheBs.<String,String>newInstance()
.size(3)
.evict(CacheEvicts.<String, String>lru2Q())
.build(); cache.put("A", "hello");
cache.put("B", "world");
cache.put("C", "FIFO"); // 访问一次A
cache.get("A");
cache.put("D", "LRU"); Assert.assertEquals(3, cache.size());
System.out.println(cache.keySet());

效果

[DEBUG] [2020-10-03 13:15:50.670] [main] [c.g.h.c.c.s.l.r.CacheRemoveListener.listen] - Remove key: B, value: world, type: evict
[D, A, C]

LRU-2 实现

说明

FIFO 中的缺点还是比较明显的,需要 O(n) 的时间复杂度做遍历。

而且命中率和 LRU-2 比起来还是会差一点。

准备工作

这里 LRU map 出现了多次,我们为了方便,将 LRU map 简单的封装为一个数据结构。

我们使用双向链表+HashMap 实现一个简单版本的。

节点

node 节点和以前一致:

public class DoubleListNode<K,V> {

    /**
* 键
* @since 0.0.12
*/
private K key; /**
* 值
* @since 0.0.12
*/
private V value; /**
* 前一个节点
* @since 0.0.12
*/
private DoubleListNode<K,V> pre; /**
* 后一个节点
* @since 0.0.12
*/
private DoubleListNode<K,V> next; //fluent getter & setter
}

接口

我们根据自己的需要,暂时定义 3 个最重要的方法。

/**
* LRU map 接口
* @author binbin.hou
* @since 0.0.13
*/
public interface ILruMap<K,V> { /**
* 移除最老的元素
* @return 移除的明细
* @since 0.0.13
*/
ICacheEntry<K, V> removeEldest(); /**
* 更新 key 的信息
* @param key key
* @since 0.0.13
*/
void updateKey(final K key); /**
* 移除对应的 key 信息
* @param key key
* @since 0.0.13
*/
void removeKey(final K key); /**
* 是否为空
* @return 是否
* @since 0.0.13
*/
boolean isEmpty(); /**
* 是否包含元素
* @param key 元素
* @return 结果
* @since 0.0.13
*/
boolean contains(final K key);
}

实现

我们基于 DoubleLinkedList + HashMap 实现。

就是把上一节中的实现整理一下即可。

import com.github.houbb.cache.api.ICacheEntry;
import com.github.houbb.cache.core.exception.CacheRuntimeException;
import com.github.houbb.cache.core.model.CacheEntry;
import com.github.houbb.cache.core.model.DoubleListNode;
import com.github.houbb.cache.core.support.struct.lru.ILruMap;
import com.github.houbb.heaven.util.lang.ObjectUtil;
import com.github.houbb.log.integration.core.Log;
import com.github.houbb.log.integration.core.LogFactory; import java.util.HashMap;
import java.util.Map; /**
* 基于双向列表的实现
* @author binbin.hou
* @since 0.0.13
*/
public class LruMapDoubleList<K,V> implements ILruMap<K,V> { private static final Log log = LogFactory.getLog(LruMapDoubleList.class); /**
* 头结点
* @since 0.0.13
*/
private DoubleListNode<K,V> head; /**
* 尾巴结点
* @since 0.0.13
*/
private DoubleListNode<K,V> tail; /**
* map 信息
*
* key: 元素信息
* value: 元素在 list 中对应的节点信息
* @since 0.0.13
*/
private Map<K, DoubleListNode<K,V>> indexMap; public LruMapDoubleList() {
this.indexMap = new HashMap<>();
this.head = new DoubleListNode<>();
this.tail = new DoubleListNode<>(); this.head.next(this.tail);
this.tail.pre(this.head);
} @Override
public ICacheEntry<K, V> removeEldest() {
// 获取尾巴节点的前一个元素
DoubleListNode<K,V> tailPre = this.tail.pre();
if(tailPre == this.head) {
log.error("当前列表为空,无法进行删除");
throw new CacheRuntimeException("不可删除头结点!");
} K evictKey = tailPre.key();
V evictValue = tailPre.value(); return CacheEntry.of(evictKey, evictValue);
} /**
* 放入元素
*
* (1)删除已经存在的
* (2)新元素放到元素头部
*
* @param key 元素
* @since 0.0.12
*/
@Override
public void updateKey(final K key) {
//1. 执行删除
this.removeKey(key); //2. 新元素插入到头部
//head<->next
//变成:head<->new<->next
DoubleListNode<K,V> newNode = new DoubleListNode<>();
newNode.key(key); DoubleListNode<K,V> next = this.head.next();
this.head.next(newNode);
newNode.pre(this.head);
next.pre(newNode);
newNode.next(next); //2.2 插入到 map 中
indexMap.put(key, newNode);
} /**
* 移除元素
*
* 1. 获取 map 中的元素
* 2. 不存在直接返回,存在执行以下步骤:
* 2.1 删除双向链表中的元素
* 2.2 删除 map 中的元素
*
* @param key 元素
* @since 0.0.13
*/
@Override
public void removeKey(final K key) {
DoubleListNode<K,V> node = indexMap.get(key); if(ObjectUtil.isNull(node)) {
return;
} // 删除 list node
// A<->B<->C
// 删除 B,需要变成: A<->C
DoubleListNode<K,V> pre = node.pre();
DoubleListNode<K,V> next = node.next(); pre.next(next);
next.pre(pre); // 删除 map 中对应信息
this.indexMap.remove(key);
} @Override
public boolean isEmpty() {
return indexMap.isEmpty();
} @Override
public boolean contains(K key) {
return indexMap.containsKey(key);
}
}

实现思路

LRU 的实现保持不变。我们直接将 FIFO 替换为 LRU map 即可。

为了便于理解,我们将 FIFO 对应为 firstLruMap,用来存放用户只访问了一次的元素。

将原来的 LRU 中存入访问了 2 次及其以上的元素。

其他逻辑和 2Q 保持一致。

实现

基本属性

定义两个 LRU,用来分别存储访问的信息

public class CacheEvictLru2<K,V> extends AbstractCacheEvict<K,V> {

    private static final Log log = LogFactory.getLog(CacheEvictLru2.class);

    /**
* 第一次访问的 lru
* @since 0.0.13
*/
private final ILruMap<K,V> firstLruMap; /**
* 2次及其以上的 lru
* @since 0.0.13
*/
private final ILruMap<K,V> moreLruMap; public CacheEvictLru2() {
this.firstLruMap = new LruMapDoubleList<>();
this.moreLruMap = new LruMapDoubleList<>();
} }

淘汰实现

和 lru 2Q 模式类似,这里我们优先淘汰 firstLruMap 中的数据信息。

@Override
protected ICacheEntry<K, V> doEvict(ICacheEvictContext<K, V> context) {
ICacheEntry<K, V> result = null;
final ICache<K,V> cache = context.cache();
// 超过限制,移除队尾的元素
if(cache.size() >= context.size()) {
ICacheEntry<K,V> evictEntry = null;
//1. firstLruMap 不为空,优先移除队列中元素
if(!firstLruMap.isEmpty()) {
evictEntry = firstLruMap.removeEldest();
log.debug("从 firstLruMap 中淘汰数据:{}", evictEntry);
} else {
//2. 否则从 moreLruMap 中淘汰数据
evictEntry = moreLruMap.removeEldest();
log.debug("从 moreLruMap 中淘汰数据:{}", evictEntry);
}
// 执行缓存移除操作
final K evictKey = evictEntry.key();
V evictValue = cache.remove(evictKey);
result = new CacheEntry<>(evictKey, evictValue);
}
return result;
}

删除

/**
* 移除元素
*
* 1. 多次 lru 中存在,删除
* 2. 初次 lru 中存在,删除
*
* @param key 元素
* @since 0.0.13
*/
@Override
public void removeKey(final K key) {
//1. 多次LRU 删除逻辑
if(moreLruMap.contains(key)) {
moreLruMap.removeKey(key);
log.debug("key: {} 从 moreLruMap 中移除", key);
} else {
firstLruMap.removeKey(key);
log.debug("key: {} 从 firstLruMap 中移除", key);
}
}

更新

/**
* 更新信息
* 1. 如果 moreLruMap 已经存在,则处理 more 队列,先删除,再插入。
* 2. 如果 firstLruMap 中已经存在,则处理 first 队列,先删除 firstLruMap,然后插入 Lru。
* 1 和 2 是不同的场景,但是代码实际上是一样的,删除逻辑中做了二种场景的兼容。
*
* 3. 如果不在1、2中,说明是新元素,直接插入到 firstLruMap 的开始即可。
*
* @param key 元素
* @since 0.0.13
*/
@Override
public void updateKey(final K key) {
//1. 元素已经在多次访问,或者第一次访问的 lru 中
if(moreLruMap.contains(key)
|| firstLruMap.contains(key)) {
//1.1 删除信息
this.removeKey(key);
//1.2 加入到多次 LRU 中
moreLruMap.updateKey(key);
log.debug("key: {} 多次访问,加入到 moreLruMap 中", key);
} else {
// 2. 加入到第一次访问 LRU 中
firstLruMap.updateKey(key);
log.debug("key: {} 为第一次访问,加入到 firstLruMap 中", key);
}
}

实际上使用 LRU-2 的代码逻辑反而变得清晰了一些,主要是因为我们把 lruMap 作为独立的数据结构抽离了出去。

测试

代码

ICache<String, String> cache = CacheBs.<String,String>newInstance()
.size(3)
.evict(CacheEvicts.<String, String>lru2Q())
.build();
cache.put("A", "hello");
cache.put("B", "world");
cache.put("C", "FIFO");
// 访问一次A
cache.get("A");
cache.put("D", "LRU");
Assert.assertEquals(3, cache.size());
System.out.println(cache.keySet());

日志

为了便于定位分析,源代码实现的时候,加了一点日志。

[DEBUG] [2020-10-03 14:39:04.966] [main] [c.g.h.c.c.s.e.CacheEvictLru2.updateKey] - key: A 为第一次访问,加入到 firstLruMap 中
[DEBUG] [2020-10-03 14:39:04.967] [main] [c.g.h.c.c.s.e.CacheEvictLru2.updateKey] - key: B 为第一次访问,加入到 firstLruMap 中
[DEBUG] [2020-10-03 14:39:04.968] [main] [c.g.h.c.c.s.e.CacheEvictLru2.updateKey] - key: C 为第一次访问,加入到 firstLruMap 中
[DEBUG] [2020-10-03 14:39:04.970] [main] [c.g.h.c.c.s.e.CacheEvictLru2.removeKey] - key: A 从 firstLruMap 中移除
[DEBUG] [2020-10-03 14:39:04.970] [main] [c.g.h.c.c.s.e.CacheEvictLru2.updateKey] - key: A 多次访问,加入到 moreLruMap 中
[DEBUG] [2020-10-03 14:39:04.972] [main] [c.g.h.c.c.s.e.CacheEvictLru2.doEvict] - 从 firstLruMap 中淘汰数据:EvictEntry{key=B, value=null}
[DEBUG] [2020-10-03 14:39:04.974] [main] [c.g.h.c.c.s.l.r.CacheRemoveListener.listen] - Remove key: B, value: world, type: evict
[DEBUG] [2020-10-03 14:39:04.974] [main] [c.g.h.c.c.s.e.CacheEvictLru2.updateKey] - key: D 为第一次访问,加入到 firstLruMap 中
[D, A, C]

小结

对于 LRU 算法的改进我们主要做了两点:

(1)性能的改进,从 O(N) 优化到 O(1)

(2)批量操作的改进,避免缓存污染

其实除了 LRU,我们还有其他的淘汰策略。

我们需要考虑下面的问题:

A 数据被访问了 10 次,B 数据被访问了 2 次。那么二者谁是热点数据呢?

如果你认为肯定 A 是热点数据,这里实际上是另一种淘汰算法,基于 LFU 的淘汰算法,认为访问次数越多,就越是热点数据

我们下一节共同学习下 LFU 淘汰算法的实现。

开源地址:https://github.com/houbb/cache

觉得本文对你有帮助的话,欢迎点赞评论收藏关注一波,你的鼓励,是我最大的动力~

目前我们通过两次优化,解决了性能问题,和批量导致的缓存污染问题。

不知道你有哪些收获呢?或者有其他更多的想法,欢迎留言区和我一起讨论,期待与你的思考相遇。

java 从零开始手写 redis(九)LRU 缓存淘汰算法如何避免缓存污染的更多相关文章

  1. java 从零开始手写 RPC (03) 如何实现客户端调用服务端?

    说明 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 写完了客户端和服务端,那么如何实现客户端和服务端的 ...

  2. java 从零开始手写 RPC (04) -序列化

    序列化 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何实 ...

  3. java 从零开始手写 RPC (05) reflect 反射实现通用调用之服务端

    通用调用 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何 ...

  4. java 从零开始手写 RPC (07)-timeout 超时处理

    <过时不候> 最漫长的莫过于等待 我们不可能永远等一个人 就像请求 永远等待响应 超时处理 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RP ...

  5. java 从零开始手写 RPC (01) 基于 websocket 实现

    RPC 解决的问题 RPC 主要是为了解决的两个问题: 解决分布式系统中,服务之间的调用问题. 远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑. 这一节我们来学习下如何基于 we ...

  6. 昨天面试被问到的 缓存淘汰算法FIFO、LRU、LFU及Java实现

    缓存淘汰算法 在高并发.高性能的质量要求不断提高时,我们首先会想到的就是利用缓存予以应对. 第一次请求时把计算好的结果存放在缓存中,下次遇到同样的请求时,把之前保存在缓存中的数据直接拿来使用. 但是, ...

  7. 用Java写一个分布式缓存——缓存淘汰算法

    前言 之前也用过一些缓存中间件,框架,也想着自己是不是也能用Java写一个出来,于是就有了这个想法,打算在写的过程中同步进行总结. 源码:weloe/Java-Distributed-Cache (g ...

  8. 04 | 链表(上):如何实现LRU缓存淘汰算法?

    今天我们来聊聊“链表(Linked list)”这个数据结构.学习链表有什么用呢?为了回答这个问题,我们先来讨论一个经典的链表应用场景,那就是+LRU+缓存淘汰算法. 缓存是一种提高数据读取性能的技术 ...

  9. 链表:如何实现LRU缓存淘汰算法?

    缓存淘汰策略: FIFO:先入先出策略 LFU:最少使用策略 LRU:最近最少使用策略   链表的数据结构: 可以看到,数组需要连续的内存空间,当内存空间充足但不连续时,也会申请失败触发GC,链表则可 ...

  10. 《数据结构与算法之美》 <04>链表(上):如何实现LRU缓存淘汰算法?

    今天我们来聊聊“链表(Linked list)”这个数据结构.学习链表有什么用呢?为了回答这个问题,我们先来讨论一个经典的链表应用场景,那就是 LRU 缓存淘汰算法. 缓存是一种提高数据读取性能的技术 ...

随机推荐

  1. Clock Domain Crossing

    Clock Domain Crossing CDC问题主要有亚稳态问题,多比特信号同步,握手信号同步,异步Fifo等 Topics Describe the SoC Design Issues Und ...

  2. 【canvas】 绘制七巧板

    效果图: 代码 : <!DOCTYPE html> <html lang="en"> <head> <meta charset=" ...

  3. [转帖]Linux内核参数 rp_filter

    https://www.cnblogs.com/chenmh/p/6001977.html 简介 rp_filter (Reverse Path Filtering)参数定义了网卡对接收到的数据包进行 ...

  4. [转帖]Linux——Shell脚本参数传递的2种方法

    https://www.cnblogs.com/caoweixiong/p/12334418.html 前言 平时会遇到很多脚本都有参数选项,类似: ./test.sh -f config.conf ...

  5. [转帖]kafka_export 部署实战

    https://zhuanlan.zhihu.com/p/57704357 Kafka Exporter 监控 Kafka 实时数据 需要安装的组件 Prometheus:时序数据库,按时间保存监控历 ...

  6. [转帖]s3fs

    https://github.com/s3fs-fuse/s3fs-fuse s3fs allows Linux, macOS, and FreeBSD to mount an S3 bucket v ...

  7. [转帖]skywalking配置nacos集群模式

    版本: name version nacos 1.1.0 skywalking 6.2.0 elasticsearch 6.3.2 es集群管理工具 cerebro-0.8.3 https://git ...

  8. CoreMark简单学习

    CoreMark简单学习 下载 https://github.com/eembc/coremark-pro 需要注意, 其实coremark是有两组. 一组是coremark 一组是 coremark ...

  9. [转帖]Shell脚本中利用expect实现非交互式

    https://developer.aliyun.com/article/885723?spm=a2c6h.24874632.expert-profile.295.7c46cfe9h5DxWK 简介: ...

  10. drools规则动态化实践

    作者:京东物流 李振 康睿 刘斌 王北永 一 . 规则引擎业务应用背景 业务逻辑中经常会有一些冗长的判断,需要写特别多的if else,或者一些判断逻辑需要经常修改.这部分逻辑如果以java代码来实现 ...