工程师必须了解的LRU缓存淘汰算法以及python实现过程


Posted in Python onOctober 15, 2020

大家好,欢迎大家来到算法数据结构专题,今天我们和大家聊一个非常常用的算法,叫做LRU。

LRU的英文全称是Least Recently Used,也即最不经常使用。我们看着好像挺迷糊的,其实这个含义要结合缓存一起使用。对于工程而言,缓存是非常非常重要的机制,尤其是在当下的互联网应用环境当中,起到的作用非常重要。为了便于大家更好地理解,我们从缓存的机制开始说起。

缓存

缓存的英文是cache,最早其实指的是用于CPU和主存数据交互的。早年这块存储被称为高速缓存,最近已经听不到这个词了,不知道是不是淘汰了。因为缓存的读写速度要高于CPU低于主存,所以是用来过渡数据用的。CPU从缓存当中读取数据,主存的数据也会先加载到缓存当中来,之后再进入CPU。

后来缓存有了更多的应用和意为,在后端服务当中一般用来快速响应请求。其实这里的思想和记忆化搜索有点像,我们把可能要用到的数据先存起来,后期如果要用到的话,就可以直接从内存当中读取而不是再去计算一遍了。原理也是一样的,有了缓存我们可以把要返回给用户的数据储存在内存中,当同样的请求过来的时候,我们就可以直接从内存当中读取结果,而不是再走一次链路获取数据了。

举一个很简单的例子,比如说我们打开淘宝首页会看到很多商品的推荐。其实推荐商品的流程是非常复杂的,首先要根据一定的策略去商品库当中召回商品。比如根据用户之前的行为召回和历史行为相关的商品,召回了商品之后还要进行清洗,过滤掉一些明确不感兴趣或者是非法、违规的商品。过滤了之后需要使用机器学习或者是深度学习的模型来进行点击率预测,从而将发生点击可能性最高的商品排在前面。

到这里还没结束,推荐商品列表其实也是分展位的,有些位置的商品是运营配好的,有些位置固定展示的是广告。广告往往也有自己的一条链路,还有些位置有一些其他的逻辑。这些商品的数据都拿到了之后,还要获取图片以及其他一些零零散散的信息,最后才能展示出来。因此大家可以试一下打开淘宝首页要比打开百度首页慢得多,这并不是淘宝技术差,而是因为这中间的链路实在是太长了。

工程师必须了解的LRU缓存淘汰算法以及python实现过程

我们很容易发现,对于一些经常打开淘宝的用户来说,其实没有必要每一次都把完整的链路走一遍。我们大可以把之前展示的结果存储在内存里,下一次再遇到同样请求的时候,直接从内存当中读取并且返回就可以了。这样可以节省大量的时间以及计算资源,比如在大促的时候,就可以把计算资源节省下来用在更加需要的地方。

缓存虽然好用,但是也不是万能的,因为内存是很贵的,我们不可能把所有数据都存在内存里。内存里只能放一些我们认为比较高价值的数据,在这种情况下,计算科学家们想出了种种策略来调度缓存,保持缓存当中数据的高价值。LRU就是其中一种比较常用的策略。

LRU含义

我们前面也说了,LRU的意思是最长不经常使用,也可以理解成最久没有使用。在这种策略下我们用最近一次使用的时间来衡量一块内存的价值,越久之前使用的价值也就越低,最近刚刚使用过的,后面接着会用到的概率也就越大,那么自然也就价值越高。

当然只有这个限制是不够的,我们前面也说了,由于内存是非常金贵的,导致我们可以存储在缓存当中的数据是有限的。比如说我们固定只能存储1w条,当内存满了之后,缓存每插入一条新数据,都要抛弃一条最长没有使用的旧数据。这样我们就保证了缓存当中的数据的价值都比较高,并且内存不会超过限制。

我们把上面的内容整理一下,可以得到几点要求:

1.保证缓存的读写效率,比如读写的复杂度都是O(1)

2.当一条缓存当中的数据被读取,将它最近使用的时间更新

3.当插入一条新数据的时候,弹出更新时间最远的数据

LRU原理

我们仔细想一下这个问题会发现好像没有那么简单,显然我们不能通过数组来实现这个缓存。因为数组的查询速度是很慢的,不可能做到O(1)。其次我们用HashMap好像也不行,因为虽然查询的速度可以做到O(1),但是我们没办法做到更新最近使用的时间,并且快速找出最远更新的数据。

如果是在面试当中被问到想到这里的时候,可能很多人都已经束手无策了。但是先别着急,我们冷静下来想想会发现问题其实并没有那么模糊。首先HashMap是一定要用的,因为只有HashMap才可以做到O(1)时间内的读写,其他的数据结构几乎都不可行。但是只有HashMap解决不了更新以及淘汰的问题,必须要配合其他数据结构进行。这个数据结构需要能够做到快速地插入和删除,其实我这么一说已经很明显了,只有一个数据结构可以做到,就是链表。

