缓存是我们日常开发中来提高性能最直接的方式,经常会听到有人说:性能不行?是因为你没加缓存!常见的缓存有外部缓存服务以及程序内部缓存,外部缓存服务包括: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缓存及其实现的更多相关文章

  1. LRU缓存实现(Java)

    LRU Cache的LinkedHashMap实现 LRU Cache的链表+HashMap实现 LinkedHashMap的FIFO实现 调用示例 LRU是Least Recently Used 的 ...

  2. 转: LRU缓存介绍与实现 (Java)

    引子: 我们平时总会有一个电话本记录所有朋友的电话,但是,如果有朋友经常联系,那些朋友的电话号码不用翻电话本我们也能记住,但是,如果长时间没有联系了,要再次联系那位朋友的时候,我们又不得不求助电话本, ...

  3. volley三种基本请求图片的方式与Lru的基本使用:正常的加载+含有Lru缓存的加载+Volley控件networkImageview的使用

    首先做出全局的请求队列 package com.qg.lizhanqi.myvolleydemo; import android.app.Application; import com.android ...

  4. 如何用LinkedHashMap实现LRU缓存算法

    阿里巴巴笔试考到了LRU,一激动忘了怎么回事了..准备不充分啊.. 缓存这个东西就是为了提高运行速度的,由于缓存是在寸土寸金的内存里面,不是在硬盘里面,所以容量是很有限的.LRU这个算法就是把最近一次 ...

  5. 面试挂在了 LRU 缓存算法设计上

    好吧,有人可能觉得我标题党了,但我想告诉你们的是,前阵子面试确实挂在了 RLU 缓存算法的设计上了.当时做题的时候,自己想的太多了,感觉设计一个 LRU(Least recently used) 缓存 ...

  6. Java集合详解5:深入理解LinkedHashMap和LRU缓存

    今天我们来深入探索一下LinkedHashMap的底层原理,并且使用linkedhashmap来实现LRU缓存. 摘要: HashMap和双向链表合二为一即是LinkedHashMap.所谓Linke ...

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

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

  8. LRU缓存原理

    LRU(Least Recently Used)  LRU是近期最少使用的算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象. 采用LRU算法的缓存有两种:LrhCache和DisL ...

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

    一.什么是链表 和数组一样,链表也是一种线性表. 从内存结构来看,链表的内存结构是不连续的内存空间,是将一组零散的内存块串联起来,从而进行数据存储的数据结构. 链表中的每一个内存块被称为节点Node. ...

  10. [Leetcode]146.LRU缓存机制

    Leetcode难题,题目为: 运用你所掌握的数据结构,设计和实现一个  LRU (最近最少使用) 缓存机制.它应该支持以下操作: 获取数据 get 和 写入数据 put . 获取数据 get(key ...

随机推荐

  1. 读书笔记:CSAPP 11章 网络编程

    深入理解计算机系统 第11章 本章代码:Index of /afs/cs/academic/class/15213-f15/www/code/22-netprog2 其中包含本章课本示例代码,测试 T ...

  2. 记spring boot启动出现Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean.问题处理

    今天拉下了一个新的springboot工程,启动时出现了Unable to start web server; nested exception is org.springframework.cont ...

  3. a++和++a的运算区别是?

       a++和++a 都属于自增运算符, 区别是对变量a的值进行自增的时机不同.   a++是先进行取值,后进行自增.++a是先进行自增,后进行取值.  

  4. stm32芯片的SPI接口调试总结之轮询模式

    一 概念 1 组成 SPI系统可直接与各个厂家生产的多种标准外围器件接口,它只需4条线:串行时钟线(SCK).主机输入/从机输出数据线(MISO).主机输出/从机输入数据线(MOSI)和低电平有效的从 ...

  5. 前后端分离之Ajax入门

    前后端分离之Ajax入门 一.概念 Ajax(Asynchronous Javascript And XML),即是异步的JavaScript和XML,Ajax其实就是浏览器与服务器之间的一种异步通信 ...

  6. SpringMVC异常之The request sent by the client was syntactically incorrect解决方案

    最近在做SpringMVC开发的时候,直接访问后台的controller,出现如下异常 这个问题是什么原因造成的呢? 后来经过测试发现,是表单提交的内容数据类型与实体的(也就是数据表字段)的数据类型不 ...

  7. [.NET项目实战] Elsa开源工作流组件应用(一): Elsa工作流简介

    Elsa工作流简介 工作流是什么? 引用维基百科中对工作流的解释: 是对工作流程及其各操作步骤之间业务规则的抽象.概括.描述.工作流建模,即将工作流程中的工作如何前后组织在一起的逻辑和规则在计算机中以 ...

  8. drf(请求和响应)

    一 请求 源码分析 from rest_framework.request import Request class Request: def __init__(self, request, pars ...

  9. Python glob库的用法

    一.匹配指定文件夹下的所有文件 files = glob.glob("C:\\Users\\liuchunlin2\\Desktop\\测试数据\\*") print(files) ...

  10. 使用docker运行nginx服务,挂载自定义配置文件

    错误命令: 下面的方式,启动容器时,-d 后面跟一个指定容器ID的参数写在前面,导致容器不能正常启动,出现异常 docker run --name testnginx -d 7f0fd59e0094  ...