工程师必须了解的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异步回调机制实现和使用方法
Nov 26 Python
Python的字典和列表的使用中一些需要注意的地方
Apr 24 Python
解决python 输出是省略号的问题
Apr 19 Python
Python实现的读写json文件功能示例
Jun 05 Python
Python中pandas模块DataFrame创建方法示例
Jun 20 Python
Python3.5常见内置方法参数用法实例详解
Apr 29 Python
pandas.cut具体使用总结
Jun 24 Python
Django的models模型的具体使用
Jul 15 Python
python GUI库图形界面开发之PyQt5多线程中信号与槽的详细使用方法与实例
Mar 08 Python
使用 django orm 写 exists 条件过滤实例
May 20 Python
Python+腾讯云服务器实现每日自动健康打卡
Dec 06 Python
Python使用mitmproxy工具监控手机 下载手机小视频
Apr 18 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
阿拉伯的咖啡与水烟
2021/03/03 咖啡文化
PHP类与对象中的private访问控制的疑问
2012/11/01 PHP
php加密解密字符串示例
2016/10/13 PHP
详解PHP5.6.30与Apache2.4.x配置
2017/06/02 PHP
jquery 上下滚动广告
2009/06/17 Javascript
javascript JSON操作入门实例
2010/04/16 Javascript
JQuery1.4+ Ajax IE8 内存泄漏问题
2010/10/15 Javascript
jQuery的实例及必知重要的jQuery选择器详解
2016/05/20 Javascript
js轮播图代码分享
2016/07/14 Javascript
Vuejs第十二篇之动态组件全面解析
2016/09/09 Javascript
javascript学习笔记_浅谈基础语法,类型,变量
2016/09/19 Javascript
一篇看懂vuejs的状态管理神器 vuex状态管理模式
2017/04/20 Javascript
JQuery 又谈ajax局部刷新
2017/11/27 jQuery
Vuex中的State使用介绍
2019/01/19 Javascript
JavaScript 面向对象程序设计详解【类的创建、实例对象、构造函数、原型等】
2020/05/12 Javascript
详解基于element的区间选择组件校验(交易金额)
2021/01/07 Javascript
[00:56]2014DOTA2国际邀请赛 DK、iG 赛前探访
2014/07/10 DOTA
Python实现全角半角转换的方法
2014/08/18 Python
Python中的一些陷阱与技巧小结
2015/07/10 Python
python中urllib.unquote乱码的原因与解决方法
2017/04/24 Python
用Python设计一个经典小游戏
2017/05/15 Python
Python内置函数 next的具体使用方法
2017/11/24 Python
Python编程使用*解包和itertools.product()求笛卡尔积的方法
2017/12/18 Python
解决python字典对值(值为列表)赋值出现重复的问题
2019/01/20 Python
python字典排序的方法
2019/10/12 Python
基于opencv实现简单画板功能
2020/08/02 Python
安装pyinstaller遇到的各种问题(小结)
2020/11/20 Python
Django框架实现在线考试系统的示例代码
2020/11/30 Python
浅析HTML5页面元素及属性
2021/01/20 HTML / CSS
俄罗斯品牌服装和鞋子在线商店:BRIONITY
2020/03/26 全球购物
经典公益广告词
2014/03/13 职场文书
2014年电信员工工作总结
2014/12/19 职场文书
入党现实表现材料
2014/12/23 职场文书
详解nginx location指令
2022/01/18 Servers
MySQL中优化SQL语句的方法(show status、explain分析服务器状态信息)
2022/04/09 MySQL
MongoDB支持的数据类型
2022/04/11 MongoDB