跳过正文
  1. Posts/

缓存淘汰算法之LRU(转载)

·9 分钟
目录

参考博客

计组块忘光了呜呜呜。。。来复习一波。。

1. LRU
#

1.1. 原理

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

1.2. 实现
#

最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:

1. 新数据插入到链表头部;

2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;

3. 当链表满的时候,将链表尾部的数据丢弃。

1.3. 分析
#

【命中率】

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

【复杂度】

实现简单。

【代价】

命中时需要遍历链表,找到命中的数据块索引,然后需要将数据移到头部。

2. LRU-K
#

2.1. 原理
#

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

2.2. 实现
#

相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。详细实现如下:

1. 数据第一次被访问,加入到访问历史列表;

2. 如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;

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

4. 缓存数据队列中被再次访问后,重新排序;

5. 需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。

LRU-K具有LRU的优点,同时能够避免LRU的缺点,实际应用中LRU-2是综合各种因素后最优的选择,LRU-3或者更大的K值命中率会高,但适应性差,需要大量的数据访问才能将历史访问记录清除掉。

2.3. 分析
#

【命中率】

LRU-K降低了“缓存污染”带来的问题,命中率比LRU要高。

【复杂度】

LRU-K队列是一个优先级队列,算法复杂度和代价比较高。

【代价】

由于LRU-K还需要记录那些被访问过、但还没有放入缓存的对象,因此内存消耗会比LRU要多;当数据量很大的时候,内存消耗会比较可观。

LRU-K需要基于时间进行排序(可以需要淘汰时再排序,也可以即时排序),CPU消耗比LRU要高。

3. Two queues(2Q)
#

3.1. 原理
#

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

3.2. 实现
#

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

1. 新访问的数据插入到FIFO队列;

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

3. 如果数据在FIFO队列中被再次访问,则将数据移到LRU队列头部;

4. 如果数据在LRU队列再次被访问,则将数据移到LRU队列头部;

5. LRU队列淘汰末尾的数据。

注:上图中FIFO队列比LRU队列短,但并不代表这是算法要求,实际应用中两者比例没有硬性规定。

3.3. 分析
#

【命中率】

2Q算法的命中率要高于LRU。

【复杂度】

需要两个队列,但两个队列本身都比较简单。

【代价】

FIFO和LRU的代价之和。

2Q算法和LRU-2算法命中率类似,内存消耗也比较接近,但对于最后缓存的数据来说,2Q会减少一次从原始存储读取数据或者计算数据的操作。

4. Multi Queue(MQ)
#

4.1. 原理
#

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

4.2. 实现
#

MQ算法将缓存划分为多个LRU队列,每个队列对应不同的访问优先级。访问优先级是根据访问次数计算出来的,例如

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

如上图,算法详细描述如下:

1. 新插入的数据放入Q0;

2. 每个队列按照LRU管理数据;

3. 当数据的访问次数达到一定次数,需要提升优先级时,将数据从当前队列删除,加入到高一级队列的头部;

4. 为了防止高优先级数据永远不被淘汰,当数据在指定的时间里访问没有被访问时,需要降低优先级,将数据从当前队列删除,加入到低一级的队列头部;

5. 需要淘汰数据时,从最低一级队列开始按照LRU淘汰;每个队列淘汰数据时,将数据从缓存中删除,将数据索引加入Q-history头部;

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

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

4.3. 分析
#

【命中率】

MQ降低了“缓存污染”带来的问题,命中率比LRU要高。

【复杂度】

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

【代价】

MQ需要记录每个数据的访问时间,需要定时扫描所有队列,代价比LRU要高。

注:虽然MQ的队列看起来数量比较多,但由于所有队列之和受限于缓存容量的大小,因此这里多个队列长度之和和一个LRU队列是一样的,因此队列扫描性能也相近。

5. LRU类算法对比
#

由于不同的访问模型导致命中率变化较大,此处对比仅基于理论定性分析,不做定量分析。

对比点对比
命中率LRU-2 > MQ(2) > 2Q > LRU
复杂度LRU-2 > MQ(2) > 2Q > LRU
代价LRU-2  > MQ(2) > 2Q > LRU
实际应用中需要根据业务的需求和对数据的访问情况进行选择,并不是命中率越高越好。例如:虽然LRU看起来命中率会低一些,且存在”缓存污染“的问题,但由于其简单和代价小,实际应用中反而应用更多。

java中最简单的LRU算法实现,就是利用jdk的LinkedHashMap,覆写其中的removeEldestEntry(Map.Entry)方法即可