链表有一个问题是我们想要查询链表当中的某一个节点需要O(n)的时间,这也是我们无法接受的。但这个问题并非无法解决,实际上解决也很简单,我们只需要把链表当中的节点作为HashMap中的value进行储存即可,最后得到的系统架构如下:

工程师必须了解的LRU缓存淘汰算法以及python实现过程

对于缓存来说其实只有两种功能,第一种功能就是查找,第二种是更新。

查找

查找会分为两种情况,第一种是没查到,这种没什么好说的,直接返回空即可。如果能查到节点,在我们返回结果的同时,我们需要将查到的节点在链表当中移动位置。我们假设越靠近链表头部表示数据越旧,越靠近链表尾部数据越新,那么当我们查找结束之后,我们需要把节点移动到链表的尾部。

移动可以通过两个步骤来完成,第一个步骤是在链表上删除该节点,第二个步骤是插入到尾部:

工程师必须了解的LRU缓存淘汰算法以及python实现过程

更新

更新也同样分为两种情况,第一种情况就是更新的key已经在HashMap当中了,那么我们只需要更新它对应的Value,并且将它移动到链表尾部。对应的操作和上面的查找是一样的,只不过多了一个更新HashMap的步骤,这没什么好说的,大家应该都能想明白。

第二种情况就是要更新的值在链表当中不存在,这也会有两种情况,第一个情况是缓存当中的数量还没有达到限制,那么我们直接添加在链表结尾即可。第二种情况是链表现在已经满了,我们需要移除掉一个元素才可以放入新的数据。这个时候我们需要删除链表的第一个元素,不仅仅是链表当中删除就可以了,还需要在HashMap当中也删除对应的值,否则还是会占据一份内存。

因为我们要进行链表到HashMap的反向删除操作,所以链表当中的节点上也需要记录下Key值,否则的话删除就没办法了。删除之后再加入新的节点,这个都很简单:

工程师必须了解的LRU缓存淘汰算法以及python实现过程

我们理顺了整个过程之后来看代码:

class Node:
  def __init__(self, key, val, prev=None, succ=None):
    self.key = key
    self.val = val
    # 前驱
    self.prev = prev
    # 后继
    self.succ = succ

  def __repr__(self):
    return str(self.val)


class LinkedList:
  def __init__(self):
    self.head = Node(None, 'header')
    self.tail = Node(None, 'tail')
    self.head.succ = self.tail
    self.tail.prev = self.head

  def append(self, node):
    # 将node节点添加在链表尾部
    prev = self.tail.prev
    node.prev = prev
    node.succ = prev.succ
    prev.succ = node
    node.succ.prev = node

  def delete(self, node):
    # 删除节点
    prev = node.prev
    succ = node.succ
    succ.prev, prev.succ = prev, succ

  def get_head(self):
    # 返回第一个节点
    return self.head.succ


class LRU:
  def __init__(self, cap=100):
    # cap即capacity,容量
    self.cap = cap
    self.cache = {}
    self.linked_list = LinkedList()


  def get(self, key):
    if key not in self.cache:
      return None

    self.put_recently(key)
    return self.cache[key]

  def put_recently(self, key):
    # 把节点更新到链表尾部
    node = self.cache[key]
    self.linked_list.delete(node)
    self.linked_list.append(node)

  def put(self, key, value):
    # 能查到的话先删除原数据再更新
    if key in self.cache:
      self.linked_list.delete(self.cache[key])
      self.cache[key] = Node(key, value)
      self.linked_list.append(self.cache[key])
      return 

    if len(self.cache) >= self.cap:
      # 如果容量已经满了,删除最旧的节点
      node = self.linked_list.get_head()
      self.linked_list.delete(node)
      del self.cache[node.key]

    u = Node(key, value)
    self.linked_list.append(u)
    self.cache[key] = u

在这种实现当中我们没有用除了dict之外的其他任何工具,连LinkedList都是自己实现的。实际上在Python语言当中有一个数据结构叫做OrderedDict,它是一个字典,但是它当中的元素都是有序的。我们利用OrderedDict来实现LRU就非常非常简单,代码量也要少得多。

我们来看代码:

class LRU(OrderedDict):

  def __init__(self, cap=128, /, *args, **kwds):
    self.cap = cap
    super().__init__(*args, **kwds)

  def __getitem__(self, key):
    # 查询函数
    value = super().__getitem__(key)
    # 把节点移动到末尾
    self.move_to_end(key)
    return value

  def __setitem__(self, key, value):
    # 更新函数
    super().__setitem__(key, value)
    if len(self) > self.cap:
      oldest = next(iter(self))
      del self[oldest]

