LRU缓存及其实现
缓存是我们日常开发中来提高性能最直接的方式,经常会听到有人说:性能不行?是因为你没加缓存!常见的缓存有外部缓存服务以及程序内部缓存,外部缓存服务包括:Redis、Memcached等,内部缓存就是我们可以在程序内使用类似HashMap这种方式来建立缓存,另外比如Web中常见的cdn静态资源缓存等也属于缓存,以及我们计算机中的CPU缓存,文件系统缓存等都不约而同采用相同的思路来加速性能这个指标。缓存的目的很直接就是为了提升性能,是一种空间换时间的思想,既然是缓存,那么总不能把所有的数据全都缓存起来吧?通常根据存储器的层次结构,访问速度越快的硬件单位存储价格越贵,就像CPU L1缓存的访问延时在1ns,比内存快出100倍,大小也就是在32K~128K这个范围,价格也大约是内存的400倍,同样内存的速度比固态盘要快上1500倍,但是价格也贵将近40倍,所以我们看缓存的本质就是尽量用更高成本的存储来提升性能,但是受空间的限制,我们只能尽可能的将最常用的数据缓存下来,这些就是我们通常提的缓存命中率这个概念,比如CPU缓存的缓存命中率可以达到95%或者更高,这样可以极大的提升我们程序的性能。因为我们访问的数据不断变化的,因此缓存也必然处在一个动态变化的过程中,从而保证缓存命中率的相对稳定,所以缓存的数据必须要不断淘汰掉不用的数据同时将常用的添加进去,那么具体应该怎么淘汰呢?对于缓存淘汰的策略,通常有3种:
1.FIFO (First in first out):先进先出的策略,相当于队列,非常简单。
2.LFU (Least Frequently Used):最少使用策略,这个策略的思想是如果一个数据在最近一段时间内使用很少,那么在将来一段时间被使用的可能性也很少,这个策略相当于给每个数据添加使用次数这样的标记,当缓存占满的时候优先淘汰次数少的。
3.LRU (Least Recently Used):最近最少使用策略,按照访问的时间先后作为依据,如果说数据最近被访问过,那么将来被访问的几率也会更高,因此数据相当于按照时间排序,当缓存占满的时候优先淘汰上次使用时间最久远的数据。
我们日常开发最常用的其实就是LRU缓存淘汰策略,由于LRU是按照时间先后顺序,优先淘汰最近最少使用的,那么是否要为每个数据添加一个访问时间的选项?显然这样不太优雅同时每次根据时间排序的复杂度也不低,有没有更好的实现呢?答案是肯定的,我们现在使用的LRU缓存都是基于链表来实现的,最近访问的元素在链表头部,最早访问的元素在链表尾部,实现的逻辑大致如下:
1.插入元素
首先判断数据是否在链表内,如果在的话则需要删除该数据并插入到链表头部,插入元素也相当于一次访问。
如果不再链表内,需要判断缓存是不是满了,也就是链表数据个数是不是达到预定值,如果达到了,首先淘汰掉链表尾部元素,然后将新元素插入到链表头部,否则直接将新元素插入到链表的头部。
2.读取元素
判断数据是否在链表内,如果不再直接返回空也就是告诉调用者此元素不存在;如果在链表内则需要删除该数据节点,然后插入到链表头部,表示最近访问过。
上面是大致的逻辑,总体来说很简单,几句话就可以说清楚,那么接下来我们简单实现一下:
class KVNode:
def __init__(self, k, v):
self.key = k
self.value = v
self.next = None def __str__(self) -> str:
return "Node({},{})".format(self.key, self.value) class LruCache:
def __init__(self, capacity: int):
self.cap = capacity
self.size = 0
# header 哨兵
self.header = KVNode(0, 0) def __insert_to_tail(self, key, value):
"""在链表尾部插入元素
"""
if self.size >= self.cap:
return node = self.header
while node is not None and node.next is not None:
node = node.next node.next = KVNode(key, value)
self.size += 1 def __insert_to_head(self, key, value):
"""插入元素到链表头部
"""
if self.size >= self.cap:
return node = KVNode(key, value)
node.next = self.header.next
self.header.next = node
self.size += 1 def get(self, k):
"""按照key获取cache值 O(n)
"""
node = self.header.next
prev = self.header
while node is not None:
if node.key == k:
# 移动元素到头部
prev.next = node.next
node.next = self.header.next
self.header.next = node
# self.__insert_to_head(node)
return node.value prev = node
node = node.next def put(self, k, v):
"""向lru缓存中插入元素 O(n)
"""
node = self.header
prev = None
while node.next is not None:
prev = node
node = node.next
if node.key == k:
# 将节点移动至头部
node.value = v
prev.next = node.next
node.next = self.header.next
self.header.next = node
return # 未找到元素 插入到头部
if self.size == self.cap:
# 淘汰尾部元素
prev.next = None
self.size -= 1 node = KVNode(k, v)
node.next = self.header.next
self.header.next = node
self.size += 1 def __delete_tail(self):
"""删除尾节点
"""
if self.size == 0:
return node = self.header
prev = None
while node.next is not None:
prev = node
node = node.next # 此时prev为倒数第二个节点(有可能为header)
prev.next = None
self.size -= 1 def traverse(self):
"""遍历lru cache并输出
"""
if self.size == 0:
return node = self.header.next while node is not None and node.next is not None:
print(node, end="->")
node = node.next print(node) def clear(self):
"""清空缓存
"""
self.header.next = None
self.size = 0
我们这里简单实现1个支持key,value数据节点的单向链表,然后添加了get/put这两个操作方法,来实现了lru的功能,不过很容易可以发现这样无论是get还是put时间复杂度都是O(n),如果缓存数量很大的话,获取缓存的性能就会比较慢,我们仔细看无论是get还是put都需要遍历链表来判断元素是否存在于链表内,判断元素是否存在这种场景属于典型的等值查找,我们很容易想到可以用hash表或者二叉树来实现,这样复杂度就是O(1)或者O(logn),这里我们选用hash表速度上可以达到最优,这里hash表的key就是缓存的key,hash表的value就是这个节点。不过现在又有个新的问题就是如果元素满了我们要淘汰尾部元素怎么办或者移动元素该怎么办?刚才我们在遍历时可以记录下节点的prev,而现在用hash表就只能拿到next了,这个时候我们会想到使用双向链表,同时保存前驱指针和后继指针,这样就可以方便的实现节点的删除以及尾节点的淘汰了,那么这样查找和删除的问题都解决了,但是空间占用更大了,除了hash表还有双向链表的前驱指针,这样整体的空间复杂度相较于上面来说是O(n),这也是空间换时间的实现方式,好了,那么接下来上代码:
class TwoKVNode:
"""双向链表节点
"""
def __init__(self, k, v) -> None:
self.key = k
self.value = v
self.next = None
self.prev = None def __str__(self) -> str:
return "Node({}, {})".format(self.key, self.value) class LruCachePro:
def __init__(self, capacity: int):
if capacity < 0:
capacity = 0
self.cap = capacity
self.size = 0
# header和tail 哨兵
self.header = TwoKVNode(0, 0)
self.tail = TwoKVNode(0, 0)
self.header.next = self.tail
self.tail.prev = self.header
# key -> Node
self.cache = {} def get(self, k):
"""按照key获取cache值 O(1)
"""
if k not in self.cache:
return node = self.cache[k]
# 移动node到头部
# 删除
node.prev.next = node.next
node.next.prev = node.prev
# 添加
node.prev = self.header
node.next = self.header.next
self.header.next.prev = node
self.header.next = node
return node.value def put(self, k, v):
"""向lru缓存中插入元素 O(n)
"""
if self.cap == 0:
return if k in self.cache:
node = self.cache[k]
# 更新值并移动至头部
node.value = v
# 删除
node.prev.next = node.next
node.next.prev = node.prev
# 添加
node.prev = self.header
node.next = self.header.next
self.header.next.prev = node
self.header.next = node
return # 未找到元素 直接插入到头部
if self.size == self.cap:
# 淘汰尾部元素
del self.cache[self.tail.prev.key]
self.tail.prev.prev.next = self.tail
self.tail.prev = self.tail.prev.prev
self.size -= 1 node = TwoKVNode(k, v)
node.prev = self.header
node.next = self.header.next
self.header.next.prev = node
self.header.next = node
self.cache[k] = node
self.size += 1 def traverse(self):
"""遍历lru cache并输出
"""
if self.size == 0:
return node = self.header.next
while node.next is not None:
print(node, end="->")
node = node.next print("NULL")
重点是双向链表操作这里有点绕,不过没关系细心去写都非常简单,这里类叫做LruCachePro,然后我们可以测试一下两者的性能差距:
#!/usr/bin/env python3
# coding=utf-8
import time from lru_cache import LruCache
from lru_cache import LruCachePro if __name__ == '__main__':
t1 = time.time()
lru = LruCache(10000)
for i in range(1000):
lru.put(str(i), i) t2 = time.time()
print("time1: {:.3f}s".format(t2 - t1)) t1 = time.time()
lru = LruCachePro(10000)
for i in range(10000):
lru.put(str(i), i) t2 = time.time()
print("time1: {:.3f}s".format(t2 - t1))
执行结果如下:
time1: 4.324s
time1: 0.013s
可以看到第二种hash表结合双向链表的实现带来的性能提升非常大。
关于LRU缓存淘汰算法的内容就是上面这些,感谢您的阅读,有问题希望能多多交流
LRU缓存及其实现的更多相关文章
- LRU缓存实现(Java)
LRU Cache的LinkedHashMap实现 LRU Cache的链表+HashMap实现 LinkedHashMap的FIFO实现 调用示例 LRU是Least Recently Used 的 ...
- 转: LRU缓存介绍与实现 (Java)
引子: 我们平时总会有一个电话本记录所有朋友的电话,但是,如果有朋友经常联系,那些朋友的电话号码不用翻电话本我们也能记住,但是,如果长时间没有联系了,要再次联系那位朋友的时候,我们又不得不求助电话本, ...
- volley三种基本请求图片的方式与Lru的基本使用:正常的加载+含有Lru缓存的加载+Volley控件networkImageview的使用
首先做出全局的请求队列 package com.qg.lizhanqi.myvolleydemo; import android.app.Application; import com.android ...
- 如何用LinkedHashMap实现LRU缓存算法
阿里巴巴笔试考到了LRU,一激动忘了怎么回事了..准备不充分啊.. 缓存这个东西就是为了提高运行速度的,由于缓存是在寸土寸金的内存里面,不是在硬盘里面,所以容量是很有限的.LRU这个算法就是把最近一次 ...
- 面试挂在了 LRU 缓存算法设计上
好吧,有人可能觉得我标题党了,但我想告诉你们的是,前阵子面试确实挂在了 RLU 缓存算法的设计上了.当时做题的时候,自己想的太多了,感觉设计一个 LRU(Least recently used) 缓存 ...
- Java集合详解5:深入理解LinkedHashMap和LRU缓存
今天我们来深入探索一下LinkedHashMap的底层原理,并且使用linkedhashmap来实现LRU缓存. 摘要: HashMap和双向链表合二为一即是LinkedHashMap.所谓Linke ...
- 04 | 链表(上):如何实现LRU缓存淘汰算法?
今天我们来聊聊“链表(Linked list)”这个数据结构.学习链表有什么用呢?为了回答这个问题,我们先来讨论一个经典的链表应用场景,那就是+LRU+缓存淘汰算法. 缓存是一种提高数据读取性能的技术 ...
- LRU缓存原理
LRU(Least Recently Used) LRU是近期最少使用的算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象. 采用LRU算法的缓存有两种:LrhCache和DisL ...
- 链表(上):如何实现LRU缓存淘汰算法?
一.什么是链表 和数组一样,链表也是一种线性表. 从内存结构来看,链表的内存结构是不连续的内存空间,是将一组零散的内存块串联起来,从而进行数据存储的数据结构. 链表中的每一个内存块被称为节点Node. ...
- [Leetcode]146.LRU缓存机制
Leetcode难题,题目为: 运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制.它应该支持以下操作: 获取数据 get 和 写入数据 put . 获取数据 get(key ...
随机推荐
- InputNumber 不能输入点 viewDesign 需求是 只能是整数
<InputNumber ref="xxxRef" v-model="xxx" :disabled="xxx" style=" ...
- GoFrame 优化接口的错误码和异常的思路
前言 你是否想在使用 GoFrame 的过程中,拥有一个能打印异常堆栈,能自定义响应状态码,能统一处理响应数据的接口.如果你回答是,那么,请耐心看完本文,或许会对你有所启发.若文中由表达不当之处,恳请 ...
- FastWiki v0.1.0发布!新增超多功能
FastWiki 发布 v0.1.0 https://github.com/239573049/fast-wiki/releases/tag/v0.1.0 更新日志 兼容OpenAI接口格式 删除Bl ...
- 超低功耗mcu芯片AMA3B 开发备忘之初串口打印
一 前言 对于软件工程师来说,没什么比看到一个hello world的打印更让人感觉兴奋了.调试芯片,很多人都知道,hello world这个打印意味着什么. 二 软硬件准备 1 一个AM ...
- Kettle实战视频教程
kettle实战视频教程 欢迎关注笔者的公众号: java大师, 每日推送java.kettle运维等领域干货文章,关注即免费无套路附送 100G 海量学习.面试资源哟!!个人网站: http://w ...
- apache添加php模块
实验介绍: apache本身只能发布静态网站,而添加了php模块就可以发布动态网站 一:下载php 进入php官方网址https://www.php.net/ 点击进入windows版本 下载thre ...
- Linux快速入门(八)效率工具(SSH)
环境 (1)Kali(源主机),IP:10.211.55.4/24 (2)Ubuntu(目标主机),IP:10.211.55.5/24 SSH OpenSSH用于在远程系统上安全的运行Shell,假设 ...
- Redis源码学习(1)──字符串
redis 版本:5.0 本文代码在Redis源码中的位置:redis/src/sds.c.redis/src/sds.h 源码整体结构 src:核心实现代码,用 C 语言编写 tests:单元测试代 ...
- flink scala 从Oracle同步数据到MySql
pom <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> ...
- KingbaseES V8R6 运维案例之---数据库连接访问故障分析
KingbaseES V8R6运维案例之---数据库连接访问故障分析 案例说明: 在部署KingbaseES V8R6后,正常启动数据库服务,但是通过ksql连接数据库服务访问时,出现连接到postg ...