如果你去看LinkedHashMap的源码可知,LRU算法是通过双向链表来实现,当某个位置被命中,通过调整链表的指向将该位置调整到头位置,新加入的内容直接放在链表头,如此一来,最近被命中的内容就向链表头移动,需要替换时,链表最后的位置就是最近最少使用的位置。

 1import java.util.ArrayList;
 2import java.util.Collection;
 3import java.util.LinkedHashMap;
 4import java.util.concurrent.locks.Lock;
 5import java.util.concurrent.locks.ReentrantLock;
 6import java.util.Map;
 7
 8
 9/**
10 * 类说明:利用LinkedHashMap实现简单的缓存, 必须实现removeEldestEntry方法,具体参见JDK文档
11 *
12 * @author dennis
13 *
14 * @param <K>
15 * @param <V>
16 */
17public class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> {
18    private final int maxCapacity;
19
20    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
21
22    private final Lock lock = new ReentrantLock();
23
24    public LRULinkedHashMap(int maxCapacity) {
25        super(maxCapacity, DEFAULT_LOAD_FACTOR, true);
26        this.maxCapacity = maxCapacity;
27    }
28
29    @Override
30    protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
31        return size() > maxCapacity;
32    }
33    @Override
34    public boolean containsKey(Object key) {
35        try {
36            lock.lock();
37            return super.containsKey(key);
38        } finally {
39            lock.unlock();
40        }
41    }
42
43
44    @Override
45    public V get(Object key) {
46        try {
47            lock.lock();
48            return super.get(key);
49        } finally {
50            lock.unlock();
51        }
52    }
53
54    @Override
55    public V put(K key, V value) {
56        try {
57            lock.lock();
58            return super.put(key, value);
59        } finally {
60            lock.unlock();
61        }
62    }
63
64    public int size() {
65        try {
66            lock.lock();
67            return super.size();
68        } finally {
69            lock.unlock();
70        }
71    }
72
73    public void clear() {
74        try {
75            lock.lock();
76            super.clear();
77        } finally {
78            lock.unlock();
79        }
80    }
81
82    public Collection<Map.Entry<K, V>> getAll() {
83        try {
84            lock.lock();
85            return new ArrayList<Map.Entry<K, V>>(super.entrySet());
86        } finally {
87            lock.unlock();
88        }
89    }
90}

基于双链表 的LRU实现:

传统意义的LRU算法是为每一个Cache对象设置一个计数器,每次Cache命中则给计数器+1,而Cache用完,需要淘汰旧内容,放置新内容时,就查看所有的计数器,并将最少使用的内容替换掉。

它的弊端很明显,如果Cache的数量少,问题不会很大, 但是如果Cache的空间过大,达到10W或者100W以上,一旦需要淘汰,则需要遍历所有计算器,其性能与资源消耗是巨大的。效率也就非常的慢了。

它的原理: 将Cache的所有位置都用双连表连接起来,当一个位置被命中之后,就将通过调整链表的指向,将该位置调整到链表头的位置,新加入的Cache直接加到链表头中。

这样,在多次进行Cache操作后,最近被命中的,就会被向链表头方向移动,而没有命中的,而想链表后面移动,链表尾则表示最近最少使用的Cache。

当需要替换内容时候,链表的最后位置就是最少被命中的位置,我们只需要淘汰链表最后的部分即可。

上面说了这么多的理论, 下面用代码来实现一个LRU策略的缓存。