在上面一种实现当中由于只用了一个数据结构,所以整个代码要简洁许多。使用起来也更加方便,直接像是dict一样使用方括号进行查询以及更新就可以了。不过在其他语言当中可能没有OrderedDict这种数据结构,这就需要我们自己来编码实现了。

好了,今天的文章就到这里。衷心祝愿大家每天都有所收获。如果还喜欢今天的内容的话,请来一个三连支持吧~(点赞、关注、转发)

以上就是工程师必须了解的LRU缓存淘汰算法以及python实现过程的详细内容,更多关于LRU缓存淘汰算法的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
python实现数独算法实例
Jun 09 Python
Python绘制3d螺旋曲线图实例代码
Dec 20 Python
Python实现将MySQL数据库表中的数据导出生成csv格式文件的方法
Jan 11 Python
python保存数据到本地文件的方法
Jun 23 Python
Django中ORM外键和表的关系详解
May 20 Python
Django 全局的static和templates的使用详解
Jul 19 Python
Python生命游戏实现原理及过程解析(附源代码)
Aug 01 Python
Python编写打字训练小程序
Sep 26 Python
Python 中list ,set,dict的大规模查找效率对比详解
Oct 11 Python
Python包,__init__.py功能与用法分析
Jan 07 Python
python-xpath获取html文档的部分内容
Mar 06 Python
Python数据处理的三个实用技巧分享
Apr 01 Python
详解pycharm配置python解释器的问题
Oct 15 #Python
详解查看Python解释器路径的两种方式
Oct 15 #Python
几款Python编译器比较与推荐(小结)
Oct 15 #Python
python 牛顿法实现逻辑回归(Logistic Regression)
Oct 15 #Python
PyCharm 2020.2.2 x64 下载并安装的详细教程
Oct 15 #Python
Python 实现3种回归模型(Linear Regression,Lasso,Ridge)的示例
Oct 15 #Python
Python在centos7.6上安装python3.9的详细教程(默认python版本为2.7.5)
Oct 15 #Python
You might like
Thinkphp中import的几个用法详细介绍
2014/07/02 PHP
php使用Jpgraph绘制简单X-Y坐标图的方法
2015/06/10 PHP
PHP高效获取远程图片尺寸和大小的实现方法
2017/10/20 PHP
jQuery+PHP实现图片上传并提交功能
2020/07/27 PHP
[原创]js获取数组任意个不重复的随机数组元素
2010/03/15 Javascript
javascript动态加载二
2012/08/22 Javascript
jquery ui dialog实现弹窗特效的思路及代码
2013/08/03 Javascript
使用jquery清空、复位整个输入域
2015/04/02 Javascript
jQuery form插件之ajaxForm()和ajaxSubmit()的可选参数项对象
2016/01/23 Javascript
JS实用技巧小结(屏蔽错误、div滚动条设置、背景图片位置等)
2016/06/16 Javascript
JS实现iframe编辑器光标位置插入内容的方法(兼容IE和Firefox)
2016/06/24 Javascript
IE8兼容Jquery.validate.js的问题
2016/12/01 Javascript
Ext JS 实现建议词模糊动态搜索功能
2017/05/13 Javascript
Phaser.js实现简单的跑酷游戏附源码下载
2018/10/26 Javascript
详解Vue调用手机相机和相册以及上传
2019/05/05 Javascript
jquery实现二级导航下拉菜单效果实例
2019/05/14 jQuery
详解为什么Vue中不要用index作为key(diff算法)
2020/04/04 Javascript
基于JS+HTML实现弹窗提示是否确认提交功能
2020/06/17 Javascript
jquery实现穿梭框功能
2021/01/19 jQuery
如何在Vue项目中添加接口监听遮罩
2021/01/25 Vue.js
[07:39]第一届亚洲邀请赛回顾视频
2017/02/14 DOTA
Python中字符串与编码示例代码
2019/05/20 Python
tensorflow实现在函数中用tf.Print输出中间值
2020/01/21 Python
Python带参数的装饰器运行原理解析
2020/06/09 Python
python中format函数如何使用
2020/06/22 Python
HTML5 Blob对象的具体使用
2020/05/22 HTML / CSS
html5中嵌入视频自动播放的问题解决
2020/05/25 HTML / CSS
可爱的童装和鞋子:Fabkids
2019/08/16 全球购物
Shell编程面试题
2012/05/30 面试题
如何整合JQuery和Prototype
2014/01/31 面试题
四年的大学生生活自我评价
2013/12/09 职场文书
个人委托书怎么写
2014/09/17 职场文书
个人房屋转让协议书范本
2014/10/26 职场文书
优秀团员个人总结
2015/02/26 职场文书
go设置多个GOPATH的方式
2021/05/05 Golang
Python利用zhdate模块实现农历日期处理
2022/03/31 Python