我们用一个对象来表示Cache,并实现双链表,

  1public class LRUCache {
  2	/**
  3	 * 链表节点
  4	 * @author Administrator
  5	 *
  6	 */
  7	class CacheNode {
  8		……
  9	}
 10	private int cacheSize;//缓存大小
 11	private Hashtable nodes;//缓存容器
 12	private int currentSize;//当前缓存对象数量
 13	private CacheNode first;//(实现双链表)链表头
 14	private CacheNode last;//(实现双链表)链表尾
 15}
 16
 17 下面给出完整的实现这个类也被Tomcat所使用 org.apache.tomcat.util.collections.LRUCache),但是在tomcat6.x版本中已经被弃用使用另外其他的缓存类来替代它
 18
 19public class LRUCache {
 20	/**
 21	 * 链表节点
 22	 * @author Administrator
 23	 *
 24	 */
 25	class CacheNode {
 26		CacheNode prev;//前一节点
 27		CacheNode next;//后一节点
 28		Object value;//
 29		Object key;//
 30		CacheNode() {
 31		}
 32	}
 33	public LRUCache(int i) {
 34		currentSize = 0;
 35		cacheSize = i;
 36		nodes = new Hashtable(i);//缓存容器
 37	}
 38
 39	/**
 40	 * 获取缓存中对象
 41	 * @param key
 42	 * @return
 43	 */
 44	public Object get(Object key) {
 45		CacheNode node = (CacheNode) nodes.get(key);
 46		if (node != null) {
 47			moveToHead(node);
 48			return node.value;
 49		} else {
 50			return null;
 51		}
 52	}
 53
 54	/**
 55	 * 添加缓存
 56	 * @param key
 57	 * @param value
 58	 */
 59	public void put(Object key, Object value) {
 60		CacheNode node = (CacheNode) nodes.get(key);
 61
 62		if (node == null) {
 63			//缓存容器是否已经超过大小.
 64			if (currentSize >= cacheSize) {
 65				if (last != null)//将最少使用的删除
 66					nodes.remove(last.key);
 67				removeLast();
 68			} else {
 69				currentSize++;
 70			}
 71
 72			node = new CacheNode();
 73		}
 74		node.value = value;
 75		node.key = key;
 76		//将最新使用的节点放到链表头表示最新使用的.
 77		moveToHead(node);
 78		nodes.put(key, node);
 79	}
 80	/**
 81	 * 将缓存删除
 82	 * @param key
 83	 * @return
 84	 */
 85	public Object remove(Object key) {
 86		CacheNode node = (CacheNode) nodes.get(key);
 87		if (node != null) {
 88			if (node.prev != null) {
 89				node.prev.next = node.next;
 90			}
 91			if (node.next != null) {
 92				node.next.prev = node.prev;
 93			}
 94			if (last == node)
 95				last = node.prev;
 96			if (first == node)
 97				first = node.next;
 98		}
 99		return node;
100	}
101	public void clear() {
102		first = null;
103		last = null;
104	}
105	/**
106	 * 删除链表尾部节点
107	 *  表示 删除最少使用的缓存对象
108	 */
109	private void removeLast() {
110		//链表尾不为空,则将链表尾指向null. 删除连表尾删除最少使用的缓存对象
111		if (last != null) {
112			if (last.prev != null)
113				last.prev.next = null;
114			else
115				first = null;
116			last = last.prev;
117		}
118	}
119
120	/**
121	 * 移动到链表头表示这个节点是最新使用过的
122	 * @param node
123	 */
124	private void moveToHead(CacheNode node) {
125		if (node == first)
126			return;
127		if (node.prev != null)
128			node.prev.next = node.next;
129		if (node.next != null)
130			node.next.prev = node.prev;
131		if (last == node)
132			last = node.prev;
133		if (first != null) {
134			node.next = first;
135			first.prev = node;
136		}
137		first = node;
138		node.prev = null;
139		if (last == null)
140			last = first;
141	}
142	private int cacheSize;
143	private Hashtable nodes;//缓存容器
144	private int currentSize;
145	private CacheNode first;//链表头
146	private CacheNode last;//链表尾
147}

相关文章

局部敏感哈希算法(Locality Sensitive Hashing)初探

·7 分钟
前言: # 其实有了前文simhash算法的基础,局部敏感hash算法已经不存在理解上的问题了吧。。。毕竟simhash算法应该是局部敏感哈希算法的一种。。所以我就直接转载几篇我认为比较好的文档结合一下好了。。。会把比较重要的概念或者定义标记重点。

蓄水池抽样算法概述(Reservoir Sampling Algorithm)[转载]

·3 分钟
面京东被这个问题卡了QAQ,来补补这方面的课。 转自:链接 蓄水池抽样算法随机算法的一种,用来从 N 个样本中随机选择 K 个样本,其中 N 非常大(以至于 N 个样本不能同时放入内存)或者 N 是一个未知数。其时间复杂度为 O(N),包含下列步骤 (假设有一维数组 S, 长度未知,需要从中随机选择 k 个元素, 数组下标从 1 开始), 伪代码如下:

软件体系结构复习笔记

·7 分钟
Cha1 1软件架构概念: 2 是系统的一个或多个结构,它们由软件组件,组件的外部可见属性以及组件之间的关系组成。 3 组件的外部可见属性是指其他组件对该组件所做的假设。 4软件架构的多个结构: 5 静态的角度: 6 模块结构 7 分析类结构 8 类结构 9 动态的角度: 10 进程结构 11 数据流 12 控制流 13 使用结构 14 调用结构 15 层次结构 16 部署的角度: 17 物理结构 18 19架构不止是功能需求的结果 20 21Ch2: 22需求包含三要素:功能,质量,限制条件 23质量属性:系统在其生命周期过程中所表现出来的各种特征 24质量属性的关系: 25 一个质量属性的获取对其他质量属性可能产生正面或者负面的影响。 26 任何质量属性都不可能在不考虑其他属性情况下单独获取。 27质量属性举例: 28 运行时可见属性:性能,可用性,安全性 29 维护时可见属性:可修改,可扩展,可移植 30 易用性: 31 可学习性 32 可记忆性 33 错误避免 34 错误处理 35 满意度 36 37质量场景创建的参与人员: 38 最终用户 39 系统管理员 40 维护人员 41 客户 42 开发组织 43构架本身的质量属性: 44 一致性 45 正确性和完整性 46 可构建性 47生成质量属性场景的目的和意义: 48 帮助构架师生成有意义的质量属性需求 49 使质量属性需求的描述规范化 50 某一场景是一类场景的代表,系统将以完全相同的方式做出反应。 51构架的商业属性(限制): 52 上市时间 53 成本和收益 54 预期系统生命周期长短 55 目标市场 56 推出计划 57 与老系统的集成 58 59第三章: 60软件架构样式的种类: 61 以数据为中心 62 数据流 63 虚拟机 64 调用-返回 65 独立组件 66 C/S 67构架的异质性: 68 局部异质 69 层次异质 70 并行异质 71 72ISO/OSI七层参考模型: 73 应用层 74 表示层 75 会话层 76 传输层 77 网络层 78 数据链路层 79 物理层 80 81软件框架: 82 提取特定领域软件的共性部分形成的体系结构。 83框架和架构的关系: 84 框架不是构架。 85 构架确定了系统整体结构、层次划分、不同部分之间的协作等设计老驴。 86 框架比构架更具体,更偏重于技术。 87 一个框架对应一个架构,一个架构可以有多个框架。 88 89第四章: 90架构战术:影响质量属性的设计决策。 91架构策略:架构中所采用的战术的集合。 92可用性的战术: 93 错误检测的战术: 94 回声 95 心跳 96 异常 97 错误恢复的战术: 98 表决 99 主动冗余 100 被动冗余 101 备件 102 状态再同步 103 检查点/回滚 104 错误预防的战术: 105 进程监视器 106 从服务中删除 107 事物 108可修改性的战术: 109 局部化修改的战术: 110 维持语义一致性 111 预期期望的变更 112 泛化模块 113 限制可能的选择 114 防止连锁反应的战术: 115 信息隐藏 116 维持现有的接口 117 添加结构 118 添加适配器 119 提供一个占位程序 120 推迟绑定时间的战术: 121 运行时注册 122 配置文件 123 多态 124 组件更换 125 遵守已定义的协议 126实施性能的战术: 127 影响响应时间的两个基本因素: 128 资源消耗 129 阻塞时间: 130 资源争用 131 资源的可用性 132 对其他计算的依赖性 133 控制对资源需求的战术: 134 减少处理一个事件所需要的资源: 135 提高计算效率 136 减少计算开销 137 减少需要同时处理: 138 管理事件率 139 控制采样频率 140 控制系统的使用: 141 限制执行时间 142 限制队列的大小 143 资源管理的战术: 144 引入并发 145 维持数据或计算的多个副本 146 增加可用资源 147 资源仲裁常见的调度策略: 148 先进/先出 149 固定优先级:语义重要性;时限时间单调;速率单调 150 动态优先级调度:轮转;时限时间最早优先 151 静态调度 152实施安全性的战术: 153 用于抵抗攻击的战术: 154 对用户进行身份验证 155 对用户进行授权 156 维护数据的机密性 157 维护完整性 158 限制暴露的信息 159 限制访问 160 检测攻击的战术: 161 从攻击中恢复的战术: 162 回复状态 163 识别攻击者 164易用性的战术: 165 运行时战术: 166 维持任务的一个模型 167 维护用户的一个模型 168 维护系统的一个模型 169 设计时战术: 170软件架构样式与战术的关系: 171 软件架构样式是从战略层面解决质量问题,战术是从具体部署上给猪解决质量问题的局部策略。 172 173 174第五章:设计构架 175基于构架的开发步骤: 176 为软件系统创建一个商业案例 177 弄清系统需求 178 构建构架 179 正确表述此构架,并与有关各方进行交流 180 对此构架进行分析和评价 181 实现基于构架的系统并保证与构架相一致 182 系统维护时,构架文档应同步维护 183构架驱动的因素: 184 功能 185 质量 186 部分限制条件(限制条件的某个子集) 187 188良好架构的评判原则(判断题常考): 189 设计构架过程的建议: 190 架的设计应该由一门设计师来完成 191 设计师应该全面掌握对系统的技术需求,以及对各项定性指标的优先级清单。 192 构架的文档完备,并蚕蛹所有人员认可的文档形式。 193 构架设计文档应让各风险承担者积极评估。 194 通过对构架分析,得出明确的定性与定量指标。 195 构架设计应该有助于具体实现。 196 允许构架带来一定的资源争用,并给出可行的解决方案。 197 关于构架的结构的建议: 198 构架由定义良好的模块组成,各个模块的功能划分应该基于信息隐藏。 199 模块的划分应体现出相互独立的原则。 200 把计算机基础结构的特性封装在一定的模块 201 构架尽量不依赖某个特定版本的商品产品或工具。 202 产生数据的功能和使用数据的功能应分属于不同的模块。 203 对并发系统,构架应充分考虑进程与模块结构的不对应。 204 进程编写要考虑到与特定处理器的关系,并容易改变关系。 205 构架应尽量采用一些已知的设计模式。 206 207ADD构架设计的步骤: 208 样本输入 209 选择要分解的模块 210 根据下列5个步骤对模块进行求精(重点): 211 从具体的质量场景和功能需求集合中选择构架驱动因素。 212 选择满足构架驱动因素的构架模式。 213 实例化模块并根据用例分配功能,使用多个视图进行表示 214 定义子模块的接口 215 验证用例和质量场景并对其进行求精,使它们称为子模块的限制。 216 对需求进一步分解的每个模块重复上述步骤。 217创建骨架系统: 218 思想:提供一种基本能力,以一种对项目有利的顺序实现系统的功能。 219 好处: 220 提高开发效率,鼓舞士气。 221 能更早发现复杂的依赖关系。 222 使开发人员更多关注最难实现的部分。 223 能够缩短系统集成时间,降低其成本,并使集成成本更明确。 224 便于评审和测试。 225 步骤: 226 实现处理构架组件交互的软件部分 227 选择组件逐步添加到系统中。 228 逐步进行测试。 229架构师的职责: 230 了解所在组织的业务目标,使架构更好地支持业务目标。 231 规划产品的开发与严禁 232 规划和建设架构级的重用etc 233 234 235分析软件构架的原因(重要): 236 它是风险承担者之间的交流平台,是早期设计决策的体现,是可传递的模型。 237 软件质量不可能在软件开发的最后阶段追加上去,必须在设计之初就考虑到。 238 239第七章: 240构架评审: 241 成本: 242 人员时间成本 243 构架评审部门的组织开销 244 构架评审部分要求高级设计人员参与的代价(不就是人员时间成本吗。。。 245 收益: 246 及早发现构架中存在的问题 247 构架的改进 248 财务收益 249 强制位评审做准备 250 捕获构架设计的基本思想 251 验证需求的有效性 252评审实施: 253 按问题的重要性进行分类 254 强调那些与偶家相符或相悖的重要问题 255 必须记载评审中所提的每个问题 256构架评审的主要指导原则: 257 把由独立部门实施的正规的构架评审作为项目开发周期规划的一部分。 258 选择评审的最佳时间,尽早预审一次。 259 选择恰当的评审技巧 260 签署评审合同 261 限制所要品神的质量属性的个数 262 要保证评审小组中有构架方面的专家,领域专家,资料员,后勤员。 263 一定要有系统设计师。 264 收集各种场景数据,并在此基础上形成评审清单。 265 266第八章: 267架构权衡分析法(ATAM): 268 特点:不仅可以揭示出构架满足特定质量目标的情况,而且可以让我们更清楚地认识质量目标之间的联系。 269 输入:用场景集合捕获的质量要求。 270 输出: 271 简介的框架表述 272 表述清楚的业务目标 273 构架决策到质量需求的映射 274 所确定的敏感点和权衡点集合 275 有风险决策和无风险决策 276 风险主题的集合 277 阶段: 278 评估小组和项目决策者共同决定评估细节 279 评估小组收集信息和分析 280 风险承担着参与评估 281 评估小组自我检查和改进,提交书面报告 282 步骤(重点): 283 ATAM方法的表述 284 商业动机的表述 285 构架的表述 286 对构架方法进行分类 287 生成质量属性效用树 288 分析构架方法 289 集体讨论并确定场景优先级 290 再次分析构架方法 291 结果的表述 292 293第九章: 294 文档: 295 目的与作用:让不同的风险承担者都能快速找到和理解他们需要的信息。 296 基本原则:从读者的角度出发。